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

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

Structures cuncurrency

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

Иерархия корутин: Все корутины должны быть запущены в контексте родительской корутины. Это позволяет отслеживать и управлять ими в рамках одного логического блока кода.

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

Автоматическая отмена: Если родительская корутина отменяется, все дочерние корутины также автоматически отменяются. Это предотвращает выполнение долгих или нежелательных операций, если они больше не нужны.

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

fun main() = runBlocking {
    launch {
        // Родительская корутина
        val childJob = launch {
            // Дочерняя корутина
            delay(2000)
            println("Дочерняя корутина завершена")
        }
        
        // Имитация задержки для родительской корутины
        delay(1000)
        println("Родительская корутина завершена, дочерняя корутина будет отменена")
        childJob.cancel() // Отмена дочерней корутины
    }
}
Синхронизация

Mutex

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

val mutex = Mutex()

suspend fun criticalSection() {
    mutex.withLock {
        // Код, который должен быть выполнен эксклюзивно
    }
}
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.

awaitAll

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

fun main() = runBlocking {
    val result = awaitAll(
        async { fetchData1() },
        async { fetchData2() }
    )
    println("Results: $result")
}

suspend fun fetchData1(): String {
    delay(1000)
    return "Data 1"
}

suspend fun fetchData2(): String {
    delay(1500)
    return "Data 2"
}
Deferred

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

Context
CoroutineContext

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

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

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

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

withContext

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

NonCancellable

Специальный объект, который используется для создания контекста корутины, который нельзя отменить. Когда корутина запускается с контекстом NonCancellable, она не реагирует на попытки отмены, пока находится в этом контексте. Основной случай использования — выполнение завершающих операций, таких как очистка ресурсов или сохранение данных, которые должны быть выполнены даже при отмене корутины.

launch {
    try {
        // Выполнение основного действия, которое можно отменить
    } finally {
        withContext(NonCancellable) {
            // Код, который не должен быть отменен
            println("Завершение ресурса")
        }
    }
}
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) {
		...
}
MainScope

Предоставляет корутины, которые запускаются в главном потоке (UI-потоке) приложения. Он упрощает управление корутинами, особенно в Android-приложениях, где необходимо взаимодействовать с пользовательским интерфейсом. Создается с использованием ContextScope(SupervisorJob() + Dispatchers.Main).

val mainScope = MainScope()

fun fetchData() {
    mainScope.launch {
        // Здесь можно выполнять асинхронные задачи, работающие с UI
        val value = loadData()
        updateUI(value)
    }
}
GlobalScope

Глобальный контекст, который позволяет запускать корутины, которые будут работать независимо от жизненного цикла конкретных компонентов (например, Activity в Android). Корутины живут на протяжении всего времени работы приложения.

fun main() {
    // Запускаем корутину в GlobalScope
    GlobalScope.launch {
        // Имитация длительной фоновой задачи
        delay(1000)
        println("Hello from GlobalScope!")
    }

    // Ожидание завершения программы, чтобы корутина успела выполниться
    Thread.sleep(1500)
}
lifecycleScope

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

class MainActivity: AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        lifecycleScope.launch {
            val result = fetchData()
            updateUI(result)
        }
    }
}
class ExampleFragment: Fragment() {

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        viewLifecycleOwner.lifecycleScope.launch {
            val result = fetchData()
            updateUI(result)
        }
    }
}
viewModelScope

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

class ExampleViewModel: ViewModel() {

    fun fetchData() {
        viewModelScope.launch {
            val result = fetchDataFromNetwork()
        }
    }
}
rememberCoroutineScope

Функция, которая позволяет получить доступ к CoroutineScope, связанному с текущим состоянием пользовательского интерфейса. Это полезно для запуска корутин в рамках Composable-функций, не нарушая принципов управления состоянием в Compose.

@Composable
fun ExampleScreen() {
    val coroutineScope = rememberCoroutineScope()

    Button(onClick = {
        coroutineScope.launch {
            val result = fetchData()
            println("Результат: $result")
        }
    }) {
        Text("Загрузить данные")
    }
}
Dispatchers
https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/limited-parallelism.html

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

Dispatcher.Default

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

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

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

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

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

fun main() = runBlocking {
    val result = withContext(Dispatchers.Default) {
        // Выполнение ресурсоёмкой операции
        computeIntensiveTask()
    }
    println("Результат вычислений: $result")
}
Dispatcher.IO

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

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

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

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

launch(Dispatchers.IO) {
    // Выполнение сетевого запроса
    val result = fetchDataFromNetwork()
    println("Результат сетевого запроса: $result")
}
Dispatcher.Main

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

