Корутины спрашивают на каждом Kotlin- и Android-собесе, от junior до senior. Нужно уметь объяснить, что такое suspend, чем launch отличается от async, что делают диспетчеры и почему structured concurrency спасает от утечек.
Что такое корутина
Корутина это вычисление, которое можно приостановить и потом продолжить, не блокируя поток. Пока корутина ждёт сеть, поток свободен и выполняет другие корутины. Тысячи корутин спокойно живут на нескольких потоках.
suspend fun loadUser(id: Int): User {
val dto = api.getUser(id) // точка приостановки
return dto.toDomain()
}
suspend значит «функция может приостановиться». Вызвать её можно только из корутины или другой suspend-функции. Под капотом компилятор превращает её в машину состояний и добавляет скрытый параметр Continuation: продолжение, которое вызовут, когда результат будет готов (CPS-преобразование). Слово suspend само по себе не переносит код в другой поток.
Билдеры: launch, async, runBlocking
scope.launch { // Job, результат не нужен
repository.sync()
}
val deferred = scope.async { api.getProfile() } // Deferred<Profile>
val profile = deferred.await()
coroutineScope { // параллельно и дождаться обоих
val a = async { api.a() }
val b = async { api.b() }
a.await() + b.await()
}
launchзапускает корутину и возвращаетJob. Исключение в ней сразу уходит родителю.asyncвозвращаетDeferred<T>, результат забираютawait().runBlockingблокирует текущий поток до завершения. Только дляmain, тестов и мостов с блокирующим кодом, никогда в Android на главном потоке.
CoroutineScope и CoroutineContext
Каждая корутина запускается в CoroutineScope, а у scope есть CoroutineContext: набор элементов Job, CoroutineDispatcher, CoroutineName, CoroutineExceptionHandler. Контексты складываются через +.
В Android готовые scope: viewModelScope (отменяется в onCleared), lifecycleScope (отменяется при уничтожении владельца). Свой scope: CoroutineScope(SupervisorJob() + Dispatchers.Main), и его обязательно отменять.
GlobalScope не привязан ни к чему: корутина в нём живёт до конца процесса, её никто не отменит. Почти всегда это ошибка.
Диспетчеры
Dispatchers.Main: главный поток (UI).Main.immediateне делает лишний диспатч, если уже на главном потоке.Dispatchers.IO: блокирующий ввод-вывод (файлы, JDBC, старые SDK). Пул до 64 потоков (или числа ядер, если их больше).Dispatchers.Default: CPU-работа (парсинг, сортировка). Потоков столько, сколько ядер.Dispatchers.Unconfined: стартует в текущем потоке, после приостановки продолжает там, где возобновили. Для особых случаев.
withContext(Dispatchers.IO) { } переключает диспетчер на время блока и возвращает результат. Правило: suspend-функция сама отвечает за свой поток и безопасна для вызова с главного потока (main-safe).
IO и Default делят общий пул потоков, поэтому переключение между ними часто обходится без смены потока. limitedParallelism(n) создаёт представление диспетчера с ограничением параллельности.
Job и structured concurrency
Корутины образуют дерево: корутина, запущенная внутри другой, становится её дочерней. Правила:
- Родитель не завершится, пока не завершатся все дети.
- Отмена родителя отменяет всех детей.
- Ошибка ребёнка (не
CancellationException) отменяет родителя и через него всех остальных детей.
Это и есть structured concurrency: корутины не теряются, их время жизни ограничено scope. Закрыли экран, viewModelScope отменён, все запросы этого экрана отменены.
Состояния Job: New, Active, Completing, Completed, Cancelling, Cancelled. job.join() ждёт завершения, job.cancel() отменяет.
Отмена кооперативная
cancel() не останавливает поток. Он помечает корутину отменённой, а реально она прервётся в ближайшей точке приостановки (delay, withContext, yield, await), где бросится CancellationException.
while (isActive) { heavyStep() } // или ensureActive() в цикле
Цикл вычислений без точек приостановки отмену не заметит. После отмены нельзя вызывать suspend-функции: для финальной очистки нужен withContext(NonCancellable) { }. И никогда не глотайте CancellationException в catch (e: Exception): корутина продолжит работу, хотя её отменили.
coroutineScope и supervisorScope
coroutineScope { } создаёт вложенный scope, ждёт всех детей и пробрасывает ошибку: упал один, отменяются все. supervisorScope { } то же, но ошибка одного ребёнка не отменяет остальных. Подробно об ошибках в модуле «Исключения в корутинах».
Частые ошибки
GlobalScope.launchв Android-коде.runBlockingна главном потоке.- Блокирующий вызов (
Thread.sleep, JDBC) вDispatchers.DefaultилиMain. asyncбезawait: исключение не потеряется, но придёт неожиданно.catch (e: Exception), который проглатываетCancellationException.- Свой
CoroutineScopeбез отмены.
Что спросят на собесе
- Что такое корутина и чем она отличается от потока?
- Что делает компилятор с
suspend-функцией? - Разница
launchиasync,coroutineScopeиsupervisorScope. - Какие есть диспетчеры и чем
IOотличается отDefault? - Что такое structured concurrency?
- Как работает отмена и почему она кооперативная?