Senior
Основы Kotlin Coroutines
Базовая модель Kotlin Coroutines: приостановка, scope, dispatcher, structured concurrency, отмена и Android lifecycle.
На этой странице
Это русскоязычная подготовленная версия предоставленного конспекта. Все исходные кодовые блоки сохранены как read-only примеры. Примеры не компилировались и не запускались в ходе подготовки.
Теория
Зачем нужны Coroutines
Обычный блокирующий код:
fun loadUser(): User {
val response = networkCall()
return response.toUser()
}
Если выполнить blocking-вызов на Main Thread, UI не сможет нормально обрабатывать ввод, рисовать кадры и выполнять callbacks.
Coroutines позволяют писать последовательный по форме код:
val user = repository.loadUser()
showUser(user)
при этом loadUser() может быть suspend-функцией:
suspend fun loadUser(): User
Главный принцип:
Suspension ≠ blocking.
Официальная документация: Документация Kotlin Coroutines.
Coroutine ≠ Thread
Coroutine — вычисление, которое можно приостановить и затем продолжить.
Thread — ресурс ОС, на котором выполняются инструкции.
Thread-1:
Coroutine A ────────┐
│ suspend
Coroutine B ├───────────────>
│
Coroutine C ────────┘
Можно иметь огромное количество coroutine поверх относительно небольшого количества threads.
Но coroutine всё равно исполняется на каком-то thread.
Официальная документация: Документация Kotlin Coroutines.
Что значит suspend
suspend fun loadUser(): User {
delay(1000)
return User()
}
suspend не означает background thread.
Он означает, что функция может приостановить coroutine и позже продолжить её.
println("A")
│
▼
delay()
│
├── coroutine suspended
└── thread свободен
│
▼
coroutine resumed
│
▼
println("B")
Сравнение:
Thread.sleep:
Thread ───────── BLOCKED ─────────>
delay:
Coroutine ── suspend ───────── resume
Thread ── свободен ─────────────>
Официальная документация: Документация Kotlin Coroutines.
Как suspend работает внутри
Компилятор преобразует suspend-функцию в state machine.
suspend fun foo() {
println("A")
delay(1000)
println("B")
}
Концептуально становится чем-то вроде:
fun foo(continuation: Continuation<Unit>): Any
State machine хранит точку продолжения:
label = 0
→ println("A")
→ delay()
→ suspend
label = 1
→ println("B")
Coroutine — это не замороженный thread. Сохраняется состояние вычисления.
Официальная документация: Документация Kotlin Coroutines.
Continuation
В основе suspend лежит:
interface Continuation<in T> {
val context: CoroutineContext
fun resumeWith(result: Result<T>)
}
Continuation отвечает на вопрос:
Что делать дальше, когда suspend operation завершится?
Официальная документация: Документация Kotlin Coroutines.
launch
val job: Job = scope.launch {
repository.sync()
}
launch возвращает Job и используется, когда отдельный возвращаемый результат не нужен.
Официальная документация: Job API.
async
val deferred: Deferred<User> = scope.async {
repository.loadUser()
}
val user = deferred.await()
Ментально:
Deferred<T> ≈ Job + Result<T>
Плохая «параллельность»:
val user = async { loadUser() }.await()
val posts = async { loadPosts() }.await()
Фактически операции последовательны.
Правильно для независимых задач:
val user = async { loadUser() }
val posts = async { loadPosts() }
val result = UserPage(
user = user.await(),
posts = posts.await(),
)
Официальная документация: Job API.
runBlocking
runBlocking {
doSomething()
}
runBlocking реально блокирует текущий thread.
В Android UI-коде это почти всегда подозрительно. Полезен прежде всего как bridge между blocking и coroutine world; в тестах обычно предпочтительнее runTest.
Официальная документация: Тестирование coroutine.
CoroutineScope
Coroutine запускается внутри CoroutineScope.
CoroutineScope
│
▼
CoroutineContext
│
├── Job
├── Dispatcher
├── CoroutineName
└── ExceptionHandler
Пример:
val scope = CoroutineScope(
SupervisorJob() +
Dispatchers.IO +
CoroutineName("SyncScope")
)
Официальная документация: CoroutineScope API.
CoroutineContext
CoroutineContext — набор элементов.
Job -> SupervisorJob
Dispatcher -> Dispatchers.IO
CoroutineName -> "Loader"
Handler -> ...
Оператор + собирает единый context из элементов.
Официальная документация: Dispatchers API.
Dispatchers
Основные:
Dispatchers.Main
Dispatchers.IO
Dispatchers.Default
Dispatchers.Unconfined
Ментальная модель:
Main → UI
IO → blocking I/O
Default → CPU work
Dispatchers.IO — для blocking I/O, а не для любой операции, связанной с сетью или диском концептуально.
Dispatchers.Default — CPU-intensive работа: сортировки, тяжёлый parsing, image processing, cryptography, game-tree search.
Официальная документация: Dispatchers API.
Почему Retrofit suspend API обычно не требует Dispatchers.IO
@GET("users")
suspend fun users(): List<User>
Асинхронный HTTP-клиент не обязан блокировать Main Thread в ожидании сети.
Main
│
▼
api.users()
│
├── suspend
└── HTTP выполняется асинхронно
│
▼
response
│
▼
resume
Dispatchers.IO нужен для blocking API.
Официальная документация: Документация Kotlin Coroutines.
withContext
val result = withContext(Dispatchers.IO) {
readFile()
}
withContext меняет часть coroutine context, suspend'ит caller до завершения блока и возвращает результат.
Важно: coroutine логически остаётся той же, физический thread может поменяться.
Официальная документация: Документация Kotlin Coroutines.
Structured concurrency
coroutineScope {
launch { taskA() }
launch { taskB() }
}
Формируется дерево:
Parent Job
│
├── Child A
└── Child B
Parent знает о children и не считается завершённым, пока children не завершились.
Официальная документация: Job API.
Job hierarchy
viewModelScope Job
│
└── Parent coroutine
│
├── taskA Job
└── taskB Job
Это определяет lifecycle, cancellation и exception propagation.
GlobalScope отрывает работу от естественного lifecycle owner и поэтому почти всегда является smell.
Официальная документация: Job API.
Cancellation
Cancellation cooperative.
val job = scope.launch {
while (isActive) {
calculate()
}
}
job.cancel()
cancel() не убивает thread. Coroutine должна заметить cancellation.
Cancellation обычно проверяется в:
delay()
yield()
await()
join()
или вручную:
ensureActive()
CancellationException
При отмене coroutine используется CancellationException.
Опасно:
try {
load()
} catch (e: Exception) {
log(e)
}
Можно случайно проглотить cancellation.
Безопаснее:
try {
load()
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
handle(e)
}
runCatching и cancellation
runCatching ловит Throwable, поэтому при suspend-коде важно не превратить cancellation в обычный Result.failure.
suspend inline fun <T> runCatchingCancellable(
crossinline block: suspend () -> T
): Result<T> =
try {
Result.success(block())
} catch (e: CancellationException) {
throw e
} catch (e: Throwable) {
Result.failure(e)
}
Официальная документация: Документация Kotlin Coroutines.
coroutineScope
suspend fun loadScreen(): Screen =
coroutineScope {
val user = async { loadUser() }
val posts = async { loadPosts() }
Screen(
user.await(),
posts.await(),
)
}
Если один child падает, обычный coroutineScope отменяет siblings и пробрасывает failure caller'у.
Официальная документация: Документация Kotlin Coroutines.
supervisorScope
supervisorScope {
launch { loadAds() }
launch { loadRecommendations() }
}
Failure одного child не обязан отменять sibling.
Job vs SupervisorJob
Job
│
├── Child A ❌
└── Child B → cancelled
SupervisorJob
│
├── Child A ❌
└── Child B → continues
Supervision изолирует failure, но не «обрабатывает» exception автоматически.
Официальная документация: Job API.
Exceptions: launch vs async
launch представляет Job; необработанный failure распространяется через Job hierarchy.
async представляет Deferred<T>; await() возвращает T или выбрасывает сохранённый failure.
Но child async всё равно участвует в structured concurrency и может отменить parent ещё до await().
Официальная документация: Job API.
CoroutineExceptionHandler
val handler = CoroutineExceptionHandler { _, throwable ->
log(throwable)
}
Это не универсальный try/catch. Он предназначен прежде всего для uncaught/root coroutine exceptions.
Expected business failures обычно обрабатываются локально.
viewModelScope
class UserViewModel(
private val repository: UserRepository
) : ViewModel() {
fun load() {
viewModelScope.launch {
val user = repository.load()
}
}
}
Когда ViewModel очищается, его scope отменяется вместе с дочерними coroutine.
lifecycleScope и repeatOnLifecycle
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
flow.collect {
render(it)
}
}
}
STARTED → start collecting
STOPPED → cancel collecting
STARTED → start collecting again
Официальная документация: repeatOnLifecycle.
Coroutines ≠ thread safety
var counter = 0
repeat(1000) {
launch(Dispatchers.Default) {
counter++
}
}
counter++ не atomic.
Coroutines сами по себе не дают synchronization.
Официальная документация: Dispatchers API.
Mutex
val mutex = Mutex()
mutex.withLock {
sharedState++
}
Ожидающая lock coroutine может suspend'иться, а не блокировать thread.
Официальная документация: Документация Kotlin Coroutines.
Concurrency vs Parallelism
Concurrency — несколько задач находятся в процессе выполнения.
Parallelism — несколько задач физически исполняются одновременно на разных cores/threads.
Coroutines дают удобную concurrency model; parallelism зависит от dispatcher и hardware.
Main-safety
Хорошая suspend-функция data layer должна быть безопасна для вызова с Main:
suspend fun loadUser(): User =
withContext(ioDispatcher) {
blockingDatabase.loadUser()
}
Caller не должен знать детали blocking/non-blocking implementation.
Официальная документация: Документация Kotlin Coroutines.
Dispatcher injection
class Repository(
private val ioDispatcher: CoroutineDispatcher
) {
suspend fun load() =
withContext(ioDispatcher) {
blockingCall()
}
}
Это улучшает тестируемость.
Официальная документация: Документация Kotlin Coroutines.
Практика
Вопросы для разбора
Для каждого примера нарисуйте иерархию Job, определите CoroutineContext и Dispatcher, отметьте точки suspension/cancellation, затем проследите failure вверх и cancellation вниз. Для Flow дополнительно определите cold/hot semantics, владельца collection, поведение медленного consumer, replay и последствия lifecycle stop/start.
Проверка архитектурного решения
Спросите: кто владеет этой работой, кто может её отменить, где обрабатывается ошибка, нужна ли каждая emission или только latest, и переживает ли результат пересоздание UI. Coroutine не переживает process death; для гарантированной фоновой работы нужен durable scheduler, например WorkManager.