Coroutines


https://developer.android.com/kotlin/coroutines
https://developer.android.com/topic/libraries/architecture/coroutines
https://developer.android.com/kotlin/coroutines/coroutines-best-practices
https://d.android.com/kotlin/coroutines/coroutines-adv
https://d.android.com/kotlin/coroutines#features
https://kotlinlang.org/docs/coroutines-guide.html
21.01.2023https://youtu.be/ITLe4FIrrTg
28.09.2022https://youtu.be/L04cpMbNQ10
24.05.2022https://youtu.be/arUctP5yAYc
24.05.2022https://www.youtube.com/playlist?list=PL0SwNXKJbuNmsKQW9mtTSxNn00oJlYOLA
27.08.2024https://habr.com/ru/articles/838974/
11.07.2024https://habr.com/ru/articles/827866/

Coroutines

API для асинхронных операций, которые могут быть приостановлены.

• созданы для асинхронных операций (потоки - для многозадачности).

• возможность писать асинхронный код в синхронном стиле.

• когда корутина приостанавливается - она освобождает поток, когда она готова - найдет первый свободный поток и продолжит работу.

• не гарантируется выполнение на одном и том же потоке.

yield

Вызывается для проверки статуса отмены корутины. Уступить поток (или пул потоков) текущей корутины чтобы позволить другим корутинам работать на том же потоке (или пуле потоков).

Здесь мы запускаем две корутины, каждая из которых вызывает функцию yield, чтобы позволить другой корутине работать в потоке main. Мы видим вывод первой корутины, после чего она вызывает команду yield. Это приостанавливает работу первой корутины и позволяет выполняться второй. Аналогичным образом вторая корутина также вызывает функцию yield и позволяет первой корутине возобновить выполнение.

fun main() = runBlocking{
    try {
        val job1 = launch {
            repeat(20) {
                println("processing job 1: ${Thread.currentThread().name}")
                yield()
            }
        }
        val job2 = launch {
            repeat(20) {
                println("processing job 2: ${Thread.currentThread().name}")
                yield()
            }
        }
        job1.join()
        job2.join()
    } catch (e: CancellationException) {
        // код очистки
    }
}

processing job 1: main
processing job 2: main
processing job 1: main
processing job 2: main
processing job 1: main

newSingleThreadContext

Вручную запускает поток с указанным именем. API деликатное, использовать осторожно.

