Ты читал документацию. Смотрел видео. Писал viewModelScope.launch десятки раз. Можешь объяснить, что такое suspend-функция. Но на собеседовании, после третьего уточняющего вопроса про корутины, уверенность испаряется.
Проблема не в том, что ты не знаешь Coroutines. Проблема в том, что ты знаешь их на уровне API, а собеседование проверяет на уровне понимания. Разница — пропасть. И именно в неё падают 8 из 10 кандидатов с 1–2 годами опыта.
Ниже — разбор типичных вопросов, на которых сыплются. Не теория из документации, а то, что реально спрашивают и где именно ломаются ответы.
«Dispatchers.IO — для тяжёлых вычислений, Dispatchers.Default — для сети»
«Dispatchers.IO нужен для ввода-вывода: сеть, файлы, база данных. Dispatchers.Default для CPU-тяжёлых задач: парсинг JSON, сортировка.»
Это заученная фраза из первого попавшегося туториала. Она формально верна, но интервьюер сразу копнёт глубже:
- А сколько потоков в
Dispatchers.IOи вDispatchers.Default? - Что произойдёт, если я запущу 200 сетевых запросов на
Dispatchers.Default? - Можно ли переключиться внутри корутины с одного диспетчера на другой? Как?
Dispatchers.Defaultограничен количеством ядер CPU (Runtime.getRuntime().availableProcessors()). Он для задач, которые грузят процессор.Dispatchers.IOимеет пул до 64 потоков (или больше) и оптимизирован под блокирующие операции. Потоки могут переиспользоваться между IO и Default.- Сетевые запросы через OkHttp/Retrofit уже асинхронны внутри. Запускать их на
Dispatchers.IOчасто избыточно. - Переключение — через
withContext(Dispatchers.IO) { ... }. Это не создаёт новую корутину, а перевешивает текущую на другой диспетчер.
«Dispatchers — это не про тип задачи, а про характер нагрузки. IO — для операций, где поток большую часть времени ждёт. Default — для операций, где поток реально считает. Если я запускаю Retrofit-запрос, мне не нужен IO, потому что OkHttp сам управляет своими потоками. А вот если я читаю большой файл побайтово — тогда да, withContext(Dispatchers.IO).»
«structured concurrency — это когда корутины вложены друг в друга»
«Structured concurrency означает, что дочерние корутины привязаны к родителю и отменяются вместе с ним.»
Это определение из вики, а не понимание. Интервьюер спросит:
- Как распространяется отмена и исключения между родителем и детьми?
- Что будет, если дочерняя корутина выбросит исключение? Умрёт ли родитель?
- А если я не хочу, чтобы умирал?
- Корутина, запущенная в
CoroutineScope, привязана к егоJob. Отмена scope → отмена всех детей. GlobalScope.launch— нарушение structured concurrency. В Android это почти всегда баг.- Если дочерняя корутина падает с исключением, по умолчанию родитель тоже отменяется. Это fail-fast behaviour.
- Чтобы изолировать падение —
SupervisorJobилиsupervisorScope.
«Structured concurrency — это гарантия, что у каждой корутины есть родитель и она не переживёт свой scope. В Android это критично: viewModelScope привязан к ViewModel, lifecycleScope — к Lifecycle. Если ViewModel очищается, все корутины отменяются, и я не получаю утечку. Если нужно, чтобы падение одной задачи не убивало остальные, я использую SupervisorJob — например, при загрузке независимых секций экрана параллельно.»
Хочешь разобрать structured concurrency на живом примере?
На менторских сессиях мы пишем код, ломаем его и смотрим, что произойдёт. Не теорию, а реальные сценарии из прода.
Записаться на бесплатный разбор →«Утечка памяти через корутины? Ну, я же в viewModelScope запускаю»
«Если использовать viewModelScope или lifecycleScope, утечек не будет, потому что scope сам отменяется.»
- Что если внутри корутины ты держишь ссылку на Activity?
- А если используешь
GlobalScopeили создаёшь свойCoroutineScopeи забываешь отменить? - Что будет, если suspend-функция бесконечно ждёт в
delay()или в блокирующем вызове?
viewModelScopeзащищает от отмены корутины, но не от утечки объектов внутри неё. Если внутриlaunchты захватил ссылку наActivityилиContext— утечка.GlobalScope.launch— корутина живёт, пока не завершится сама. Если внутри бесконечный цикл — это утечка.- Блокирующие вызовы (
Thread.sleep, синхронный IO) на Main не отменяются черезJob.cancel().
«viewModelScope закрывает 90% случаев, но не все. Если я внутри корутины держу ссылку на View или Context, и корутина переживает их lifecycle — утечка. Поэтому я передаю applicationContext или использую WeakReference. И я помню, что cancel() корутины не прерывает блокирующий код — только suspend-точки. Если нужно прервать тяжёлую операцию, я проверяю isActive или использую withTimeout.»
«SupervisorJob — это чтобы корутины не отменялись»
«SupervisorJob нужен, чтобы при падении одной корутины не отменялись остальные.»
- В чём разница между
supervisorScopeиCoroutineScope(SupervisorJob())? - Если использую
SupervisorJob, исключения вообще куда-то деваются? - Что будет, если не поставить
CoroutineExceptionHandler?
SupervisorJobменяет направление распространения исключений: ребёнок больше не отменяет родителя и братьев. Но исключение не исчезает — оно поднимается доCoroutineExceptionHandlerили крашит приложение.supervisorScope { }— структурный аналог. Создаёт scope с SupervisorJob внутри текущей корутины.- Без
CoroutineExceptionHandlerв scope сSupervisorJobнеобработанное исключение приведёт к крашу.
«SupervisorJob изолирует детей, но не проглатывает исключения. Мне всё равно нужен CoroutineExceptionHandler, иначе приложение упадёт. Я использую supervisorScope, когда загружаю несколько независимых блоков UI: если один упадёт, остальные доедут. Логирую ошибку через handler и показываю заменитель вместо краша.»
Узнал себя в этих ошибках?
Это значит, что пробелы в Coroutines мешают тебе звучать как middle. На диагностике за 120 минут мы разберём твой уровень и составим план закрытия именно этих пробелов.
Узнать про диагностику →«withContext и async — это одно и то же, просто синтаксис другой»
«async возвращает Deferred, а withContext — результат напрямую. Но по сути оба переключают контекст.»
- Что будет, если вызвать
asyncи не вызватьawait()? withContextвнутриasync— это норма?- Если нужно запустить 10 запросов параллельно и дождаться всех?
asyncзапускает новую корутину и возвращаетDeferred. Результат не появится, пока не вызовешьawait(). Если не вызвать — корутина отработает впустую, а исключение потеряется.withContextне создаёт новую корутину. Это suspend-функция, которая переключает диспетчер внутри текущей корутины.- Для параллельных запросов — несколько
async+awaitAll().
«async — это запуск параллельной задачи. withContext — переключение внутри текущей задачи. Если нужно параллельно загрузить профиль и уведомления — делаю два async и awaitAll. Если переключиться на IO для чтения кэша — withContext. И я помню: async без await — молча проглоченное исключение.»
«Отмена корутины — это просто вызвать job.cancel()»
«Вызываю job.cancel(), и корутина останавливается.»
- Если внутри корутины цикл
for (i in 1..1_000_000)без suspend-точек? - Что такое cooperative cancellation?
cancel()vscancelAndJoin()?
- Отмена в Coroutines — кооперативная. Корутина проверяет флаг отмены только в suspend-точках:
delay(),yield(),withContext(),await(). - Если плотный CPU-цикл без suspend-вызовов,
cancel()не прервёт его. Нужно вручную проверятьisActiveилиensureActive(). job.cancelAndJoin()— ждёт, пока корутина реально завершится.withTimeoutиwithTimeoutOrNull— тоже механизм отмены черезCancellationException.
«Отмена — это не kill, а просьба. Корутина увидит её только в следующей suspend-точке. Если тяжёлый цикл — ставлю yield() или проверяю isActive на каждой итерации. Если нужно дождаться реальной остановки перед следующим шагом — cancelAndJoin. И помню, что CancellationException — это не ошибка, а нормальное завершение.»
Что объединяет все ошибки
Одна и та же причина: знание API без понимания модели.
Курсы учат синтаксису: «напиши launch», «вызови withContext», «поставь Dispatchers.IO». Но не учат думать в терминах coroutine, не учат тому, как распространяются исключения, кто принадлежит к lifecycle.
Проверяют не «знаешь ли, что такое SupervisorJob», а «понимаешь ли, зачем он и что сломается, если не поставить».
Что делать прямо сейчас
- Перестань зубрить определения. Вместо «structured concurrency — это...» объясни вслух, что произойдёт в конкретном сценарии.
- Пиши мини-примеры и ломай их. Запусти корутину, отмени её, брось исключение, посмотри, что выживет.
- Проговаривай ответы вслух. Формат «я бы сделал X, потому что Y, а если не так — Z».
- Разбирай реальные вопросы с собеседований. Не абстрактные «расскажи про корутины», а конкретные: «что вернёт этот код», «где утечка», «как это отменить».
Частые вопросы по Coroutines на собеседованиях
Чем withContext отличается от async?
async запускает новую корутину и возвращает Deferred. Результат не появится, пока не вызовешь await(). withContext не создаёт новую корутину — это suspend-функция, которая переключает диспетчер внутри текущей корутины. Для параллельных запросов — несколько async + awaitAll(). Для переключения контекста — withContext.
Что такое structured concurrency?
Гарантия, что у каждой корутины есть родитель и она не переживёт свой scope. В Android это критично: viewModelScope привязан к ViewModel, lifecycleScope — к Lifecycle. Если ViewModel очищается, все корутины отменяются. Если нужно, чтобы падение одной задачи не убивало остальные, используй SupervisorJob.
Сколько потоков в Dispatchers.IO и Dispatchers.Default?
Dispatchers.Default ограничен количеством ядер CPU. Dispatchers.IO имеет пул до 64 потоков и оптимизирован под блокирующие операции. Сетевые запросы через OkHttp/Retrofit уже асинхронны внутри — запускать их на Dispatchers.IO часто избыточно.
Что будет если не вызвать await() после async?
Корутина отработает впустую, а исключение потеряется. Это молча проглоченная ошибка. Всегда вызывай await() или используй awaitAll() для нескольких параллельных задач.
Как отменить корутину с плотным циклом без suspend-вызовов?
Отмена в Coroutines — кооперативная. Корутина проверяет флаг отмены только в suspend-точках. Если плотный CPU-цикл без suspend-вызовов, cancel() не прервёт его. Нужно вручную проверять isActive или ensureActive() на каждой итерации.
Читайте также
Другие материалы для подготовки к собеседованию