Coroutines для Kotlin: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Что такое Coroutines и зачем они нужны?

Coroutines (сопрограммы) — это мощный инструмент для асинхронного программирования в Kotlin, который позволяет писать неблокирующий код в императивном стиле. В отличие от традиционных потоков (threads), корутины являются легковесными: вы можете запускать тысячи корутин на одном потоке без значительного потребления памяти.

Основная задача Coroutines — упростить работу с асинхронными операциями: сетевыми запросами, чтением из базы данных, файловыми операциями и другими задачами, которые требуют ожидания. Вместо того чтобы использовать сложные callback-и или реактивные стримы, вы пишете последовательный код, который приостанавливается во время ожидания и возобновляется после завершения операции.

Библиотека Coroutines является частью экосистемы Kotlin и официально поддерживается JetBrains. Она широко используется в Android-разработке, серверных приложениях (Ktor, Spring WebFlux) и даже в desktop-приложениях.

Установка Coroutines

Поскольку Coroutines — это не отдельная библиотека для установки через pip или npm, а часть экосистемы Kotlin, её подключение зависит от вашего менеджера сборки.

Gradle (Kotlin DSL)

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
    // Для Android или использования Dispatchers.Main
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0")
}

Gradle (Groovy DSL)

dependencies {
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0'
}

Maven

<dependency>
    <groupId>org.jetbrains.kotlinx</groupId>
    <artifactId>kotlinx-coroutines-core</artifactId>
    <version>1.9.0</version>
</dependency>

Быстрый старт: первый пример

Давайте напишем простую программу, которая запускает корутину, делает паузу и выводит сообщение:

import kotlinx.coroutines.*

fun main() = runBlocking {
    println("Начало программы: ${Thread.currentThread().name}")
    
    launch {
        delay(1000L) // Приостанавливаем корутину на 1 секунду (не блокируя поток)
        println("Мир! (из корутины: ${Thread.currentThread().name})")
    }
    
    println("Привет, (из main: ${Thread.currentThread().name})")
    delay(500L) // Ждём, чтобы корутина успела выполниться
    println("Конец программы")
}

Вывод:

