В чём разница между launch и async в Kotlin coroutines?
launch запускает корутину без возврата результата и возвращает Job; async возвращает Deferred<T>, результат получают через await(). async используют для параллельных вычислений, launch — для фоновых задач.
launch vs async в Kotlin Coroutines
Оба строителя корутин запускают асинхронный блок кода, но принципиально отличаются тем, нужен ли вам результат вычисления.
launch — fire-and-forget
launch запускает корутину и возвращает Job — дескриптор, которым можно управлять (отменить, дождаться завершения). Возвращаемое значение блока игнорируется.
import kotlinx.coroutines.*
fun main() = runBlocking {
val job: Job = launch {
delay(1000)
println("Фоновая задача завершена")
}
println("Главный поток продолжает работу")
job.join() // ждём завершения
println("Всё готово")
}
// Вывод:
// Главный поток продолжает работу
// Фоновая задача завершена
// Всё готово
async — корутина с результатом
async возвращает Deferred — Future-подобный объект. Результат получают вызовом await(), который приостанавливает текущую корутину до готовности значения (без блокировки потока).
fun main() = runBlocking {
val deferred: Deferred<Int> = async {
delay(500)
42
}
println("Вычисляем...")
val result = deferred.await() // suspend, не блокирует поток
println("Результат: $result")
}
Параллельное выполнение с async
Главная сила async — запустить несколько независимых операций параллельно и собрать результаты:
suspend fun fetchUserData(userId: Long): UserProfile = coroutineScope {
val profile = async { userService.getProfile(userId) }
val orders = async { orderService.getOrders(userId) }
val score = async { scoringService.getScore(userId) }
// await() вызывается только здесь — все три запроса идут параллельно
UserProfile(
profile = profile.await(),
orders = orders.await(),
score = score.await()
)
}
// Суммарное время ≈ max(t_profile, t_orders, t_score), а не сумма
Обработка ошибок
Поведение при исключениях различается:
launch— исключение немедленно распространяется на родительский scope и отменяет всех siblings (если нетSupervisorJob).async— исключение сохраняется вDeferredи выбрасывается только в момент вызоваawait().
// async — ошибка «спит» до await
val deferred = async {
throw RuntimeException("Упс")
}
try {
deferred.await() // вот здесь бросает
} catch (e: RuntimeException) {
println("Поймали: ${e.message}")
}
// launch — ошибка сразу уходит в CoroutineExceptionHandler
val handler = CoroutineExceptionHandler { _, e ->
println("Handler: ${e.message}")
}
val job = launch(handler) {
throw RuntimeException("Упс")
}
Ленивый старт
Оба поддерживают CoroutineStart.LAZY — корутина не стартует до явного вызова start() или await()/join():
val deferred = async(start = CoroutineStart.LAZY) {
heavyComputation()
}
// Корутина не запущена
deferred.start() // или deferred.await() — стартует здесь
Подводные камни
- Не вызвать await() у async — если забыть
await(), исключение внутри блока будет проглочено молча и задача завершится в фоне; это классический источник "тихих" багов. - Последовательный await вместо параллельного запуска —
val a = async { f1() }.await(); val b = async { f2() }.await()выполняет задачи последовательно; нужно сначала запустить оба async, потом вызвать оба await. - GlobalScope — запуск в
GlobalScope.launchилиGlobalScope.asyncне привязан к жизненному циклу; приводит к утечкам корутин. - Отмена и await — если
Deferredотменён,await()бросаетCancellationException; его нужно обрабатывать явно или пробрасывать. - SupervisorScope и обработка ошибок — в обычном
coroutineScopeсбой одногоasyncотменяет остальных; для независимых задач нуженsupervisorScope. - Диспетчер по умолчанию —
asyncнаследует диспетчер родительского scope; если scope наDispatchers.Main, тяжёлое вычисление заблокирует UI-поток. - Structured concurrency —
asyncвнеcoroutineScope/supervisorScopeтрудно отследить; всегда используйте builders внутри scope.
Common mistakes
- Объяснять «launch и async» только как синтаксис и не описывать поведение runtime/compiler.
- Игнорировать важный риск: Нельзя использовать async без await ради параллельности: так легко потерять ошибку и нарушить жизненный цикл задачи.
- Давать пример без edge case: отмены, null, recomposition, platform boundary или ошибки.
What the interviewer is testing
- Формулирует суть темы «launch и async» своими словами и связывает ее с кодом.
- Называет механизм: Исключения в launch всплывают как unhandled у root coroutine, а исключение async материализуется в Deferred и обычно проявляется при await.
- Видит production-последствие: Нельзя использовать async без await ради параллельности: так легко потерять ошибку и нарушить жизненный цикл задачи.