Kotlin guide done. (20.10.26)

This commit is contained in:
2026-10-20 12:54:25 +03:00
parent da3ad9dd1a
commit 6853ada348
81 changed files with 30112 additions and 0 deletions
@@ -0,0 +1,417 @@
---
title: Билдеры корутин
description: launch, async, runBlocking, withContext, produce и actor - что выбрать и чем они различаются
tags:
- kotlin
- coroutines
---
Билдер - функция, которая создаёт корутину. Их немного, и выбор между ними определяется двумя вопросами: нужен ли результат и нужна ли новая корутина вообще.
| Билдер | Возвращает | Блокирует поток | Нужен результат |
|---|---|---|---|
| `launch` | `Job` | нет | нет |
| `async` | `Deferred<T>` | нет | да |
| `runBlocking` | `T` | да | да |
| `withContext` | `T` | нет | да, смена контекста |
| `produce` | `ReceiveChannel<T>` | нет | поток значений |
| `actor` | `SendChannel<T>` | нет | обработка сообщений |
## launch: запустить и не ждать результата
```kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch {
delay(50)
println("работа выполнена")
}
println("launch вернулся сразу: ${job.isActive}")
job.join()
println("дождались")
// launch вернулся сразу: true
// работа выполнена
// дождались
}
```
`launch` возвращает `Job` - дескриптор для `join()`, `cancel()` и проверки состояния. Результат тела отбрасывается: тип всегда `Job`, даже если блок что-то вычислил.
```kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch { 42 } // значение 42 теряется
job.join()
println(job::class.simpleName != null) // true
}
```
## async: запустить и получить результат
```kotlin
import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis
suspend fun loadPrice(id: Int): Int { delay(100); return id * 10 }
fun main() = runBlocking {
val time = measureTimeMillis {
val a = async { loadPrice(1) }
val b = async { loadPrice(2) }
println("сумма: ${a.await() + b.await()}") // сумма: 30
}
println("параллельно: ${time < 180}") // параллельно: true
}
```
`Deferred<T>` - это `Job` с результатом: у него есть и `join()`, и `await()`. Вызов `await()` приостанавливает корутину до готовности значения.
`await` можно вызывать повторно - результат кешируется:
```kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
var calls = 0
val d = async { calls++; delay(20); "значение" }
println(d.await()) // значение
println(d.await()) // значение
println("тело выполнилось раз: $calls") // тело выполнилось раз: 1
}
```
Ленивый запуск - тело не начнётся до `await()` или `start()`:
```kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
val lazy = async(start = CoroutineStart.LAZY) {
println("тело стартовало")
"результат"
}
println("создан, но не запущен")
delay(20)
println(lazy.await())
// создан, но не запущен
// тело стартовало
// результат
}
```
Внимание: ленивый `async`, у которого так и не вызвали `await()`, никогда не выполнится и заблокирует завершение области до её отмены.
## runBlocking: мост из синхронного кода
```kotlin
import kotlinx.coroutines.*
suspend fun compute(): Int { delay(20); return 42 }
fun main() {
val result = runBlocking { compute() } // обычная функция вызывает suspend
println(result) // 42
}
```
`runBlocking` блокирует текущий поток до завершения корутины и всех её детей. Это единственный билдер, который **не** требует scope - он его создаёт.
Применимость:
| Место | Уместен ли `runBlocking` |
|---|---|
| функция `main` | да |
| тесты (если нет `runTest`) | да |
| CLI-утилита | да |
| обработчик HTTP-запроса | нет, фреймворк даёт scope |
| Android UI-поток | категорически нет |
| внутри другой корутины | нет, это блокировка потока |
## withContext: сменить контекст без новой корутины
```kotlin
import kotlinx.coroutines.*
suspend fun readFile(): String = withContext(Dispatchers.IO) {
Thread.sleep(20) // здесь блокирующий вызов допустим
"содержимое"
}
fun main() = runBlocking {
println(readFile()) // содержимое
}
```
`withContext` не создаёт новую корутину - он переключает контекст текущей и возвращает значение блока. Это главный инструмент для «main-safe» функций.
Разница с `async { }.await()` существенна:
```kotlin
import kotlinx.coroutines.*
suspend fun viaWithContext(): Int = withContext(Dispatchers.Default) { 1 }
suspend fun viaAsync(): Int = coroutineScope {
async(Dispatchers.Default) { 1 }.await() // лишняя корутина
}
fun main() = runBlocking {
println("${viaWithContext()} ${viaAsync()}") // 1 1
}
```
Результат одинаков, но `async().await()` создаёт объект `Deferred` и корутину зря. Правило: если результат нужен **сразу**, это `withContext`; `async` оправдан только когда между запуском и `await` есть другая работа.
## Параллельная декомпозиция
```kotlin
import kotlinx.coroutines.*
data class Dashboard(val user: String, val orders: List<String>, val balance: Int)
suspend fun fetchUser(): String { delay(30); return "Иван" }
suspend fun fetchOrders(): List<String> { delay(40); return listOf("заказ 1") }
suspend fun fetchBalance(): Int { delay(20); return 1000 }
suspend fun loadDashboard(): Dashboard = coroutineScope {
val user = async { fetchUser() }
val orders = async { fetchOrders() }
val balance = async { fetchBalance() }
Dashboard(user.await(), orders.await(), balance.await())
}
fun main() = runBlocking {
println(loadDashboard())
// Dashboard(user=Иван, orders=[заказ 1], balance=1000)
}
```
Это каноничный шаблон: `coroutineScope` + несколько `async` + сборка результата. Время работы - максимум из трёх, а не сумма; при ошибке любого запроса остальные отменяются.
## CoroutineStart
```kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
launch(start = CoroutineStart.DEFAULT) { println("3: обычный, после текущего кода") }
launch(start = CoroutineStart.UNDISPATCHED) { println("1: сразу, до первой приостановки") }
val lazy = launch(start = CoroutineStart.LAZY) { println("4: только после start()") }
println("2: тело main")
lazy.start()
delay(20)
// 1: сразу, до первой приостановки
// 2: тело main
// 3: обычный, после текущего кода
// 4: только после start()
}
```
| Значение | Поведение |
|---|---|
| `DEFAULT` | запланировать запуск согласно диспетчеру |
| `LAZY` | не запускать до `start()`, `join()` или `await()` |
| `ATOMIC` | запустить, игнорируя отмену до первой точки приостановки |
| `UNDISPATCHED` | выполнять в текущем потоке до первой приостановки |
`UNDISPATCHED` используют, когда важно выполнить начало тела немедленно (например, зарегистрировать подписку до возврата из функции). Остальные значения нужны редко.
## produce и actor
`produce` - билдер, возвращающий канал с результатами:
```kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
fun CoroutineScope.numbers(): ReceiveChannel<Int> = produce {
for (i in 1..3) {
delay(10)
send(i)
}
}
fun main() = runBlocking {
val channel = numbers()
for (value in channel) print("$value ")
println() // 1 2 3
}
```
`actor` - обратное: корутина, принимающая сообщения:
```kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
sealed interface Msg {
data class Add(val value: Int) : Msg
data class GetSum(val response: CompletableDeferred<Int>) : Msg
}
fun CoroutineScope.counter() = actor<Msg> {
var sum = 0
for (msg in channel) {
when (msg) {
is Msg.Add -> sum += msg.value
is Msg.GetSum -> msg.response.complete(sum)
}
}
}
fun main() = runBlocking {
val c = counter()
c.send(Msg.Add(10))
c.send(Msg.Add(5))
val response = CompletableDeferred<Int>()
c.send(Msg.GetSum(response))
println(response.await()) // 15
c.close()
}
```
Оба помечены `@ObsoleteCoroutinesApi` - их планируют заменить. Для потоков значений используйте [Flow](Flow.md), для разделяемого состояния - [Mutex или StateFlow](Разделяемое%20состояние.md).
## Как выбирать
```mermaid
graph TD
A["Нужна корутина"] --> B{"Вы в suspend-функции<br/>или в scope?"}
B -->|"нет, обычный код"| C["runBlocking"]
B -->|"да"| D{"Нужен результат?"}
D -->|"нет, fire-and-forget"| E["launch"]
D -->|"да"| F{"Нужен прямо сейчас?"}
F -->|"да, просто другой поток"| G["withContext"]
F -->|"нет, сначала другая работа"| H["async + await"]
```
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `executor.execute(task)` | `launch` |
> | `executor.submit(task)` -> `Future<T>` | `async` -> `Deferred<T>` |
> | `future.get()` (блокирует) | `deferred.await()` (приостанавливает) |
> | `CompletableFuture.supplyAsync` | `async` |
> | `future.get()` в `main` | `runBlocking` |
> | `CompletableFuture.allOf` | `awaitAll` |
> | явная передача пула в каждый вызов | контекст наследуется |
>
> **`await()` приостанавливает, `get()` блокирует.** Это ключевая разница. `Future.get()` занимает поток до готовности результата, поэтому в пуле из 8 потоков 8 ожидающих задач полностью его парализуют - классический deadlock при вложенных `get()`. `Deferred.await()` освобождает поток, поэтому миллион ожидающих корутин живут в нескольких потоках.
>
> **Контекст наследуется, а не передаётся.** В Java пул приходится протаскивать в каждый вызов: `supplyAsync(task, executor)`, и забытый второй аргумент молча отправит задачу в `ForkJoinPool.commonPool`. В Kotlin дочерняя корутина наследует контекст родителя:
> ```kotlin
> import kotlinx.coroutines.*
>
> fun main() = runBlocking {
> withContext(Dispatchers.IO) {
> launch {
> val name = Thread.currentThread().name
> val onIo = name.contains("DefaultDispatcher")
> println("ребёнок унаследовал IO: $onIo")
> }
> }
> // ребёнок унаследовал IO: true
> }
> ```
>
> **`CompletableFuture` комбинируется методами, корутины - кодом.** Java предлагает `thenCombine`, `allOf`, `anyOf`, `thenCompose` - по методу на каждый случай. В Kotlin сбор результатов пишется обычным кодом (`listOf(a.await(), b.await())`), поэтому `if`, циклы и `try/catch` работают там естественно.
>
> **Нет аналога `runBlocking` в Java, потому что он не нужен:** `future.get()` и так блокирует. Обратная ситуация - в Kotlin `runBlocking` нужен именно как явная граница между блокирующим и приостанавливаемым миром, и эта явность полезна: видно, где поток встаёт.
>
> **`launch` против `submit` в обработке ошибок.** Исключение в `submit` попадает в `Future` и теряется, если не вызвать `get()`. Исключение в `launch` уходит родителю и обязательно будет обработано - либо родителем, либо `CoroutineExceptionHandler`, либо уронит приложение. Терять ошибки молча корутины не позволяют. Подробнее: [Исключения в корутинах](Исключения%20в%20корутинах.md)
>
> Сравнить: [Взаимодействие между потоками в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md)
> [!warning] Подводные камни
> **`runBlocking` внутри корутины блокирует поток диспетчера:**
> ```kotlin
> import kotlinx.coroutines.*
>
> fun main() = runBlocking {
> withContext(Dispatchers.Default.limitedParallelism(1)) {
> launch {
> runBlocking { delay(50) } // держит единственный поток
> println("первая закончила")
> }
> launch { println("вторая ждала освобождения потока") }
> }
> // первая закончила
> // вторая ждала освобождения потока
> }
> ```
> На ограниченном диспетчере это прямой путь к deadlock. Внутри корутины нужен `withContext`, а не `runBlocking`.
>
> **`async` без `await` проглатывает исключение до конца области:**
> ```kotlin
> import kotlinx.coroutines.*
>
> fun main() = runBlocking {
> val result = runCatching {
> coroutineScope {
> async { throw IllegalStateException("ошибка в async") }
> delay(20)
> "тело завершилось"
> }
> }
> println(result.exceptionOrNull()?.message) // ошибка в async
> }
> ```
> Ошибка не исчезла - она отменила область при завершении. Но заметить её в месте возникновения невозможно, а если `async` висит в `SupervisorJob`, она потеряется совсем.
>
> **Ленивый `async` без `await` подвешивает область навсегда:**
> ```kotlin
> import kotlinx.coroutines.*
>
> fun main() = runBlocking {
> val job = launch {
> async(start = CoroutineStart.LAZY) { "никогда не выполнится" }
> println("тело launch закончилось")
> }
> val finished = withTimeoutOrNull(200) { job.join(); true } ?: false
> println("launch завершился: $finished")
> job.cancel()
> // тело launch закончилось
> // launch завершился: false
> }
> ```
> Ленивый `Deferred` остаётся незавершённым ребёнком, а родитель обязан дождаться всех детей - поэтому `launch` не завершится никогда, даже после выхода из тела. Без `job.cancel()` программа повиснет. Это тихая утечка: ни ошибки, ни предупреждения.
>
> **`async` последовательно - это не параллельно:**
> ```kotlin
> import kotlinx.coroutines.*
> import kotlin.system.measureTimeMillis
>
> suspend fun slow(): Int { delay(100); return 1 }
>
> fun main() = runBlocking {
> val wrong = measureTimeMillis {
> val a = async { slow() }.await() // await сразу — ждём здесь же
> val b = async { slow() }.await()
> println(a + b) // 2
> }
> println("последовательно: ${wrong >= 200}") // последовательно: true
> }
> ```
> `await()` сразу после `async` убивает параллельность. Запускайте все `async`, потом ждите.
>
> **`launch` теряет результат молча.** Тип `Job` не содержит значения, поэтому `launch { computeSomething() }` компилируется и ничего не делает с результатом. Компилятор не предупредит.
>
> **`withContext` на каждый мелкий вызов стоит дорого:**
> ```kotlin
> import kotlinx.coroutines.*
>
> suspend fun badLoop(): Int = withContext(Dispatchers.Default) {
> var sum = 0
> repeat(1000) { sum += it } // одно переключение — правильно
> sum
> }
>
> fun main() = runBlocking {
> println(badLoop()) // 499500
> }
> ```
> Переключение контекста - это передача задачи в другой пул с синхронизацией. Выносите `withContext` наружу цикла, а не внутрь.
>
> **`runBlocking` в Android UI-потоке вызывает ANR.** Это самая частая серьёзная ошибка: UI замирает до конца корутины. В Android используйте `lifecycleScope`/`viewModelScope`.
>
> **`produce` и `actor` устарели.** Они помечены `@ObsoleteCoroutinesApi`, их API может измениться. Для новых проектов - `Flow` и `Channel` напрямую, либо `Mutex` для состояния.
>
> **`CoroutineStart.ATOMIC` игнорирует отмену:**
> ```kotlin
> import kotlinx.coroutines.*
>
> fun main() = runBlocking {
> @OptIn(ExperimentalCoroutinesApi::class)
> val job = launch(start = CoroutineStart.ATOMIC) {
> println("тело всё равно начнётся")
> delay(100)
> println("а это уже нет")
> }
> job.cancel()
> job.join()
> // тело всё равно начнётся
> }
> ```
> Начало тела выполнится даже после `cancel()`. Это нужно для гарантированного освобождения ресурсов, но легко превращается в сюрприз.
>
> **Возврат `Deferred` из публичной функции ломает структурированность** - вызывающий может не вызвать `await()`, и ошибка потеряется. Возвращайте значение из `suspend`-функции.
Далее: [Диспетчеры и контекст](Диспетчеры%20и%20контекст.md)