coroutineScope.launch(Dispatchers.Main) {
    // Этот код выполнится в главном потоке
    textView.text = "Обновлено на главном потоке"
}
Dispatcher.Main.immediate

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

coroutineScope.launch(Dispatchers.Main.immediate) {
    // Выполнится немедленно, если уже на главном потоке
    textView.text = "Hello, World!"
}
Dispatchers.Unconfined

Запускает корутину в текущем потоке, но не привязан к какому-либо конкретному потоку. Это означает, что корутина может продолжить выполнение в другом потоке после первой приостановки.

coroutineScope.launch(Dispatchers.Unconfined) {
    println("Запущено в потоке: ${Thread.currentThread().name}")
    delay(1000)
    println("Возобновлено в потоке: ${Thread.currentThread().name}")
}
limitedParallelism

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

• Например, если у вас есть пул потоков с 4 потоками, и вы хотите, чтобы только 2 корутины обрабатывали изображения одновременно, вы можете использовать limitedParallelism(2).

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

// Диспетчер фоновых задач для приложения
val dispatcher = newFixedThreadPoolContext(4, "App Background")
// Максимум 2 потока будут обрабатывать изображения, так как это очень медленная и ресурсоемкая задача
val imageProcessingDispatcher = dispatcher.limitedParallelism(2, "Image processor")
// Максимум 3 потока будут обрабатывать JSON, чтобы избежать блокировки потоков для обработки изображений
val jsonProcessingDispatcher = dispatcher.limitedParallelism(3, "Json processor")
// Максимум 1 поток будет выполнять операции ввода-вывода (IO)
val fileWriterDispatcher = dispatcher.limitedParallelism(1, "File writer")
Exceptions
CancellationException

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

Когда корутина отменяется, выполнение её кода прекращается, но блоки finally и catch могут быть выполнены для очистки ресурсов или завершения работы. CancellationException не рассматривается как ошибка — это нормальный способ завершения работы корутины.

val job = launch {
    try {
        // Действия корутины
    } catch (e: CancellationException) {
        println("Корутина отменена: ${e.message}")
    } finally {
        println("Очистка ресурсов")
    }
}

// Отмена корутины
job.cancel(CancellationException("Причина отмены"))
CoroutineExceptionHandler

Интерфейс, который используется для обработки необработанных исключений в корутинах. Он позволяет задать кастомную логику для обработки исключений, которые возникают в корутине и не были пойманы. Его можно передать в контекст корутины и перехватывать только необработанные исключения в корутинах, запущенных через launchasync исключения обрабатываются через метод await).

val handler = CoroutineExceptionHandler { _, exception ->
    println("Исключение поймано: ${exception.localizedMessage}")
}

val job = GlobalScope.launch(handler) {
    throw RuntimeException("Ошибка в корутине")
}
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, который используется для создания корутин, не зависимых друг от друга. В отличие от обычного 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() // Ожидание завершения корутины
joinAll

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

fun main() = runBlocking {
    val job1 = launch { delay(1000); println("Job 1 done") }
    val job2 = launch { delay(2000); println("Job 2 done") }
    
    // Ожидаем завершения обеих корутин
    joinAll(job1, job2)
    println("All jobs done")
}
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 { 
    // Код, выполняемый после завершения корутины
}
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

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

• Блокировка потока: runBlocking запускает новую корутину и блокирует текущий поток до тех пор, пока все вложенные корутины не завершат свою работу. Это позволяет использовать корутины в обычных (не корутинных) функциях.

• Контекст: runBlocking принимает контекст корутины, который определяет диспетчер и другие параметры выполнения. По умолчанию он использует CoroutineScope, созданный с Dispatchers.Default.

• Использование: runBlocking обычно используется в тестах или в функции main, где необходимо выполнить корутины, но вы не хотите, чтобы основной поток продолжал выполняться, пока они не завершатся.

fun main() = runBlocking {
    launch {
        delay(1000)
        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
    }
}
yield

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

suspend fun doWork() {
    for (i in 1..5) {
        println("Working on task $i")
        yield()  // Приостановка, чтобы дать шанс другим корутинам выполниться
    }
}
ensureActive

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

suspend fun doWork() {
    for (i in 1..5) {
        ensureActive()  // Проверка, не отменена ли корутина
        println("Working on task $i")
    }
}
delay

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

fun main() = runBlocking {
    println("Start")
    
    launch {
        delay(1000) // Приостановит корутину на 1 секунду
        println("Executed after 1 second")
    }
    
    println("End")
}
suspendCancellableCoroutine

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

