Корутины и асинхронность

Корутины — это лёгкие потоки выполнения Kotlin, которые позволяют писать асинхронный код в линейном, читаемом стиле через suspend-функции, не блокируя главный поток.
Суть: любая сеть и работа с базой на Android выполняется асинхронно, и корутины делают это просто — код выглядит последовательным, а под капотом не блокирует интерфейс.

Главный поток Android отвечает за отрисовку интерфейса. Если выполнить в нём долгую операцию — сетевой запрос или чтение базы, — интерфейс зависнет, и система может показать диалог «приложение не отвечает». Раньше для фоновой работы городили колбэки, и код превращался в «лестницу» вложенных вызовов. Корутины решают это: вы пишете код последовательно, а тяжёлая часть выполняется в фоне.

Функция, которая может приостанавливаться, помечается словом suspend. Вызвать её можно только из другой suspend-функции или из корутины. Приостановка не блокирует поток: пока операция идёт, поток свободен для другой работы.

import kotlinx.coroutines.*

suspend fun loadUser(): String {
    delay(1000)            // имитация сетевого запроса, поток не блокируется
    return "Аня"
}

fun main() = runBlocking {
    println("Загрузка...")
    val user = loadUser()  // приостановка без блокировки
    println("Готово: $user")
}

Диспетчеры

Диспетчер определяет, на каком пуле потоков выполняется корутина. Dispatchers.Main — главный поток для UI. Dispatchers.IO — для сети и диска. Dispatchers.Default — для тяжёлых вычислений. Современные библиотеки (Retrofit, Room) уже переключаются на нужный диспетчер сами, давая main-safety из коробки.

suspend fun fetchData(): String = withContext(Dispatchers.IO) {
    // выполняется в фоновом пуле, безопасно для главного потока
    // ... сетевой запрос ...
    "данные"
}

Как работает под капотом

Корутина — не отдельный поток ОС, а конструкция языка. Компилятор превращает suspend-функцию в конечный автомат: в точке приостановки сохраняется состояние, поток освобождается, а когда результат готов, выполнение возобновляется с того же места. Поэтому тысячи корутин могут работать на нескольких потоках. Structured concurrency связывает корутины в иерархию через CoroutineScope: если родитель отменяется (например, экран закрылся), все дочерние корутины отменяются автоматически — это предотвращает утечки.

  Главный поток vs корутина

  Без корутин (блокировка):
   main: [---- сетевой запрос 1с ----] UI завис

  С корутиной (приостановка):
   main: [запуск] ... свободен для UI ... [результат]
   IO:          [---- запрос 1с ----]

Частые ошибки

Запускать тяжёлую работу на Main. Сеть и диск на главном потоке вешают интерфейс; используйте IO-диспетчер (или доверьтесь main-safety библиотек).

Запускать корутины в GlobalScope. Такие корутины не привязаны к жизненному циклу и утекают; используйте scope, связанный с ViewModel или экраном.

Глотать отмену. Перехватывая общий Exception, можно случайно поймать отмену корутины; обрабатывайте её корректно.

Best practices

  • Сеть и диск выполняйте в фоне (Dispatchers.IO) или доверяйтесь main-safety библиотек.
  • Запускайте корутины в правильном scope (viewModelScope), а не в GlobalScope.
  • Опирайтесь на structured concurrency: отмена родителя отменяет детей.
  • Не блокируйте главный поток ничем, что занимает заметное время.

Идею «приостановка вместо блокировки» удобно прочувствовать на Python через asyncio — там та же модель кооперативной многозадачности. Запустите врезку.

# Аналог приостановки без блокировки (как suspend в Kotlin)
import asyncio

async def load_user():
    await asyncio.sleep(0)   # точка приостановки, поток свободен
    return 'Аня'

async def main():
    print('Загрузка...')
    user = await load_user()
    print('Готово:', user)

asyncio.run(main())

Попробуй сам ▶ — добавьте параллельный запуск двух задач через asyncio.gather. В Kotlin это были бы две корутины через async/await внутри одного scope.

Закрепим главное

Удержите ключевое различие: приостановка — это не блокировка. Заблокированный поток стоит и ничего не делает; приостановленная корутина освобождает поток, и он занимается другой работой. Именно поэтому на горстке потоков уживаются тысячи корутин, и именно поэтому асинхронный код на Kotlin выглядит линейно, без лестницы колбэков. Эта модель — фундамент всей сетевой и дисковой работы в Android, и понимание её снимает страх перед асинхронностью.

Второй ориентир — structured concurrency как защита от утечек. Корутина всегда живёт в каком-то scope, и судьба scope определяет её судьбу. Привязав работу к viewModelScope, вы получаете автоматическую отмену при уничтожении экрана: незавершённый запрос не повиснет в воздухе и не попытается обновить уже мёртвый UI. Поэтому правило «никакого GlobalScope, всегда осмысленный scope» — это не педантизм, а прямая профилактика трудноуловимых багов.

Итог: корутины делают асинхронный код линейным и безопасным для интерфейса, а диспетчеры и structured concurrency управляют тем, где и как долго он живёт. На этой основе мы построим связку ViewModel и реактивного состояния.

Проверьте себя
1. Что делает ключевое слово suspend у функции?
AДелает функцию приватной
BПомечает функцию как способную приостанавливаться без блокировки потока; вызывается из корутины или другой suspend-функции
CЗапрещает вызывать функцию
DДелает функцию синхронной
2. Какой диспетчер выбирают для сетевых и дисковых операций?
ADispatchers.Main
BDispatchers.IO
CDispatchers.Default
DЛюбой, разницы нет