--- title: Билдеры корутин description: launch, async, runBlocking, withContext, produce и actor - что выбрать и чем они различаются tags: - kotlin - coroutines --- Билдер - функция, которая создаёт корутину. Их немного, и выбор между ними определяется двумя вопросами: нужен ли результат и нужна ли новая корутина вообще. | Билдер | Возвращает | Блокирует поток | Нужен результат | |---|---|---|---| | `launch` | `Job` | нет | нет | | `async` | `Deferred` | нет | да | | `runBlocking` | `T` | да | да | | `withContext` | `T` | нет | да, смена контекста | | `produce` | `ReceiveChannel` | нет | поток значений | | `actor` | `SendChannel` | нет | обработка сообщений | ## 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` - это `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, val balance: Int) suspend fun fetchUser(): String { delay(30); return "Иван" } suspend fun fetchOrders(): List { 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 = 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) : Msg } fun CoroutineScope.counter() = actor { 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() 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-функции
или в 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` | `async` -> `Deferred` | > | `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)