418 lines
20 KiB
Markdown
418 lines
20 KiB
Markdown
---
|
||
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)
|