Введение

«Чем 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, оба наследуют контекст скоупа, из которого вызваны.

Example.kt
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] по документации.

Example.kt
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 ждёт, поток свободен — на нём спокойно работают другие корутины.

ProfileRepository.kt
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 — не создаёт корутину, а переключает контекст (обычно диспетчер) внутри текущей и ждёт завершения блока.
Example.kt
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, а непонимание модели: кандидат перечисляет факты, но не может сказать, что произойдёт в конкретном сценарии. Попробуйте прогнать себя через три вопроса:

  1. Что будет, если дочерняя корутина упадёт с исключением? — Отменит родителя (это не CancellationException, а обычное исключение), а вместе с ним — остальных детей. Это задокументированное поведение.
  2. Что будет, если отменить дочернюю корутину? — Умрёт только она; родитель продолжит работать.
  3. Сколько корутин можно запустить одновременно? — Тысячи: корутина не поток, suspend не занимает поток во время ожидания.

Если на второй вопрос вы ответили «отменится вся цепочка» — вы путаете отмену с исключением, и это типичный провал уровня middle. Разберитесь с иерархией отмены до собеседования: именно здесь интервьюер отделяет зазубренное определение от понимания.

Если чувствуете, что в корутинах плаваете на уровне «вроде пишу, но объяснить не могу» — это закрывается за пару недель плотной практики, а не за год пассивного чтения статей.

Нужна помощь в переходе на middle?

Бесплатная диагностика: разберу ваши пробелы в корутинах и составлю план роста за 30–40 минут. Без обязательств — если менторство вам не подойдёт, честно скажу.

Итоги

Базовые строители корутин — простая тема, но именно на ней выявляется глубина понимания:

  • launch — запуск задачи без результата, возвращает Job; async — запуск задачи с результатом, возвращает Deferred.
  • Deferred — это Job + результат; результат получается через suspend-функцию await().
  • withContext — переключение диспетчера внутри текущей корутины, а не запуск новой.

Главное

Формула собеседования: «launch — это Job, async — это Deferred, Deferred — это Job. await ждёт результат и не блокирует поток. async без await — потерянное исключение». Если держите эту цепочку в голове, уточняющие вопросы не собьют.

Проверьте себя: можете ли вы сейчас объяснить, чем отмена дочерней корутины отличается от падения дочерней корутины? Если сомневаетесь — напишите в Telegram, разберём вашу ситуацию и составим план, где именно закрыть пробелы перед собеседованием.