Files
obsidian/Kotlin/Корутины/Билдеры корутин.md
T

418 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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)