Урок по треку Kotlin 8 мин

Корутины и structured concurrency за 8 минут

suspend, CoroutineScope, Dispatchers, Job, launch и async, отмена и structured concurrency.

Корутины спрашивают на каждом 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

Корутины образуют дерево: корутина, запущенная внутри другой, становится её дочерней. Правила:

  1. Родитель не завершится, пока не завершатся все дети.
  2. Отмена родителя отменяет всех детей.
  3. Ошибка ребёнка (не 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?
  • Как работает отмена и почему она кооперативная?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.