launch(newSingleThreadContext("Custom Thread")) {
    println("coroutine flow: ${Thread.currentThread().name}")"}

newFixedThreadPoolContext

Позволяет создать собственный отдельный пул потоков. Все корутины по умолчанию выполняются в CommonPool. Все ресурсы будут освобождены после того как программа отработает. API деликатное, использовать осторожно.

val pool: ExecutorCoroutineDispatcher = newFixedThreadPoolContext(8, "myPool")
Builders

launch

Функция, запускающая новую корутину в заданном контексте, которая не блокирует текущий поток и возвращает объект Job, позволяющий управлять выполнением корутины. Выстрелил и забыл.

// корутина запускается в рамках runBlocking и выполняется асинхронно, а job.join() ожидает её завершения.
fun main() = runBlocking {
    val job = launch {
		    // Код корутины
        delay(1000)
        println("Hello from coroutine!")
    }

    job.join()  // Ожидание завершения корутины
}

async

Функция, которая запускает новую корутину и возвращает объект Deferred, позволяющий получить результат выполнения корутины в будущем. Позволяет использовать функцию await() для получения результата, который будет доступен после завершения корутины.

// async запускает корутину, возвращающую значение 42, и мы используем await() для получения этого значения после её завершения.
fun main() = runBlocking {
    val deferred: Deferred<Int> = async {
        // Код корутины, возвращающей результат
        delay(1000)
        42
    }

    val result = deferred.await()  // Ожидание завершения корутины и получение результата
    println("The result is $result")
}

await

Используется в корутинах для ожидания завершения корутины, запущенной с помощью async.

Deferred

Представляет собой задачу, результат которой будет доступен в будущем. Это значение, которое ещё не вычислено, но будет доступно после завершения асинхронной операции. Обычно создаётся с помощью функции async.

Structures cuncurrency

Механизм, предоставляющий иерархическую структуру для организации работы coroutine. Все принципы строятся на основе CoroutineScope и оношения родитель-ребенок у Job.

• scope хранит все ссылки на coroutines, запущенные в нем.

• отмена scope - отмена coroutines.

• отмена родительской Job приведет к отмене всех дочерних Job.

• отмена дочерней Job приведет к отмене родительской Job и отмене всех других дочерних Job.

Синхронизация

Mutex

Механизм синхронизации, который позволяет управлять доступом к разделяемым ресурсам между корутинами. Он блокирует доступ к ресурсу, пока один поток или корутина его не освободит.

val mutex = Mutex()

suspend fun criticalSection() {
    mutex.withLock {
        // Код, который должен быть выполнен эксклюзивно
    }
}
Job

Основной механизм управления корутинами в Kotlin, который позволяет эффективно управлять жизненным циклом корутин, их отменой и композицией.

• Когда вы запускаете корутину с помощью функций launch или async, возвращается объект Job, который представляет корутину.

• На основе Job можно организовать иерархию parent-child.

fun main() = runBlocking {
    val job: Job = launch {
        // Код корутины
        delay(1000)
        println("Coroutine finished")
    }

    // Ожидание завершения корутины
    job.join()
    println("Main program finished")
}

SupervisorJob

Специальный тип Job в Kotlin Coroutines, который используется для создания корутин, не зависимых друг от друга. В отличие от обычного Job, который отменяет все дочерние корутины при отмене или ошибке одной из них, SupervisorJob позволяет дочерним корутинам продолжать работу, даже если одна из них завершится с ошибкой.

fun main() = runBlocking {
    val supervisor = SupervisorJob()
    
    val scope = CoroutineScope(Dispatchers.Default + supervisor)

    val child1 = scope.launch {
        println("Child 1 started")
        throw RuntimeException("Error in Child 1")
    }

    val child2 = scope.launch {
        println("Child 2 started")
        delay(1000)
        println("Child 2 finished")
    }

    child1.join() // Ожидание завершения Child 1
    child2.join() // Ожидание завершения Child 2

    println("Main program finished")
}

Lifecycle

New Корутина создана, но ещё не запущена.

Active Корутина запущена и выполняется.

Completing Корутина завершается.

Completed Корутина успешно завершилась без ошибок.

Cancelling Корутина отменяется.

Canceled Корутина была отменена до завершения (вызван метод cancel у Job)

start

Запускает корутину, если она ещё не запущена.

val job = GlobalScope.launch { /* корутина */ }
job.start() // Явный запуск корутины

join

Блокирует текущий поток до тех пор, пока корутина не завершится.

val job = GlobalScope.launch { /* корутина */ }
job.join() // Ожидание завершения корутины

cancel

Отменяет выполнение корутины.

val job = GlobalScope.launch { /* корутина */ }
job.cancel() // Отменяет корутину

cancelAndJoin

Отменяет выполнение корутины и ожидает её завершения.

val job = GlobalScope.launch { /* корутина */ }
job.cancelAndJoin() // Отмена корутины и ожидание её завершения

isActive

Проверяет, активна ли корутина (т.е., она запущена и не отменена).

val job = GlobalScope.launch { /* корутина */ }
if (job.isActive) { /* корутина активна */ }

isCancelled

Проверяет, была ли корутина отменена.

val job = GlobalScope.launch { /* корутина */ }
if (job.isCancelled) { /* корутина отменена */ }

isCompleted

Проверяет, завершена ли корутина (успешно или из-за ошибки).

val job = GlobalScope.launch { /* корутина */ }
if (job.isCompleted) { /* корутина завершена */ }

invokeOnCompletion

Устанавливает обработчик, который будет вызван, когда корутина завершится.

val job = GlobalScope.launch { /* корутина */ }
job.invokeOnCompletion { 
    // Код, выполняемый после завершения корутины
}
Context

CoroutineContext

Определяет поведение корутины. Является набором параметров для выполнения coroutines.

• каждая корутина выполняется в каком-либо контексте.

• явно не создается, задается в scope либо при запуске корутины (в launch).

• можно объединить несколько контекстов в один

withContext

Переносит выполнение текущей корутины на новый контекст, в большинстве случаев на новый диспетчер.

Scope

CoroutineScope

Отслеживает любую корутину, которую создает, используя launch или async. Scope хранит все ссылки на корутины, запущенные в нем. Scope может отменить выполнение всех дочерних корутин, если возникнет ошибка или операция будет отменена.

• отмена дочернего scope приведет к отмене родительского scope и наоборот.

• scope будет завершен успешно, когда выполнятся все корутины в нем.

• если запустить бесконечный цикл внутри scope и отменить его - цикл закончит выполнение.

supervisorScope

Аналог coroutineScope. SupervisorJob для scope. Переопределяет Job контекст, поэтому функция не отменяется, когда дочерний элемент выдает исключение.

supervisorScope {
    ...
}

withTimeout

Устанавливает ограничение по времени для выполнения работы. Если время вышло - корутина отменится и вызовется TimeoutCancellationException.

withTimeout(1_000L) {
		...
}

withTimeoutOrNull

Устанавливает ограничение по времени для выполнения работы. Если время вышло вернет результат null.

val result = withTimeoutOrNull(1_000L) {
		...
}

GlobalScope

Специальный CoroutineScope, который не привязан к какай-либо Job. Все корутины, запущенные в рамках него будут работать до своей остановки или остановки процесса. Использование может легко привести к утечке памяти.

• этот scope невозможно отменить.

• использование нарушает принципы structured concurrency и может привести к утечке памяти.

viewModelScope

Отменяется при очистке ViewModel.

rememberCoroutineScope

Отменяется когда компонуемый объект выходит из рекомпозиции.

viewLifecycleOwner.lifecycleScope

Отменяется по окончании жизненного цикла Android View.

Dispatchers

Dispatcher - объект, который определяет, на каком потоке или пуле потоков будет выполняться корутина. Он управляет контекстом выполнения корутины, что позволяет эффективно распределять задачи и управлять ресурсами.

Dispatcher.Default

Используется для выполнения корутин, требующих значительных вычислительных ресурсов.

• Использует пул потоков, размер которого обычно соответствует числу доступных процессоров (или ядер).

• Пул потоков автоматически масштабируется в зависимости от нагрузки.

• Не рекомендуется для I/O операций так как использует пул с ограниченным числом потоков, блокировка одного из этих потоков может снизить общую производительность приложения, особенно если таких операций много.

• Используется по умолчанию в launch и async.

• Ограничить количество потоков: Dispatchers.Default.limitedParallelism(3).

• Под капотом FixedThreadPool.

Dispatcher.IO

Предназначен для выполнения операций ввода/вывода, таких как сетевые запросы и операции с файлами.

• Пул потоков имеет динамический размер. Количество потоков может увеличиваться в зависимости от текущей нагрузки и количества ожидающих задач.

• Если все потоки заняты, новые задачи помещаются в очередь и обрабатываются по мере освобождения потоков.

• Под капотом CachedThreadPool.

Dispatcher.Main

Предназначен для работы с основным потоком пользовательского интерфейса в Android. Добавляет задачу в очередь MainLooper.

Dispatcher.Main.immediate

Специальная версия Dispatchers.Main, которая предназначена для обеспечения немедленного исполнения корутин на основном потоке, если это возможно. Гарантирует, что корутины будут выполнены немедленно, если основной поток уже активен и готов к выполнению. Это полезно, когда требуется убедиться, что задачи на основном потоке выполняются как можно быстрее. Полезен, когда необходимо минимизировать задержку выполнения задач на основном потоке.

Dispatchers.Unconfined

Не привязан к конкретному потоку или диспетчеру. Начинается выполнение корутины на текущем потоке, а затем продолжает выполнение в том контексте, где будет возобновлена корутина. Это означает, что выполнение может происходить на любом потоке или в любом контексте, где корутина будет приостановлена и возобновлена. Полезен для корутин, которые не требуют привязки к конкретному потоку или когда вам не важен поток, на котором корутина будет выполняться. Обычно он используется для тестирования или в случаях, когда выполнение корутины не зависит от потока.

Exceptions

CancellationException

Специальное исключение для обработки отмены выполнения корутин
• вызов cancel приведет к этому исключению

try {
    ...
} catch (e: CancellationException) {
    // обязательно пробрасываем дальше
    throw e
} catch (e: Exception) {
    // обрабатываем другие исключения
}

CoroutineExceptionHandler

Определить поведение для всех необработанных исключений, которые происходят в текущем контексте выполнения корутин. Общий catch-блок. Если исключение захвачено, то дальше к родителю оно пробрасываться не будет. Под капотом Thread.uncatchExceptionHandler.

• вызывается в последнюю очередь, когда произошла ошибка. на момент вызова корутина завершена.

• может быть вызван на любом потоке. переопределять Dispatchers.Main если обновляем UI.

• не будет ловить CancellationException так как они не являются ошибками выполнения корутин.

val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
    withContext(Dispatchers.Main) {
		    view.showError()
    }
}
CoroutineScope(exceptionHandler)
scope.launch(exceptionHandler)

