Асинхронное программирование в Kotlin: полное руководство

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

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

Начать курс

Введение в асинхронное программирование в Kotlin

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

В этом руководстве мы разберем ключевые концепции асинхронного программирования на Kotlin: от базовых корутин до продвинутых паттернов с использованием Flow и Channel. Вы научитесь создавать эффективные асинхронные приложения, которые будут работать быстро и надежно.

Основы корутин в Kotlin

Что такое корутина?

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

Запуск первой корутины

Для работы с корутинами необходимо добавить зависимость в build.gradle.kts:

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
}

Базовый пример запуска корутины:

import kotlinx.coroutines.*

fun main() = runBlocking {
    launch {
        delay(1000L)
        println("Мир!")
    }
    println("Привет,")
}

Результат выполнения:

Привет,
Мир!

Suspend-функции

Suspend-функции — это функции, которые могут приостанавливать выполнение корутины без блокировки потока. Они помечаются ключевым словом suspend:

suspend fun fetchUserData(): String {
    delay(2000L) // имитация сетевого запроса
    return "Данные пользователя"
}

fun main() = runBlocking {
    val data = fetchUserData()
    println(data)
}

Управление корутинами: Scope и Dispatchers

CoroutineScope

CoroutineScope определяет жизненный цикл корутин. Все корутины запускаются внутри определенного скоупа. Если скоуп отменяется, отменяются и все его дочерние корутины.

class MyViewModel : ViewModel() {
    private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
    
    fun loadData() {
        scope.launch {
            val data = fetchData()
            updateUI(data)
        }
    }
    
    override fun onCleared() {
        super.onCleared()
        scope.cancel()
    }
}

Dispatchers: выбор потока выполнения

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

  • Dispatchers.Main — для UI-потока (Android, JavaFX)
  • 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}")
    }
}

Продвинутые техники: async/await и отмена корутин

Параллельное выполнение с async

Билдер async позволяет запускать несколько задач параллельно и получать результаты с помощью await():

suspend fun fetchDataFromApi1(): String {
    delay(1000L)
    return "Результат API 1"
}

suspend fun fetchDataFromApi2(): String {
    delay(1500L)
    return "Результат API 2"
}

fun main() = runBlocking {
    val time = measureTimeMillis {
        val result1 = async { fetchDataFromApi1() }
        val result2 = async { fetchDataFromApi2() }
        println("${result1.await()} и ${result2.await()}")
    }
    println("Время выполнения: $time мс")
}

Обратите внимание: обе функции выполняются параллельно, поэтому общее время составит ~1.5 секунды вместо 2.5.

Отмена корутин и обработка исключений

Корутины можно отменять, и это важно для предотвращения утечек памяти. Используйте isActive для проверки состояния:

fun main() = runBlocking {
    val job = launch {
        repeat(1000) { i ->
            if (!isActive) return@launch
            println("Итерация $i")
            delay(500L)
        }
    }
    delay(3000L)
    println("Отменяем корутину")
    job.cancelAndJoin()
    println("Готово")
}

Для обработки исключений используйте try-catch внутри корутины или CoroutineExceptionHandler:

val handler = CoroutineExceptionHandler { _, exception ->
    println("Поймано исключение: $exception")
}

fun main() = runBlocking {
    val job = launch(handler) {
        throw RuntimeException("Ошибка!")
    }
    job.join()
}

Потоки данных: Flow и Channel

Flow: реактивные потоки данных

Flow — это асинхронный поток данных, который может эмитировать несколько значений последовательно. Он похож на RxJava, но полностью интегрирован с корутинами:

fun simpleFlow(): Flow<Int> = flow {
    for (i in 1..3) {
        delay(1000L)
        emit(i)
    }
}

fun main() = runBlocking {
    simpleFlow().collect { value ->
        println("Получено: $value")
    }
}

Channel: коммуникация между корутинами

Channel используется для передачи данных между корутинами. Это асинхронная очередь с поддержкой разных стратегий буферизации:

fun main() = runBlocking {
    val channel = Channel<String>()
    
    launch {
        for (msg in channel) {
            println("Получено: $msg")
        }
    }
    
    launch {
        channel.send("Первое сообщение")
        channel.send("Второе сообщение")
        channel.close()
    }
}

Блоги

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