Coroutines в Kotlin нужны, чтобы писать асинхронный код в более обычном последовательном стиле. В этом уроке мы не будем строить сложную архитектуру. Запустим первый пример с runBlocking, launch, async, delay, suspend и поймем, чем задержка coroutine отличается от блокировки потока
Материал рассчитан на новичка: сначала рабочий пример, потом объяснение, затем домашка. Без гонки в Flow, Dispatcher и Android lifecycle
- Что получится в конце
- Что такое suspend
- runBlocking: мост для первого примера
- launch и async
- Последовательный и параллельный сценарий
- Домашка: загрузка карточки товара
- coroutineScope вместо GlobalScope
- Обработка ошибки в async
- Домашка: последовательный и параллельный режим
- Как объяснять delay новичку
- Частые ошибки и порядок проверки
- Мини-чек перед Android-coroutines
- Почему не надо начинать с Flow
- Что может быть еще интересно по этой теме
- Что почитать дальше по Kotlin
Что получится в конце
Для локального Gradle-проекта нужна зависимость kotlinx-coroutines-core. В Kotlin Playground coroutines тоже можно пробовать, если окружение их поддерживает
Код:
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
suspend fun loadUser(): String {
delay(500)
return "Dinar"
}
suspend fun loadScore(): Int {
delay(700)
return 95
}
fun main() = runBlocking {
val userJob = async { loadUser() }
val scoreJob = async { loadScore() }
launch {
println("Loading profile")
}
val user = userJob.await()
val score = scoreJob.await()
println("$user has score $score")
}
Ожидаемый вывод:
Loading profile
Dinar has score 95
Порядок первых строк может быть чуть интереснее в более сложных примерах, но итог должен появиться после завершения обеих async-задач
Что такое suspend
suspend fun — функция, которая может приостановиться без блокировки потока:
suspend fun loadUser(): String {
delay(500)
return "Dinar"
}
delay не равен Thread.sleep. Thread.sleep блокирует поток. delay приостанавливает coroutine и дает потоку возможность выполнять другую работу
Это ключевая идея: coroutine может выглядеть как последовательный код, но под капотом умеет паузиться и продолжаться позже
runBlocking: мост для первого примера
Обычная main не является suspend-функцией. Чтобы вызвать suspend-код из простого консольного примера, используют runBlocking:
fun main() = runBlocking {
val user = loadUser()
println(user)
}
runBlocking блокирует текущий поток, пока coroutine внутри не завершится. Для учебной main это нормально. В Android UI так делать нельзя: блокировка главного потока заморозит интерфейс
Запомните границу: runBlocking хорош для примеров, тестов и bridge-кода, но не как универсальная кнопка для приложения
launch и async
launch запускает coroutine, которая не возвращает результат:
launch {
println("Loading profile")
}
async запускает coroutine, которая вернет значение через await:
val userJob = async { loadUser() }
val user = userJob.await()
Если вам нужен результат, используйте async. Если нужно выполнить действие без возвращаемого значения, часто подходит launch
Не превращайте каждый вызов в async. Если операции должны идти по порядку, обычный suspend-вызов проще
Последовательный и параллельный сценарий
Последовательный код:
val user = loadUser()
val score = loadScore()
Если loadUser ждет 500 мс, а loadScore ждет 700 мс, суммарно будет примерно 1200 мс
Параллельный вариант:
val userJob = async { loadUser() }
val scoreJob = async { loadScore() }
val user = userJob.await()
val score = scoreJob.await()
Обе операции стартуют раньше, и итоговое ожидание ближе к самой долгой операции, а не к сумме. Это полезно для независимых запросов
Если операции зависят друг от друга, параллелить нельзя. Например, если второй запрос требует id из первого, сначала получите id
Домашка: загрузка карточки товара
Сделайте две suspend-функции:
suspend fun loadProduct(): String
suspend fun loadPrice(): Int
Каждая должна делать delay, затем возвращать значение. В main запустите обе через async, дождитесь результата и напечатайте:
Product Keyboard costs 8000
Дополнительная задача: измерьте время через System.currentTimeMillis() и сравните последовательный вариант с async-вариантом
coroutineScope вместо GlobalScope
В старых примерах можно встретить GlobalScope.launch. Для новичка это плохая стартовая привычка. Такая coroutine живет отдельно от текущей структуры и ее легко потерять
Лучше мыслить через scope:
suspend fun loadProfile(): String = coroutineScope {
val user = async { loadUser() }
val score = async { loadScore() }
"${user.await()} has score ${score.await()}"
}
coroutineScope ждет дочерние coroutines и завершится только когда они закончат работу. Это и есть практический вкус structured concurrency: связанные задачи живут внутри понятной области
Обработка ошибки в async
Если внутри async произойдет ошибка, она проявится при await:
val job = async {
error("Network failed")
}
try {
job.await()
} catch (error: IllegalStateException) {
println("Failed: ${error.message}")
}
В реальном коде нужно аккуратно решать, где обрабатывать ошибку: внутри загрузки, на уровне use case или на уровне UI. Для первого урока достаточно увидеть, что async не делает ошибку невидимой
Домашка: последовательный и параллельный режим
Напишите две функции:
suspend fun sequential()
suspend fun parallel()
В каждой вызовите loadProduct и loadPrice. В первой — последовательно, во второй — через async
Измерьте время:
val start = System.currentTimeMillis()
// work
val time = System.currentTimeMillis() - start
println("Time: $time ms")
Смысл домашки — увидеть, что coroutines помогают параллелить независимое ожидание, но не делают магически быстрее код, где операции зависят друг от друга
Как объяснять delay новичку
delay в учебном примере имитирует сетевой запрос, чтение файла или ожидание ответа. Это не настоящая работа с API, но хороший безопасный стенд
Когда появится HTTP-клиент, сама идея останется: suspend-функция может ждать результат, не блокируя поток как Thread.sleep. Поэтому delay — не игрушка, а модель ожидания
Частые ошибки и порядок проверки
Unresolved reference: kotlinx Не подключена зависимость kotlinx-coroutines-core или проект не синхронизирован с Gradle
Suspend function should be called only from a coroutine Вы вызвали suspend-функцию из обычного места. Используйте runBlocking в консольном примере или вызывайте из другой suspend-функции
Используете runBlocking в Android UI Не делайте так. Для Android нужны lifecycle-aware scopes. В этом уроке runBlocking нужен только для консольного старта
Ставите async без await Если нужен результат, его нужно получить через await. Иначе вы запустили работу, но не использовали итог
Мини-чек перед Android-coroutines
Перед тем как переносить coroutines в Android, убедитесь, что вы понимаете четыре слова:
suspend— функция может приостановитьсяdelay— учебное ожидание без блокировки потокаlaunch— запустить работу без результатаasync— запустить работу с будущим результатом черезawait
Если эти четыре идеи не уложились, Android-код с lifecycleScope, ViewModel и сетевыми запросами будет казаться магией. Лучше задержаться на консольном примере и руками сравнить последовательный и параллельный запуск
Почему не надо начинать с Flow
Flow полезен для потока значений во времени, но он опирается на понимание coroutines. Если сразу открыть Flow, StateFlow и SharedFlow, легко потерять базовую модель. Сначала научитесь запускать suspend-функции и обрабатывать ошибки, потом переходите к потокам данных
Что может быть еще интересно по этой теме
Coroutines заменяют threads? Не напрямую. Coroutines — легковесная модель поверх потоков и dispatcher. Они помогают писать конкурентный код без ручного управления множеством threads
Что такое structured concurrency? Идея в том, что связанные coroutines живут в scope и завершаются вместе предсказуемо. Это защищает от потерянных фоновых задач
Когда изучать Flow? После базовых coroutines. Сначала suspend, launch, async, delay, scope. Потом потоки данных и Flow
Что дальше после coroutines? Можно открыть Android-урок, потому что coroutines часто используются для загрузки данных без блокировки UI
Что почитать дальше по Kotlin
- Коллекции Kotlin: list, map, filter и groupBy
- Kotlin Android: первый экран без перегруза
- Kotlin и Java: в чем разница для новичка
- Null safety в Kotlin: ?, !! и Elvis operator