NonCancellable

Использовать для освобождения ресурсов при исключении
• только для использования в withContext()

val inputStream: InputStream
try {
    doSomethingLong(inputStream)
} catch (e: Exception) {
    // обрабатываем исключение
} finally {
    withContext(NonCancellable) {
        shutdown(inputStream)
    }
}
try {
    coroutineScope {
        ...
    }
} catch (e: Exception) {
    // обработать все исключения scope
}
/**
 * Не вызывается в случае CancelException и если исключение произошло до задания CompletionHandler
 */
job.invokeOnCompletion { cause: Throwable? ->
    if (cause != null) {
        // произошла ошибка
    } else {
        // корутина успешно выполнена
    }
}
suspend

Используется для обозначения функций, которые могут быть приостановлены и возобновлены в будущем. Такие функции могут выполнять асинхронные задачи и могут вызываться только из других suspend-функций или корутин. Это позволяет эффективно управлять асинхронными операциями без блокировки потоков. Такие функции могут использовать функции-расширения корутин, например, delay, withContext и другие, для работы с асинхронными задачами.

// Определение suspend-функции
suspend fun doWork() {
    delay(1000L) // Приостанавливает выполнение на 1 секунду
    println("Work done!")
}

fun main() = runBlocking { // Запускает корутину
    launch {
        doWork() // Вызов suspend-функции внутри корутины
    }
    println("Starting work...")
}

