Введение
«Чем launch отличается от async?» — с этого вопроса начинается блок про корутины почти на каждом собеседовании. Ответ «async возвращает результат, а launch — нет» формально верен, но интервьюер от него отталкивается и копает глубже: в Job, Deferred, await и отмену.
Coroutines — это suspendable-вычисления: код, который может приостановиться, не блокируя поток. А launch, async, runBlocking — строители (coroutine builders), которые запускают такие вычисления внутри CoroutineScope по официальной документации. Каждый строитель решает две задачи: как корутина стартует и как вы получаете её результат.
Разница между строителями и есть любимый вопрос собеседований: кто-то путает async и withContext, кто-то не знает, что Deferred — это Job, кто-то уверен, что async без await просто «потеряет» результат. Разберём по порядку, что стоит за каждым понятием.
launch и async: два главных строителя
launch запускает корутину «в сторону»: вам не нужен её результат, вам нужно, чтобы работа шла параллельно с остальным кодом. async запускает корутину ради результата, который получите позже через await(). Оба — extension-функции на CoroutineScope, оба наследуют контекст скоупа, из которого вызваны.
import kotlinx.coroutines.*
fun main() = runBlocking {
val job: Job = launch {
// задача без результата: логирование, аналитика, предзагрузка
delay(500)
println("background work finished")
}
val deferred: Deferred<Int> = async {
// задача с результатом
delay(500)
42
}
job.join() // ждём завершения launch
println(deferred.await()) // ждём результат async
}Ключевое отличие по документации: launch возвращает Job и не несёт результата, async возвращает Deferred — лёгкий non-blocking «future», обещание результата. И важная деталь, которую кандидаты часто пропускают: Deferred — это подтип Job, поэтому его можно отменять и ждать точно так же.
Ещё нюанс: корутины дёшевы. Запуск 50 000 корутин с delay — нормальная нагрузка, примеры такого есть прямо в документации. Это не потоки: suspend не занимает поток, пока ждёт.
Что запомнить
launch — «запусти и забудь», async — «запусти и потом спроси результат». Оба создают дочернюю корутину в скоупе, оба можно отменить. Разница только в способе получения результата.
Job: дескриптор корутины
Job — это дескриптор корутины: её идентификатор, состояние и механизм управления. Через него можно отменить корутину, дождаться её завершения, проверить, жива ли она. Job — часть CoroutineContext, и достать его из любой корутины можно через coroutineContext[Job] по документации.
val job = scope.launch {
// какая-то работа
}
job.isActive // true, пока корутина выполняется
job.cancel() // запросить отмену
job.join() // дождаться фактического завершения
job.cancelAndJoin() // отмена + ожидание одной операциейЖизненный цикл Job — это конечный автомат: New → Active → Completing → Completed и ветка отмены Cancelling → Cancelled. Звучит скучно, но на собеседовании это часто всплывает в вопросе «что произойдёт с дочерними корутинами, если отменить родительскую?».
Тут работает структурная конкурентность: Job-родитель всегда ждёт своих детей, а отмена родителя рекурсивно отменяет всех детей по документации. Обратное не всегда правда: job.cancel() на дочерней корутине не отменяет родителя — только сам Job подтверждено в документации по исключениям.
Ещё одна частая ошибка на собеседовании: «чтобы отменить корутину, нужно вызвать cancel() — и всё». Нет: отмена в корутинах кооперативная. Корутина увидит её только в suspend-точке. Если внутри плотный цикл без suspend-вызовов, cancel() не сработает, пока цикл не закончится — нужно самому проверять isActive или ensureActive().
Deferred и await
Deferred — это Job плюс результат. await() — suspend-функция, которая приостанавливает текущую корутину до момента, когда результат готов, и возвращает его. Пока await ждёт, поток свободен — на нём спокойно работают другие корутины.
suspend fun loadProfile(): Profile = coroutineScope {
val user = async { api.getUser() }
val posts = async { api.getPosts() }
Profile(user.await(), posts.await())
}Этот паттерн — параллельная загрузка двух независимых данных — прямо из официального гайда по композиции suspend-функций. Обратите внимание на coroutineScope { }: если одна из задач упадёт, scope отменит вторую — то есть исключение не оставит висеть осиротевшую корутину.
Нюансы await, которые любят проверять:
asyncбезawait— антипаттерн. Исключение, выброшенное внутри такой корутины, «прячется» вDeferredи всплывёт только приawait(). Еслиawaitникто не вызвал — ошибка молча проглочена по документации.launchиasyncпо-разному работают с исключениями.launchпередаёт необработанное исключение вCoroutineExceptionHandler(или крашит приложение), аasyncвсегда «заворачивает» его вDeferredи отдаёт на откуп тому, кто вызоветawait.- Ленивый старт.
async(start = CoroutineStart.LAZY)не запускает корутину до первогоawait()или явногоstart()— это способ отложить дорогую операцию до момента, когда результат реально понадобится по документации.
async или withContext: где что
Самый частый провал на собеседовании — слияние async и withContext. Формула простая:
async— запускает новую корутину, создаёт параллельную работу;withContext— не создаёт корутину, а переключает контекст (обычно диспетчер) внутри текущей и ждёт завершения блока.
suspend fun readConfig(): String = withContext(Dispatchers.IO) {
// блок выполняется на IO, текущая корутина приостановлена
File("config.json").readText()
}withContext приостанавливает текущую корутину, переключает её на другой диспетчер и возвращает обратно — это документированное поведение. Для смены диспетчера внутри корутины он и нужен. А async нужен, когда работа должна идти параллельно другим задачам, и результат понадобится позже.
Как отвечать на вопрос «а можно ли запустить 10 запросов параллельно»: да, 10 × async + awaitAll(). Но если запросы уже внутри Retrofit/OkHttp — они сами по себе асинхронные, и городить вокруг них корутины только ради «параллельности» не нужно.
Короткая проверка на собеседовании: «withContext внутри async — это нормально?» Нормально, это переключение диспетчера для части работы. «async внутри withContext?» Тоже допустимо, но спросите себя, зачем — возможно, параллельность здесь не нужна.
- launch → Job, задача без результата; async → Deferred, задача с результатом.
- Deferred — это Job: можно отменять, ждать, проверять состояние.
- await() приостанавливает текущую корутину, поток не блокируется.
- async без await — потерянное исключение; для launch исключение уходит в handler.
- withContext не создаёт корутину, а переключает диспетчер.
- Отмена кооперативна: плотный цикл без suspend-точек cancel() не прервёт.
Как отвечать на собеседовании
На собеседованиях я чаще вижу не незнание API, а непонимание модели: кандидат перечисляет факты, но не может сказать, что произойдёт в конкретном сценарии. Попробуйте прогнать себя через три вопроса:
- Что будет, если дочерняя корутина упадёт с исключением? — Отменит родителя (это не
CancellationException, а обычное исключение), а вместе с ним — остальных детей. Это задокументированное поведение. - Что будет, если отменить дочернюю корутину? — Умрёт только она; родитель продолжит работать.
- Сколько корутин можно запустить одновременно? — Тысячи: корутина не поток, suspend не занимает поток во время ожидания.
Если на второй вопрос вы ответили «отменится вся цепочка» — вы путаете отмену с исключением, и это типичный провал уровня middle. Разберитесь с иерархией отмены до собеседования: именно здесь интервьюер отделяет зазубренное определение от понимания.
Если чувствуете, что в корутинах плаваете на уровне «вроде пишу, но объяснить не могу» — это закрывается за пару недель плотной практики, а не за год пассивного чтения статей.
Нужна помощь в переходе на middle?
Бесплатная диагностика: разберу ваши пробелы в корутинах и составлю план роста за 30–40 минут. Без обязательств — если менторство вам не подойдёт, честно скажу.
Итоги
Базовые строители корутин — простая тема, но именно на ней выявляется глубина понимания:
launch— запуск задачи без результата, возвращаетJob;async— запуск задачи с результатом, возвращаетDeferred.Deferred— этоJob+ результат; результат получается через suspend-функциюawait().withContext— переключение диспетчера внутри текущей корутины, а не запуск новой.
Главное
Формула собеседования: «launch — это Job, async — это Deferred, Deferred — это Job. await ждёт результат и не блокирует поток. async без await — потерянное исключение». Если держите эту цепочку в голове, уточняющие вопросы не собьют.
Проверьте себя: можете ли вы сейчас объяснить, чем отмена дочерней корутины отличается от падения дочерней корутины? Если сомневаетесь — напишите в Telegram, разберём вашу ситуацию и составим план, где именно закрыть пробелы перед собеседованием.