Coroutines
https://kotlinlang.org/docs/coroutines-guide.html |
21.01.2023 | https://youtu.be/ITLe4FIrrTg |
28.09.2022 | https://youtu.be/L04cpMbNQ10 |
24.05.2022 | https://youtu.be/arUctP5yAYc |
24.05.2022 | https://www.youtube.com/playlist?list=PL0SwNXKJbuNmsKQW9mtTSxNn00oJlYOLA |
27.08.2024 | https://habr.com/ru/articles/838974/ |
11.07.2024 | https://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. Вопросы на собесе
- Что такое корутины в котлине? Для чего они нужны?
- Для чего нужно ключевое слово suspend?
- Во что под капотом разворачивается suspend-функция?
- Для чего используются каждый из диспетчеров?
- Как в корутинах работает структурированный параллелизм (structure cuncurrency)?
- Как в корутинах работает backpressure?
- Как в корутинах запустить горячий и холодный потоки?
- Как не отменять корутину если один из ее дочерних элементов вышел из строя?
- Разница между coroutineScope и supervisorScope?
- Чем Dispatchers.Main отличается от Dispatchers.Main.immediate?
- Из чего состоит контекст корутины?
•
CoroutineDispatcher
- определяет поток или диспетчер, на котором будет выполняться корутина.•
CoroutineName
- устанавливает имя для отладки.•
CoroutineExceptionHandler
- для обработки исключений.•
Job
- управляет жизненным циклом корутины.•
SupervisorJob
- обеспечить более гибкое управление ошибками и отменой корутин.
- Чем корутины отличаются от обычных потоков?
Отличаются тем, что они легковесны, не блокируют потоки при переключении контекста, позволяют писать асинхронный код в синхронном стиле и интегрируются с механизмами управления жизненным циклом для более эффективного управления задачами.
- Чем Thread.sleep отличается от delay?
Thread.sleep
блокирует текущий поток, тогда какdelay
приостанавливает выполнение корутины, освобождая поток для других задач.
- Какие есть способы синхронизировать корутины?
Mutex
,Atomic
,limitedParallelism(1)
- Как обрабатывать исключения в корутинах?
Исключения в корутинах можно обрабатывать с помощью
try-catch
,CoroutineExceptionHandler
для необработанных исключений, иSupervisorJob
для изоляции исключений в дочерних корутинах.
- Что нужно учитывать при обработки ошибок через try-catch в корутинах?
При обработке ошибок через
try-catch
в корутинах нужно учитывать, что исключения (CancellationException
), возникшие в корутинах, могут отменить корутину, иtry-catch
работает только в пределах текущего контекста корутины.
- Если передать CoroutineExceptionHandler в async, как обработается исключение?
Корутина выполнится, исключение будет в
Deferred
.
- Как отменить корутину?
Методом
cancel()
- Какое поведение у корутины при вызове метода cancel?
При вызове метода
cancel
корутина помечается какCancelled
, и её выполнение прекращается при следующем проверке на отмену.
- Как корутина понимает что она отменена?
Корутина понимает, что она отменена, когда проверяет состояние своей
Job
или когда происходит вызов методаisActive
. Отмена сигнализируется выбросом исключенияCancellationException
, которое корутина может поймать и обработать, если это предусмотрено.
- Для чего нужен Job?
Управляет временем жизни корутины и позволяет отменить ее выполнение, а также отслеживать ее статус.
- Для чего нужен SupervisorJob?
Используется для управления иерархией корутин, позволяя дочерним корутинам завершаться независимо — сбой одной корутины не отменяет остальных.
- Что такое scope в корутинах?
Scope в корутинах управляет областью выполнения корутин, их временем жизни и отменой, обеспечивая контекст для запуска и управления корутинами.
- Как переключить контекст выполнения корутины?
Через
withContext
.
- Что произойдет с корутиной при переключении с Dispatcher.Default на Dispathcer.IO?
Если диспетчеры используют разные потоки, то фактически корутина будет перенесена на новый поток. Это происходит за счет переноса контекста и выполнения задачи на нужном пуле потоков. Переключение между диспетчерами требует переключения контекста, что может быть дорогой операцией, так как это связано с изменением потоков и управлением контекстом выполнения. Однако это меньше затраты по сравнению с созданием нового потока или корутины с нуля.
- Как запустить корутину? (Какие существуют билдеры корутин)
С помощью билдеров корутин
launch
илиasync
.
- Разница между launch и async?
launch
запускает корутину без возврата результата и возвращаетJob
, в то время какasync
также запускает корутину, но возвращаетDeferred
, который используется для получения результата выполнения.
- Как дождаться выполнения launch?
Использовать метод
join()
. Он приостановит выполнение текущей корутины до завершения корутины, запущенной через launch.
- Как дождаться выполнения всех корутин в scope?
Использовать метод
joinAll()
.
- Какие есть диспетчеры у корутин?
Dispatchers.Default
Dispatchers.IO
Dispatchers.Main
Dispatcher.Main.immediate
Dispatchers.Unconfined
- Можно ли вызвать 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! } }
- Каких диспатчеров не существует?
•
Default
• Computation
•
IO
•
Unconfined
• Single
- Какой метод используется для запуска coroutine в пределах существующей coroutine, ожидая ее завершения?
•
withContext
•
async
•
await
•
launch
- Какой метод используется для выполнения coroutine в главном потоке UI в Android?
• withMainThread
•
withContext(Dispatchers.Main)
• withUl
• runOnUl
- Какой тип coroutine контекста предназначен для задач, требующих значительных вычислительных ресурсов?
•
Dispatchers.IO
•
Dispatchers.Computation
•
Dispatchers.Main
•
Dispatchers.Default
- В каком случае выполнение будет параллельным, а в каком последовательным?
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 параллельно.
- Что произойдет в обоих случаях, на каком этапе будет исключение?
fun someFun1() { val a: Int = async { suspendFun) } } fun someFun2() { launch { suspendFun() } } private fun suspendFun(): Int { throw RuntimeException) }
• В someFun1 исключение будет при вызове
await
в объектеDeferred
.• В someFun2 исключение будет при запуске
launch
.
- Что произойдет при вызове данной функции?
suspend fun unknownFunction() { coroutineScope { launch(Job()) { delay(1000) println("Hello!") } cancel() } }
• Запустится корутина
launch
, после задержки в 1 секунду, будет выведено "Hello!", потом скоуп отменится.• Запустится корутина
launch
, scope будет сразу же отменен, отменится корутинаlaunch
, ничего выведено не будет.• Запустится корутина
launch
, scope будет сразу же отменен, однако корутинаlaunch
продолжит свою работу, через секунду будет выведено "Hello!".• Нет правильного ответа.