Конкурентность и многопоточность в Kotlin: от теории к практике
Современные приложения требуют быстрой и отзывчивой работы. Будь то загрузка данных из сети, обработка больших массивов или работа с пользовательским интерфейсом — без конкурентности и многопоточности не обойтись. Kotlin, будучи современным языком для JVM, Android и мультиплатформенных проектов, предлагает гибкие инструменты для решения этих задач. В этой статье мы разберем ключевые подходы: от классических потоков до легковесных корутин, и покажем, как писать эффективный код.
1. Потоки (Threads) в Kotlin: классический подход
Потоки — это базовый механизм параллельного выполнения, унаследованный от Java. В Kotlin вы можете создавать потоки так же, как в Java, но с более лаконичным синтаксисом.
Простой пример запуска потока:
fun main() {
thread {
println("Поток запущен: ${Thread.currentThread().name}")
Thread.sleep(1000)
println("Поток завершен")
}
println("Главный поток: ${Thread.currentThread().name}")
Thread.sleep(2000)
}
Однако у потоков есть недостатки: они тяжеловесны (каждый поток потребляет около 1 МБ памяти), их количество ограничено, а переключение контекста дорого. Для современных приложений с тысячами параллельных задач потоки — не лучший выбор. Именно здесь на помощь приходят корутины.
2. Корутины (Coroutines): легковесная альтернатива
Корутины — это основная фича Kotlin для конкурентности. Они работают поверх потоков, но используют меньше ресурсов. Один поток может выполнять тысячи корутин, приостанавливая и возобновляя их без блокировки.
Чтобы начать, добавьте зависимость в 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("Привет из главной корутины")
delay(2000L)
}
Здесь launch запускает новую корутину, а delay приостанавливает её без блокировки потока. runBlocking используется для блокировки главного потока до завершения всех корутин (обычно применяется только в main и тестах).
2.1. Suspend-функции
Ключевое слово suspend позволяет приостанавливать выполнение функции, не блокируя поток. Это основа асинхронного программирования в Kotlin.
suspend fun fetchUser(): String {
delay(500L) // имитация сетевого запроса
return "User123"
}
fun main() = runBlocking {
val user = fetchUser()
println("Получен пользователь: $user")
}
2.2. Async и Deferred для параллельных задач
Если нужно выполнить несколько задач параллельно и дождаться их результатов, используйте async и Deferred:
fun main() = runBlocking {
val time = measureTimeMillis {
val user = async { fetchUser() }
val profile = async { fetchProfile() }
println("Результаты: ${user.await()}, ${profile.await()}")
}
println("Время выполнения: $time мс")
}
suspend fun fetchUser(): String {
delay(500)
return "User"
}
suspend fun fetchProfile(): String {
delay(500)
return "Profile"
}
Обе задачи выполняются параллельно, что сокращает общее время.
3. Диспетчеры корутин (Dispatchers)
Диспетчеры определяют, на каких потоках будут выполняться корутины. Основные типы:
Dispatchers.Default— для CPU-интенсивных задач (по умолчанию).Dispatchers.IO— для операций ввода-вывода (сеть, файлы).Dispatchers.Main— для UI-потока (Android, JavaFX).Dispatchers.Unconfined— не привязан к конкретному потоку (используется редко).
Пример использования withContext для смены диспетчера:
suspend fun loadData(): String = withContext(Dispatchers.IO) {
println("Загрузка на потоке: ${Thread.currentThread().name}")
delay(500)
"Данные загружены"
}
fun main() = runBlocking {
val data = loadData()
println(data)
}
4. Каналы (Channels) для обмена данными
Каналы позволяют передавать данные между корутинами, работая по принципу производитель-потребитель:
fun main() = runBlocking {
val channel = Channel<Int>()
launch {
for (i in 1..5) {
channel.send(i)
delay(100)
}
channel.close()
}
launch {
for (value in channel) {
println("Получено: $value")
}
}
}
Каналы полезны для организации потоков данных, например, при обработке событий или стриминге.
5. Сравнение: потоки vs корутины
| Характеристика | Потоки | Корутины |
|---|---|---|
| Потребление памяти | ~1 МБ на поток | ~несколько КБ |
| Максимальное количество | Ограничено (обычно до нескольких тысяч) | Миллионы |
| Блокировка | Блокирует поток | Приостанавливается без блокировки |
| Управление | Ручное (join, sleep) | Структурированное (scope, structured concurrency) |
Для большинства современных задач (сетевые запросы, работа с БД, UI) корутины предпочтительнее. Потоки стоит использовать только для CPU-интенсивных операций, где важна полная изоляция.
6. Обработка ошибок и структурированная конкурентность
Kotlin