Асинхронное программирование в Kotlin: полное руководство для начинающих
Современные приложения всё чаще требуют выполнения нескольких задач одновременно: загрузка данных из сети, работа с базой данных, обработка пользовательского ввода. В языке Kotlin для этого используется асинхронное программирование, основанное на корутинах (coroutines). Это мощный и элегантный инструмент, который позволяет писать неблокирующий код без сложных коллбэков.
В этой статье мы разберём основы асинхронного программирования в Kotlin: что такое корутины, как использовать launch, async и await, а также познакомимся с диспетчерами и обработкой ошибок. Материал рассчитан на начинающих разработчиков, уже знакомых с основами синтаксиса Kotlin.
Что такое корутины и зачем они нужны?
Корутина — это легковесный поток выполнения, который может быть приостановлен и возобновлён без блокировки основного потока. В отличие от традиционных потоков (threads), корутины не привязаны к системным ресурсам и могут выполняться в одном потоке, переключаясь между собой.
Основные преимущества корутин:
- Лёгкость — можно запустить тысячи корутин без значительного потребления памяти.
- Приостановка (suspend) — функция может "заморозить" своё выполнение, не блокируя поток.
- Структурированная конкурентность — корутины автоматически управляют своим жизненным циклом.
- Простота — код выглядит как синхронный, без вложенных коллбэков.
Для работы с корутинами необходимо добавить зависимость в build.gradle.kts:
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
}
Основные строительные блоки: launch, async и suspend
Функция suspend
Любая функция, которая может приостанавливать выполнение корутины, помечается ключевым словом suspend. Такие функции можно вызывать только внутри корутины или другой suspend-функции.
suspend fun fetchUserData(): String {
delay(1000) // имитация задержки
return "User data loaded"
}
Функция delay() — это аналог Thread.sleep(), но без блокировки потока.
launch — запуск корутины без результата
Билдер launch запускает новую корутину, которая не возвращает результат (возвращает Job). Используется для задач типа "запустил и забыл".
import kotlinx.coroutines.*
fun main() = runBlocking {
launch {
println("Корутина 1 запущена")
delay(1000)
println("Корутина 1 завершена")
}
println("Главный поток продолжает работу")
}
Вывод:
Главный поток продолжает работу
Корутина 1 запущена
Корутина 1 завершена
Обратите внимание: runBlocking используется только в учебных примерах или в точках входа (например, в main()). В реальных приложениях (Android, сервер) его применять не нужно.
async и await — получение результата
Если корутина должна вернуть результат, используется билдер async. Он возвращает объект Deferred<T>, из которого можно получить значение с помощью await().
fun main() = runBlocking {
val deferredResult: Deferred<String> = async {
delay(1500)
"Результат вычислений"
}
println("Ожидаем результат...")
val result = deferredResult.await()
println("Получен результат: $result")
}
Вывод:
Ожидаем результат...
Получен результат: Результат вычислений
С помощью async можно запускать несколько параллельных задач:
fun main() = runBlocking {
val task1 = async { loadDataFromNetwork() }
val task2 = async { loadDataFromDatabase() }
val result1 = task1.await()
val result2 = task2.await()
println("Оба результата получены: $result1, $result2")
}
suspend fun loadDataFromNetwork(): String {
delay(2000)
return "Network data"
}
suspend fun loadDataFromDatabase(): String {
delay(1500)
return "Database data"
}
Общее время выполнения составит ~2 секунды (максимальная задержка), а не 3.5 секунды, если бы задачи выполнялись последовательно.
Диспетчеры (Dispatchers) и контекст корутины
Диспетчер определяет, в каком потоке или пуле потоков будет выполняться корутина. Основные диспетчеры:
Dispatchers.Default— для CPU-интенсивных задач (по умолчанию используется, если не указан другой).Dispatchers.IO— для операций ввода-вывода (работа с файлами, сетью, БД).Dispatchers.Main— для UI-потока (доступен в Android или JavaFX).Dispatchers.Unconfined— не привязан к конкретному потоку (используется редко).
Пример использования диспетчера:
fun main() = runBlocking {
launch(Dispatchers.IO) {
println("Выполняется в IO-потоке: ${Thread.currentThread().name}")
// работа с сетью или файлами
}
launch(Dispatchers.Default) {
println("Выполняется в Default-потоке: ${Thread.currentThread().name}")
// сложные вычисления
}
}
Также можно создавать собственные диспетчеры с помощью newSingleThreadContext() или newFixedThreadPoolContext().
Обработка ошибок и отмена корутин
Обработка исключений
Исключения внутри корутины можно обрабатывать с помощью try-catch или через CoroutineExceptionHandler.
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
println("Ошибка: ${exception.message}")
}
val job = launch(