Continuation

Когда suspend-функция вызывается, она получает объект Continuation, который управляет состоянием функции и её продолжением. Continuation хранит информацию о том, где нужно продолжить выполнение после приостановки.

При выполнении suspend-функции, выполнение может быть приостановлено в точке, где происходит вызов другой приостановленной функции или задержка (delay). Этот процесс преобразует функцию в корутину, которая работает асинхронно и может быть возобновлена позже.

Состояние функции (например, локальные переменные и текущее положение) сохраняется в объекте Continuation. Когда функция приостанавливается, состояние сохраняется, и выполнение возобновляется с этого состояния, когда корутина продолжает выполнение.

Когда suspend функция возобновляется, объект Continuation используется для продолжения выполнения с точки, на которой функция была приостановлена. Это позволяет функции продолжить выполнение, как если бы она никогда не была приостановлена.

suspendCoroutine

Позволяет интегрировать некорутинный код (например, использующий коллбэки) с корутинами.

// Пример callback hell — когда асинхронные вызовы вложены друг в друга, что делает код сложным для чтения и сопровождения
fun fetchData(callback: (String) -> Unit) {
    getNetworkData { result1 ->
        getDatabaseData(result1) { result2 ->
            processResult(result2) { finalResult ->
                callback(finalResult)
            }
        }
    }
}

// Пример того, как можно обернуть колбэки с помощью suspendCoroutine
suspend fun fetchData(): String = suspendCoroutine { continuation ->
    getNetworkData { result1 ->
        getDatabaseData(result1) { result2 ->
            processResult(result2) { finalResult ->
                // Возвращаем результат через continuation.resume
                continuation.resume(finalResult)
            }
        }
    }
}

// Идеальное решение
suspend fun fetchData(): String {
    val networkData = getNetworkDataAsync()
    val databaseData = getDatabaseDataAsync(networkData)
    return processResultAsync(databaseData)
}
runBlocking

Функция, которая запускает корутину и блокирует текущий поток до ее завершения. Она используется для запуска корутин в обычном коде, например, в функциях main или в тестах. Выполняется в контексте вызывающего потока. Функция runBlocking синхронно ожидает завершения всех корутин внутри нее. Внутри runBlocking можно использовать другие корутины и функции, такие как launch и async, для выполнения асинхронных задач.

fun main() = runBlocking { // Запускает корутину и блокирует текущий поток
    // Запускает другую корутину внутри runBlocking
    launch {
        delay(1000L)
        println("World")
    }
    println("Hello")
}
runCatching

Функция, которая используется для выполнения блока кода и обработки возможных исключений. Она возвращает результат выполнения блока в виде объекта Result, который упрощает работу с результатами и ошибками. Позволяет избежать использования конструкции try-catch. Предоставляет методы, такие как getOrNull() и getOrElse(), для удобного извлечения результата или обработки ошибки.

