20 KiB
title, description, tags
| title | description | tags | ||
|---|---|---|---|---|
| Билдеры корутин | launch, async, runBlocking, withContext, produce и actor - что выбрать и чем они различаются |
|
Билдер - функция, которая создаёт корутину. Их немного, и выбор между ними определяется двумя вопросами: нужен ли результат и нужна ли новая корутина вообще.
| Билдер | Возвращает | Блокирует поток | Нужен результат |
|---|---|---|---|
launch |
Job |
нет | нет |
async |
Deferred<T> |
нет | да |
runBlocking |
T |
да | да |
withContext |
T |
нет | да, смена контекста |
produce |
ReceiveChannel<T> |
нет | поток значений |
actor |
SendChannel<T> |
нет | обработка сообщений |
launch: запустить и не ждать результата
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, даже если блок что-то вычислил.
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch { 42 } // значение 42 теряется
job.join()
println(job::class.simpleName != null) // true
}
async: запустить и получить результат
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 можно вызывать повторно - результат кешируется:
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():
import kotlinx.coroutines.*
fun main() = runBlocking {
val lazy = async(start = CoroutineStart.LAZY) {
println("тело стартовало")
"результат"
}
println("создан, но не запущен")
delay(20)
println(lazy.await())
// создан, но не запущен
// тело стартовало
// результат
}
Внимание: ленивый async, у которого так и не вызвали await(), никогда не выполнится и заблокирует завершение области до её отмены.
runBlocking: мост из синхронного кода
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: сменить контекст без новой корутины
import kotlinx.coroutines.*
suspend fun readFile(): String = withContext(Dispatchers.IO) {
Thread.sleep(20) // здесь блокирующий вызов допустим
"содержимое"
}
fun main() = runBlocking {
println(readFile()) // содержимое
}
withContext не создаёт новую корутину - он переключает контекст текущей и возвращает значение блока. Это главный инструмент для «main-safe» функций.
Разница с async { }.await() существенна:
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 есть другая работа.
Параллельная декомпозиция
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
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 - билдер, возвращающий канал с результатами:
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 - обратное: корутина, принимающая сообщения:
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, для разделяемого состояния - Mutex или StateFlow.
Как выбирать
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)launchexecutor.submit(task)->Future<T>async->Deferred<T>future.get()(блокирует)deferred.await()(приостанавливает)CompletableFuture.supplyAsyncasyncfuture.get()вmainrunBlockingCompletableFuture.allOfawaitAllявная передача пула в каждый вызов контекст наследуется
await()приостанавливает,get()блокирует. Это ключевая разница.Future.get()занимает поток до готовности результата, поэтому в пуле из 8 потоков 8 ожидающих задач полностью его парализуют - классический deadlock при вложенныхget().Deferred.await()освобождает поток, поэтому миллион ожидающих корутин живут в нескольких потоках.Контекст наследуется, а не передаётся. В Java пул приходится протаскивать в каждый вызов:
supplyAsync(task, executor), и забытый второй аргумент молча отправит задачу вForkJoinPool.commonPool. В 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()и так блокирует. Обратная ситуация - в KotlinrunBlockingнужен именно как явная граница между блокирующим и приостанавливаемым миром, и эта явность полезна: видно, где поток встаёт.
launchпротивsubmitв обработке ошибок. Исключение вsubmitпопадает вFutureи теряется, если не вызватьget(). Исключение вlaunchуходит родителю и обязательно будет обработано - либо родителем, либоCoroutineExceptionHandler, либо уронит приложение. Терять ошибки молча корутины не позволяют. Подробнее: Исключения в корутинахСравнить: Взаимодействие между потоками в Java
[!warning] Подводные камни
runBlockingвнутри корутины блокирует поток диспетчера:import kotlinx.coroutines.* fun main() = runBlocking { withContext(Dispatchers.Default.limitedParallelism(1)) { launch { runBlocking { delay(50) } // держит единственный поток println("первая закончила") } launch { println("вторая ждала освобождения потока") } } // первая закончила // вторая ждала освобождения потока }На ограниченном диспетчере это прямой путь к deadlock. Внутри корутины нужен
withContext, а неrunBlocking.
asyncбезawaitпроглатывает исключение до конца области: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подвешивает область навсегда: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последовательно - это не параллельно: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на каждый мелкий вызов стоит дорого: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игнорирует отмену: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-функции.
Далее: Диспетчеры и контекст