Coroutines в Kotlin: первый async-пример

Coroutines в Kotlin нужны, чтобы писать асинхронный код в более обычном последовательном стиле. В этом уроке мы не будем строить сложную архитектуру. Запустим первый пример с runBlocking, launch, async, delay, suspend и поймем, чем задержка coroutine отличается от блокировки потока

Материал рассчитан на новичка: сначала рабочий пример, потом объяснение, затем домашка. Без гонки в Flow, Dispatcher и Android lifecycle

Что получится в конце

Для локального 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

Оцените статью
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x