fun main() {
    // Выполнение блока кода и получение результата в виде объекта Result
    val result: Result<Int> = runCatching {
        // Код, который может вызвать исключение
        "123".toInt()
    }

    // Получение результата или обработки ошибки
    val value: Int? = result.getOrNull() // Возвращает значение, если не было исключения
    val error: Throwable? = result.exceptionOrNull() // Возвращает исключение, если оно произошло

    println("Value: $value") // Выводит: Value: 123
    println("Error: $error") // Выводит: Error: null (поскольку исключение не произошло)
}
CoroutineName

Позволяет назначить уникальное имя корутине для удобства отладки. Используется для улучшения читаемости логов и диагностики, помогая легче отслеживать корутины в процессе их выполнения. Отображается в Android Studio Profiler. Не влияет на логику выполнения корутины и не изменяет ее поведение.

// Создание корутины с именем
val job = CoroutineScope(Dispatchers.Default + CoroutineName("MyCoroutine")).launch {
    // Your coroutine code here
}
// При создании вложенных корутин имя будет унаследовано от родительской корутины.
val job = CoroutineScope(Dispatchers.Default + CoroutineName("ParentCoroutine")).launch {
    launch(CoroutineName("ChildCoroutine")) {
        // Child coroutine code here
    }
}
Kotlin Coroutines. Вопросы на собесе
  1. Что такое корутины в котлине? Для чего они нужны?
  1. Для чего нужно ключевое слово suspend?
  1. Во что под капотом разворачивается suspend-функция?
  1. Для чего используются каждый из диспетчеров?
  1. Как в корутинах работает структурированный параллелизм (structure cuncurrency)?
  1. Как в корутинах работает backpressure?
  1. Как в корутинах запустить горячий и холодный потоки?
  1. Как не отменять корутину если один из ее дочерних элементов вышел из строя?
  1. Разница между coroutineScope и supervisorScope?
  1. Чем Dispatchers.Main отличается от Dispatchers.Main.immediate?
  1. Из чего состоит контекст корутины?

    CoroutineDispatcher - определяет поток или диспетчер, на котором будет выполняться корутина.

    CoroutineName - устанавливает имя для отладки.

    CoroutineExceptionHandler - для обработки исключений.

    Job - управляет жизненным циклом корутины.

    SupervisorJob - обеспечить более гибкое управление ошибками и отменой корутин.

  1. Чем корутины отличаются от обычных потоков?

    Отличаются тем, что они легковесны, не блокируют потоки при переключении контекста, позволяют писать асинхронный код в синхронном стиле и интегрируются с механизмами управления жизненным циклом для более эффективного управления задачами.

  1. Чем Thread.sleep отличается от delay?

    Thread.sleep блокирует текущий поток, тогда как delay приостанавливает выполнение корутины, освобождая поток для других задач.

  1. Какие есть способы синхронизировать корутины?

    Mutex, Atomic, limitedParallelism(1)

  1. Как обрабатывать исключения в корутинах?

    Исключения в корутинах можно обрабатывать с помощью try-catch, CoroutineExceptionHandler для необработанных исключений, и SupervisorJob для изоляции исключений в дочерних корутинах.

  1. Что нужно учитывать при обработки ошибок через try-catch в корутинах?

    При обработке ошибок через try-catch в корутинах нужно учитывать, что исключения (CancellationException), возникшие в корутинах, могут отменить корутину, и try-catch работает только в пределах текущего контекста корутины.

  1. Если передать CoroutineExceptionHandler в async, как обработается исключение?

    Корутина выполнится, исключение будет в Deferred.

  1. Как отменить корутину?

    Методом cancel()

  1. Какое поведение у корутины при вызове метода cancel?

    При вызове метода cancel корутина помечается как Cancelled, и её выполнение прекращается при следующем проверке на отмену.

  1. Как корутина понимает что она отменена?

    Корутина понимает, что она отменена, когда проверяет состояние своей Job или когда происходит вызов метода isActive. Отмена сигнализируется выбросом исключения CancellationException, которое корутина может поймать и обработать, если это предусмотрено.

  1. Для чего нужен Job?

    Управляет временем жизни корутины и позволяет отменить ее выполнение, а также отслеживать ее статус.

  1. Для чего нужен SupervisorJob?

    Используется для управления иерархией корутин, позволяя дочерним корутинам завершаться независимо — сбой одной корутины не отменяет остальных.

  1. Что такое scope в корутинах?

    Scope в корутинах управляет областью выполнения корутин, их временем жизни и отменой, обеспечивая контекст для запуска и управления корутинами.

  1. Как переключить контекст выполнения корутины?

    Через withContext.

  1. Что произойдет с корутиной при переключении с Dispatcher.Default на Dispathcer.IO?

    Если диспетчеры используют разные потоки, то фактически корутина будет перенесена на новый поток. Это происходит за счет переноса контекста и выполнения задачи на нужном пуле потоков. Переключение между диспетчерами требует переключения контекста, что может быть дорогой операцией, так как это связано с изменением потоков и управлением контекстом выполнения. Однако это меньше затраты по сравнению с созданием нового потока или корутины с нуля.

  1. Как запустить корутину? (Какие существуют билдеры корутин)

    С помощью билдеров корутин launch или async.

  1. Разница между launch и async?

    launch запускает корутину без возврата результата и возвращает Job, в то время как async также запускает корутину, но возвращает Deferred, который используется для получения результата выполнения.

  1. Как дождаться выполнения launch?

    Использовать метод join(). Он приостановит выполнение текущей корутины до завершения корутины, запущенной через launch.

  1. Как дождаться выполнения всех корутин в scope?

    Использовать метод joinAll().

  1. Какие есть диспетчеры у корутин?

    Dispatchers.Default Dispatchers.IO Dispatchers.Main Dispatcher.Main.immediate Dispatchers.Unconfined

  1. Можно ли вызвать suspend-функцию из Java-кода?

    Напрямую нет, но можно обернуть ее в runBlocking и таким образом вызвать.

    // Это suspend-функция
    suspend fun suspendFunction(): String {
        delay(1000)
        return "Hello from suspend function!"
    }
    
    // Это функция, которая оборачивает suspend-функцию и делает её доступной для Java-кода
    fun callSuspendFunction(): String {
        return runBlocking {
            suspendFunction()
        }
    }
    public class Main {
        public static void main(String[] args) {
            // Вызов функции из Java-кода
            String result = KotlinCode.callSuspendFunction();
            System.out.println(result); // Выведет: Hello from suspend function!
        }
    }
  1. Каких диспатчеров не существует?

    Default

    Computation

    IO

    Unconfined

    Single

  1. Какой метод используется для запуска coroutine в пределах существующей coroutine, ожидая ее завершения?

    withContext

    async

    await

    launch

  1. Какой метод используется для выполнения coroutine в главном потоке UI в Android?

    withMainThread

    withContext(Dispatchers.Main)

    withUl

    runOnUl

  1. Какой тип coroutine контекста предназначен для задач, требующих значительных вычислительных ресурсов?

    Dispatchers.IO

    Dispatchers.Computation

    Dispatchers.Main

    Dispatchers.Default

  1. В каком случае выполнение будет параллельным, а в каком последовательным?
    fun sum1(): Int {
    		val a = async { suspendFun() }.await()
    		val b = async { suspendFun() }.await()
    		return a + b
    }
    
    fun sum2(): Int {
    		val a = async { suspendFun() }
    		val b = async { suspendFun() }
    		return a.await() + b.await()
    }

    sum1 последовательно.

    sum2 параллельно.

  1. Что произойдет в обоих случаях, на каком этапе будет исключение?
    fun someFun1() {
    		val a: Int = async { suspendFun) }
    }
    
    fun someFun2() {
    		launch { suspendFun() }
    }
    
    private fun suspendFun(): Int {
    		throw RuntimeException)
    }

    В someFun1 исключение будет при вызове await в объекте Deferred.

    В someFun2 исключение будет при запуске launch.

  1. Что произойдет при вызове данной функции?
    suspend fun unknownFunction() {
        coroutineScope {
            launch(Job()) {
                delay(1000)
                println("Hello!")
            }
            cancel()
        }
    }

    Запустится корутина launch, после задержки в 1 секунду, будет выведено "Hello!", потом скоуп отменится.

    Запустится корутина launch, scope будет сразу же отменен, отменится корутина launch, ничего выведено не будет.

    Запустится корутина launch, scope будет сразу же отменен, однако корутина launch продолжит свою работу, через секунду будет выведено "Hello!".

    Нет правильного ответа.