Начало программы: main @coroutine#1
Привет, (из main: @coroutine#1)
Мир! (из корутины: @coroutine#2)
Конец программы

Обратите внимание: delay() не блокирует поток, а приостанавливает только текущую корутину. Поток main остаётся свободным для других задач.

Основные методы и функции Coroutines

1. runBlocking

Что делает: Запускает новую корутину и блокирует текущий поток до её завершения. Используется как мост между обычным синхронным кодом и миром корутин (обычно в main-функции или тестах).

Параметры:

  • context: CoroutineContext — контекст корутины (по умолчанию EmptyCoroutineContext)
  • block: suspend CoroutineScope.() -> T — suspend-блок, который будет выполнен

Пример:

fun main() {
    val result = runBlocking {
        delay(1000L)
        "Готово!"
    }
    println(result) // Выведет: Готово!
}

2. launch

Что делает: Запускает новую корутину, которая не возвращает результат (аналог fire-and-forget). Возвращает объект Job, который можно использовать для отмены или ожидания.

Параметры:

  • context: CoroutineContext — контекст (например, Dispatchers.IO)
  • start: CoroutineStart — опция старта (DEFAULT, LAZY, ATOMIC, UNDISPATCHED)
  • block: suspend CoroutineScope.() -> Unit — выполняемый блок

Пример:

fun main() = runBlocking {
    val job = launch {
        delay(1000L)
        println("Задача выполнена")
    }
    println("Ожидаем...")
    job.join() // Ждём завершения корутины
    println("Готово")
}

3. async / await

Что делает: Запускает корутину, которая возвращает результат через объект Deferred. Метод await() приостанавливает текущую корутину до получения результата. Идеально для параллельного выполнения независимых задач.

Параметры: Аналогично launch, но block возвращает T.

Пример:

fun main() = runBlocking {
    val deferred1 = async {
        delay(1000L)
        10
    }
    val deferred2 = async {
        delay(500L)
        20
    }
    val sum = deferred1.await() + deferred2.await()
    println("Сумма: $sum") // Выведет: Сумма: 30 (через ~1 секунду)
}

4. withContext

Что делает: Позволяет переключить контекст выполнения корутины (например, с Main на IO) и вернуть результат. Не создаёт новую корутину, а временно меняет контекст текущей.

Параметры:

  • context: CoroutineContext — новый контекст
  • block: suspend CoroutineScope.() -> T — блок для выполнения в новом контексте

Пример:

fun main() = runBlocking {
    val result = withContext(Dispatchers.IO) {
        // Тяжёлая операция в фоновом потоке
        Thread.sleep(1000L)
        "Данные из БД"
    }
    println(result) // Выведет: Данные из БД
}

5. delay

Что делает: Приостанавливает текущую корутину на указанное время (в миллисекундах) без блокировки потока. Это не Thread.sleep() — поток остаётся свободным для других корутин.

Параметры:

  • timeMillis: Long — время задержки в миллисекундах

Пример:

fun main() = runBlocking {
    println("Старт")
    delay(2000L)
    println("Прошло 2 секунды")
}

6. Job.cancel() и Job.join()

Что делает: cancel() отменяет выполнение корутины. join() ожидает её завершения. Комбинация cancelAndJoin() делает оба действия сразу.

Параметры: cancel() может принимать CancellationException с сообщением.

Пример:

fun main() = runBlocking {
    val job = launch {
        repeat(1000) { i ->
            println("Работаю: $i")
            delay(500L)
        }
    }
    delay(1300L)
    println("Отменяем!")
    job.cancelAndJoin()
    println("Отменено")
}

7. CoroutineScope

Что делает: Определяет область видимости для корутин. Все корутины, запущенные внутри scope, автоматически отменяются при его завершении. Используется для структурированной конкурентности.

Параметры: Принимает CoroutineContext.

Пример:

class MyActivity {
    private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
    
    fun loadData() {
        scope.launch {
            val data = withContext(Dispatchers.IO) { fetchData() }
            showData(data)
        }
    }
    
    fun onDestroy() {
        scope.cancel() // Отменяем все корутины при уничтожении Activity
    }
}

8. Dispatchers

Что делает: Определяет, на каком пуле потоков будет выполняться корутина. Основные диспетчеры:

  • Dispatchers.Main — главный поток (UI в Android)
  • Dispatchers.IO — для операций ввода-вывода (сеть, БД)
  • Dispatchers.Default — для CPU-интенсивных задач
  • Dispatchers.Unconfined — не привязан к конкретному потоку

Пример:

fun main() = runBlocking {
    launch(Dispatchers.IO) {
        println("Работаю в IO: ${Thread.currentThread().name}")
    }
    launch(Dispatchers.Default) {
        println("Работаю в Default: ${Thread.currentThread().name}")
    }
}

9. supervisorScope

Что делает: Создаёт дочернюю область, где отмена одной корутины не влияет на другие. В отличие от coroutineScope, где исключение в одной дочерней корутине отменяет все остальные.

Параметры: Принимает suspend-блок.

Пример:

fun main() = runBlocking {
    supervisorScope {
        launch {
            delay(100L)
            throw RuntimeException("Ошибка в корутине 1")
        }
        launch {
            delay(200L)
            println("Корутина 2 завершена успешно")
        }
    }
    // Выведет: Корутина 2 завершена успешно (несмотря на ошибку в первой)
}

10. yield

Что делает: Приостанавливает текущую корутину и даёт возможность другим корутинам выполниться. Полезно для кооперативной многозадачности.

Параметры: Нет.

Пример:

fun main() = runBlocking {
    launch {
        repeat(3) {
            println("Корутина A: $it")
            yield() // Даём шанс другим корутинам
        }
    }
    launch {
        repeat(3) {
            println("Корутина B: $it")
            yield()
        }
    }
}

Реальный практический пример: загрузка данных из сети

Представим, что мы пишем приложение для Android, которое загружает список пользователей из API и отображает его:

import kotlinx.coroutines.*
import java.net.URL

// Имитация сетевого запроса
suspend fun fetchUsers(): List<String> {
    return withContext(Dispatchers.IO) {
        // Реальный запрос к API
        val response = URL("https://jsonplaceholder.typicode.com/users").readText()
        // Парсинг (упрощённо)
        response.split("\"name\":\"")
            .drop(1)
            .map { it.substringBefore("\"") }
    }
}

// Имитация сохранения в БД
suspend fun saveToDatabase(users: List<String>) {
    withContext(Dispatchers.IO) {
        delay(500L) // Имитация записи
        println("Сохранено ${users.size} пользователей в БД")
    }
}

fun main() = runBlocking {
    println("Начинаем загрузку...")
    
    // Загружаем данные параллельно
    val usersDeferred = async { fetchUsers() }
    val cachedDeferred = async { 
        delay(300L)
        listOf("Кэшированный пользователь")
    }
    
    val users = usersDeferred.await()
    val cached = cachedDeferred.await()
    
    println("Загружено ${users.size} пользователей из сети")
    println("Загружено ${cached.size} из кэша")
    
    // Сохраняем в фоне
    launch(Dispatchers.IO) {
        saveToDatabase(users + cached)
    }
    
    println                    

Рекомендаций