suspend fun fetchData(): String = suspendCancellableCoroutine { continuation ->
    val callback = object: Callback {
        override fun onSuccess(result: String) {
            continuation.resume(result) // Завершает с результатом
        }

        override fun onError(e: Throwable) {
            continuation.resumeWithException(e) // Завершает с ошибкой
        }
    }

    Api.request(callback)

    continuation.invokeOnCancellation {
        Api.cancelRequest() // Обрабатывает отмену
    }
}
Вопросы на собесе (85)
  • Обработка ошибок (5)
    1. Как обрабатывать исключения в корутинах?

      С помощью try-catch.

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

      SupervisorJob для изоляции исключений в дочерних корутинах.

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

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

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

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

    1. Разница в обработке ошибок в launch и async?

      В launch ошибки не возвращаются вызывающей функции и обрабатываются в корутине, в то время как в async исключения выбрасываются при вызове await(), что позволяет обрабатывать их в месте вызова.

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

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

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

  • Отмена корутин (9)
    1. Как отменить корутину?

      Методом cancel().

    1. Перечисли все способы отменить корутину?

      Job.cancel().

      Job.cancelChildren().

      Job.cancelAndJoin().

      Scope.cancel().

    1. Как гарантировано остановить корутину, будет ли достаточно метода cancel?

      Нет, метода cancel недостаточно для гарантированной остановки корутины. Он лишь устанавливает флаг отмены, и корутина должна регулярно проверять этот флаг с помощью функции isActive или выбрасывать CancellationException. Это позволяет корректно завершать выполнение, освобождая ресурсы. Для полного завершения рекомендуется использовать join после cancel, чтобы дождаться завершения корутины.

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

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

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

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

    1. Почему не рекомендуется перехватывать CancellationException?

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

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

      Кооперативная отмена в корутинах позволяет корутинам проверять статус отмены и завершать выполнение в заранее определенных точках, что обеспечивает освобождение ресурсов и корректное завершение работы. Отмена происходит при вызове метода cancel() на родительской корутине или с помощью withContext и других конструкций, поддерживающих отмену.

    1. Как отменить все корутины но не отменить Scope?

      Использовать cancelChildren().

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

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

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

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

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

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

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

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

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

    1. Что возвращают launch и async?

      launch возвращает объект Job.

      async возвращает объект Deferred.

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

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

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

      Использовать launch

      fun main() {
      		val job1 = launch { task1() }
      		val job2 = launch { task2() }
      
      		// Ожидаем завершения обеих задач
      		job1.join()
      		job2.join()		
      }
    1. Как запустить 2 задачи параллельно и дождаться результата?

      Использовать async

      fun main() {
      		val result1 = async { task1() }
          val result2 = async { task2() }
          val combinedResult = result1.await() + result2.await()
      }
    1. Как дождаться результата, если 1 из 2 корутин вернула исключение?

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

    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. Что выведет функция?
      suspend fun startTest() {
      		val first = scope.async {
      				delay(100)
      				println("1")
      				"First Async"
      		}
      		
      		val second = scope.async {
      				delay(400)
      				println("2")
      				"Second Async"
      		}
      		delay(300)
      		println("3")
      		println("4 - ${first.await()} ${second.await()}")
      }

      1

      3

      2

      4 - First Async Second Async

  • Dispatchers (15)
    1. Что такое диспетчер корутины?

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

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

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

    1. Для чего используются каждый из диспетчеров?

      Dispatchers.Main для работы с UI, обновления интерфейса.

      Dispatchers.Main.immediate для работы с UI, немедленного обновления интерфейса.

      Dispatchers.IO для операций ввода-вывода (сеть, файлы).

      Dispatchers.Default для вычислительно тяжелых задач.

      Dispatchers.Unconfined выполняет в текущем потоке до первой приостановки, потом переключается в зависимости от возобновления.

    1. Какой объем пула потоков на каждом из диспетчеров?

      Dispatchers.Main использует главный поток (UI) без пула потоков.

      Dispatchers.Main.immediate использует главный поток (UI) без пула потоков.

      Dispatchers.IO имеет динамический пул потоков, по умолчанию до 64 потоков, расширяется при необходимости для операций ввода-вывода.

      Dispatchers.Default использует пул потоков, равный количеству доступных ядер процессора.

      Dispatchers.Unconfined не привязан к какому-либо конкретному пулу потоков, выполняется в текущем потоке до приостановки.

    1. Разница между Dispatcher.Default и Dispathcer.IO?

      Dispatcher.Default оптимизирован для CPU-интенсивных задач и использует пул потоков равный количеству доступных ядер процессора, а Dispatcher.IO предназначен для I/O операций и имеет динамический пул потоков.

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

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

    1. Изменится ли реальный поток выполнения при смене диспетчера через withContext?

      И да и нет. Корутины используют общий пул путоков, например для Default и IO.

    1. Почему в Default можно выполнить только 4 операции одновременно (для 4 ядер), а в IO 64?

      Из-за псевдопараллельности. Современные процессоры могут содержать специализированные сетевые модули (Network Processing Units, NPUs или Network Interface Controllers, NICs), которые ускоряют обработку сетевых операций. Эти модули снимают часть нагрузки с основных ядер процессора, обрабатывая сетевые запросы более эффективно.

    1. Чем Dispatchers.Main отличается от Dispatchers.Main.immediate?

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

    1. Зачем нужен Dispatchers.Main, если в Android везде используется Dispatchers.Main.immediate?

      Для других платформ.

    1. Задача выполняется 1 сек, что выполнится быстрее: 10 тредов в цикле или 10 корутин в Dispatcher.Main?

      10 тредов быстрее, потому что в Dispatcher.Main 1 поток.

    1. Что будет если выполнять сетевые запросы на Dispatcher.Default?

      Если выполнять сетевые запросы на Dispatcher.Default, это может привести к блокированию потоков, так как Dispatcher.Default предназначен для CPU-интенсивных операций, а не для I/O задач.

    1. На каких диспетчерах выполнится код?
      withContext(Dispatchers.Main) {
          val singleValue = intFlow
              .map { ... } // В каком диспетчере?
              .flowOn(Dispatchers.IO)
              .filter { ... } // В каком диспетчере?
              .flowOn(Dispatchers.Default)
              .single() // А здесь?
      }

      map — выполнится на Dispatchers.IO, так как flowOn устанавливает диспетчер для всех операций до него.

      filter — выполнится на Dispatchers.Default, так как второй flowOn переключает диспетчер для всех операций до него и после map.

      single — выполнится на Dispatchers.Main, так как это терминальная операция, и она возвращается к диспетчеру, установленному с помощью withContext(Dispatchers.Main).

    1. Каких диспатчеров не существует?

      Default

      Computation

      IO

      Unconfined

      Single

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

      Dispatchers.IO

      Dispatchers.Computation

      Dispatchers.Main

      Dispatchers.Default

  • Job (3)
    1. Для чего нужен Job?

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

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

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

    1. Какие бывают типы Job?

      Job

      SupervisorJob

  • Scope (6)
    1. Что такое scope в корутинах?

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

    1. Какие есть стандартные Scope в Android?

      lifecycleScope состоит из Dispatchers.Main.immediate + SupervisorJob().

      viewModelScope состоит из Dispatchers.Main.immediate + SupervisorJob().

      MainScope состоит из Dispatchers.Main + SupervisorJob().

      GlobalScope состоит из EmptyCoroutineContext.

    1. Как не отменять корутину если один из ее дочерних элементов вышел из строя?

      Использовать supervisorScope.

    1. Разница между coroutineScope и supervisorScope?

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

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

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

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

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

  • suspend (9)
    1. Для чего нужно ключевое слово suspend?

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

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

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

    1. Какие ограничения есть у suspend-функций?

      Они могут быть вызваны только из другой suspend-функции или внутри корутины.

    1. Во что под капотом разворачивается suspend-функция?

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

    1. Где хранятся значения, вычисленные до приостановки корутины?

      В объекте Continuation.

    1. Какие данные хранятся в Continuation?

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

      Значения всех локальных переменных, вычисленных до приостановки.

      Контекст: Специальные данные, такие как диспетчеры потоков или информация об обработке исключений.

      Ссылка на родительский Continuation.

    1. Что такое стейт-машина корутины?

      Continuation позволяет сохранять текущее состояние выполнения корутины и передавать управление обратно в нее. Этот объект предоставляет метод resumeWith, который используется для возобновления выполнения корутины с определенным результатом или исключением. При компиляции корутины создается стейт-машина, которая управляет состоянием выполнения корутины. Это достигается с помощью switch-case, которые обрабатывают различные состояния.

    1. Чем стейт-машина suspend-функции отличается от других стейт-машин?

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

    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!
          }
      }
  • Синхронизация (5)
    1. Какие есть способы синхронизировать корутины?

      Mutex блокирует доступ к разделяемому ресурсу.

      Channels обеспечивают безопасную передачу данных между корутинами, предотвращая гонки данных.

      Dispatcher.limitedParallelism(1) сделать 1 поток.

      Atomic безопасное обновление значений в многопоточной среде без блокировок.

    1. Почему synchronized не будет работать в корутинах?

      Потому что он блокирует поток, а корутины работают асинхронно и могут сменять потоки во время выполнения.

    1. Что будет если внутри synchronized вызвать suspend-функцию?

      Вызов suspend-функции внутри synchronized блокирует поток, что нарушает асинхронность корутин и может вызвать дедлоки.

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

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

    1. Что такое Mutex?

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

  • delay (2)
    1. Как delay работает под капотом?

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

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

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

  • Context (4)
    1. Что такое контекст корутины?

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

    1. Разница между Coroutine Context и Coroutine Scope?

      CoroutineContext хранит информацию о корутине (диспетчер, job и т.д.), а CoroutineScope объединяет CoroutineContext и управляет жизненным циклом корутин, запущенных в этом контексте.

    1. Из чего состоит контекст корутины?

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

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

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

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

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

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

      Через withContext.

  • Другие (18)
    1. Для чего нужны корутины?

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

    1. Основные компоненты корутин?

      CoroutineContext

      CoroutineScope

      Dispatcher

      Job

      Deferred

    1. Как в корутинах работает структурированный параллелизм (structure cuncurrency)?

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

    1. Как в корутинах работает backpressure?

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

    1. Различие между RxJava и Coroutines?

      Модель работы: RxJava использует реактивное программирование, корутины — асинхронный код в последовательном стиле.

      Использование ресурсов: Корутины более легковесные и эффективные при высокой нагрузке.

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

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

    1. Что занимает больше ресурсов: поток или корутина?

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

    1. Почему корутины называют легковесными потоками?

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

    1. Как в корутинах отправить Single Event?

      Использовать Channel или SharedFlow с настройками replay в 0.

    1. Может ли корутина продолжить свое выполнение на другом потоке?

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

    1. За счет чего в корутинах можно писать параллельный код? кратко

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

    1. Можно ли внутри корутины запустить этот код?
      try {
      		callSomeSuspendFunction()
      } catch (e: Exception) {
      		...
      }

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

    1. Будет ли такой код работать?
      class MainActivity: AppCompatActivity() {
      
          val scope = CoroutineScope(Dispatchers.Main)
      
          override fun onCreate(savedInstanceState: Bundle?) {
              super.onCreate(savedInstanceState)
              scope.launch {
                  repeat(1000) {
                      delay(1000)
                  }
              }
          }
      }

      Да, но есть проблема: корутина, запущенная в scope, продолжит выполняться даже после уничтожения MainActivity, что приведет к утечке памяти. Чтобы этого избежать, нужно связать CoroutineScope с жизненным циклом Activity. Для этого лучше использовать lifecycleScope, который автоматически отменяет корутины при уничтожении Activity.

    1. Какой результат выведется в консоль?
      suspend fun main() {
          val scope = CoroutineScope(Job())
          scope.launch {
              delay(2000)
              error("")
          }
          scope.launch {
              repeat(5) {
                  Thread.sleep(999)
                  println("Number: $it, isActive = $isActive")
              }
          }.join()    
      }

      Через 2 секунды первая корутина завершится с исключением, что приведёт к отмене второй корутины.

    1. Что будет выведено в println?
      fun main(): Unit = runBlocking {
          val exceptionHandler = CoroutineExceptionHandler { _, _ -> print("⚠️") }
          val scope = CoroutineScope(context = SupervisorJob() + exceptionHandler)
      
          val job1 = scope.launch {
              print("A")
              delay(timeMillis = 500)
              print("B")
              throw Exception()
          }
      
          val job2 = scope.launch {
              print("C")
              delay(timeMillis = 1000)
              print("D")
          }
      
          joinAll(job1, job2)
      }

      AC⚠️BD

    1. Что будет выведено в println?
      val job = launch {
          try {
              repeat(times = 5) { i ->
                  print("A$i")
                  delay(timeMillis = 100)
              }
          } finally {
              print("B")
          }
      }
      
      delay(timeMillis = 250)
      print("C")
      job.cancelAndJoin()
      print("D")

      A0A1CBD. Корутина выводит «A0», «A1», затем «C», отменяется, выводит «B» в finally, и завершается с «D».

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

      withContext

      async

      await

      launch

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

      withMainThread

      withContext(Dispatchers.Main)

      withUl

      runOnUl