Конкурентность и многопоточность в Kotlin: от А до Я
Современные приложения всё чаще требуют выполнения нескольких задач одновременно. В Kotlin, как и в других языках JVM, многопоточность традиционно реализуется через класс Thread. Однако язык предлагает гораздо более мощный и элегантный инструмент — корутины (coroutines). В этой статье мы разберём все аспекты конкурентности и многопоточности в Kotlin: от базовых понятий до продвинутых техник.
1. Основы многопоточности: Thread и Runnable
Начнём с классического подхода. В Kotlin, как и в Java, можно создать поток напрямую:
fun main() {
val thread = Thread {
for (i in 1..5) {
println("Поток: $i")
Thread.sleep(500)
}
}
thread.start()
println("Главный поток завершён")
}
Однако ручное управление потоками имеет недостатки:
- Высокие накладные расходы — создание и уничтожение потоков дорого.
- Сложность синхронизации — легко допустить race condition.
- Ограниченное количество потоков — система может создать лишь ограниченное число потоков.
Для синхронизации доступа к общим данным используется ключевое слово synchronized или блокировки ReentrantLock:
class Counter {
private var count = 0
@Synchronized
fun increment() {
count++
}
}
2. Корутины — современный подход к конкурентности
Корутины — это легковесные потоки, которые решают проблемы классической многопоточности. Они не привязаны к системным потокам и могут выполняться на одном потоке, переключаясь между задачами.
2.1. Первая корутина
Для работы с корутинами нужно подключить зависимость org.jetbrains.kotlinx:kotlinx-coroutines-core. Пример простейшей корутины:
import kotlinx.coroutines.*
fun main() = runBlocking {
launch {
delay(1000L)
println("Мир!")
}
println("Привет,")
}
Здесь runBlocking — мост между обычным кодом и корутинами. launch запускает новую корутину, а delay — аналог Thread.sleep(), но не блокирующий поток.
2.2. Suspend-функции
Ключевое слово suspend позволяет приостановить выполнение функции без блокировки потока:
suspend fun fetchData(): String {
delay(2000L) // имитация сетевого запроса
return "Данные получены"
}
fun main() = runBlocking {
val result = async { fetchData() }
println("Ожидание...")
println(result.await())
}
2.3. Структурированная конкурентность
Kotlin поддерживает структурированную конкурентность — все дочерние корутины автоматически завершаются до завершения родительской:
fun main() = runBlocking {
coroutineScope {
launch {
delay(1000L)
println("Задача 1")
}
launch {
delay(500L)
println("Задача 2")
}
}
println("Все задачи завершены")
}
3. Продвинутые техники: Channel, Flow и Dispatchers
3.1. Каналы (Channel)
Каналы позволяют обмениваться данными между корутинами. Это аналог очереди:
fun main() = runBlocking {
val channel = Channel<Int>()
launch {
for (i in 1..5) {
channel.send(i * i)
}
channel.close()
}
for (value in channel) {
println(value)
}
}
3.2. Flow — холодные потоки данных
Flow — это реактивный поток, который эмиттирует значения по запросу:
fun simpleFlow(): Flow<Int> = flow {
for (i in 1..3) {
delay(100L)
emit(i)
}
}
fun main() = runBlocking {
simpleFlow().collect { value ->
println(value)
}
}
3.3. Dispatchers — управление потоками
Диспетчеры определяют, на каком пуле потоков выполняется корутина:
Dispatchers.Default— для CPU-интенсивных задач.Dispatchers.IO— для операций ввода-вывода (сеть, файлы).Dispatchers.Main— для UI-потока (Android, JavaFX).
fun main() = runBlocking {
launch(Dispatchers.Default) {
println("CPU-интенсивная работа на ${Thread.currentThread().name}")
}
launch(Dispatchers.IO) {
println("Сетевой запрос на ${Thread.currentThread().name}")
}
}
4. Практические рекомендации и антипаттерны
4.1. Что делать
- Используйте корутины вместо потоков для асинхронных операций.
- Применяйте
async/awaitдля параллельного выполнения независимых задач. - Всегда обрабатывайте исключения с помощью
try-catchилиCoroutineExceptionHandler.
4.2. Чего избегать
- Не вызывайте
GlobalScope.launch— это утечка памяти. - Не используйте блокирующие функции (например,
Thread.sleep()) внутри корутин — толькоdelay(). - Не смешивайте корутины с классическими потоками без необходимости — это усложняет код.
// Плохо: блокирующий вызов внутри корутины
launch {
Thread.sleep(1000) // блокирует поток!
}
// Хорошо: неблокирующая задержка
launch {
delay(1000) // приостанавливает корутину
}
Заключение
Конкурентность и многопоточность в Kotlin — это не просто возможность, а мощный инструмент для создания эффективных и отзывчивых приложений. Классические потоки остаются актуальными для простых сценари