Введение в конкурентность и многопоточность в Kotlin
Современные приложения всё чаще требуют выполнения нескольких задач одновременно — от обработки сетевых запросов до работы с базами данных. В языке Kotlin, который активно используется для разработки под Android, серверные приложения (Ktor, Spring) и даже десктопные решения, реализованы мощные инструменты для работы с конкурентностью и многопоточностью.
В этой статье мы разберём ключевые концепции: традиционные потоки (Threads), корутины (Coroutines) — современный подход к асинхронности в Kotlin, а также механизмы синхронизации. Вы узнаете, когда стоит использовать потоки, а когда — корутины, и получите готовые примеры кода.
1. Традиционные потоки (Threads) в Kotlin
Kotlin полностью совместим с Java, поэтому вы можете использовать стандартные классы Thread и Runnable. Однако Kotlin добавляет синтаксический сахар, делающий код более лаконичным.
Создание и запуск потока
Базовый способ создать поток — передать лямбду в конструктор Thread:
fun main() {
val thread = Thread {
println("Поток запущен: ${Thread.currentThread().name}")
for (i in 1..5) {
println("Счёт: $i")
Thread.sleep(500)
}
}
thread.start()
println("Главный поток продолжает работу")
thread.join() // ожидаем завершения потока
println("Главный поток завершён")
}
В этом примере мы создали поток, который выводит числа от 1 до 5 с задержкой в 500 мс. Метод join() заставляет главный поток дождаться завершения дочернего.
Проблемы потоков
- Высокое потребление ресурсов — каждый поток требует отдельного стека памяти (обычно 1 МБ).
- Сложность синхронизации — при работе с общими данными легко допустить состояние гонки (race condition).
- Ограниченное количество — на одном устройстве нельзя запустить десятки тысяч потоков без потери производительности.
Именно поэтому в Kotlin появились корутины — более лёгкая альтернатива.
2. Корутины (Coroutines) — современная конкурентность в Kotlin
Корутины — это легковесные потоки, которые могут приостанавливать своё выполнение без блокировки системного потока. Они являются основным инструментом для асинхронного программирования в Kotlin.
Подключение корутин
Для работы с корутинами необходимо добавить зависимость в build.gradle.kts:
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
}
Базовый пример корутины
import kotlinx.coroutines.*
fun main() = runBlocking {
println("Начало работы в потоке: ${Thread.currentThread().name}")
launch { // запуск новой корутины
delay(1000) // неблокирующая задержка
println("Корутина: ${Thread.currentThread().name}")
}
println("Главная корутина продолжает работу")
delay(2000) // ждём завершения дочерней корутины
println("Конец")
}
Ключевые моменты:
runBlocking— мост между обычным кодом и корутинами (блокирует текущий поток).launch— запускает корутину, которая не возвращает результат.delay— неблокирующая задержка, в отличие отThread.sleep().
Async и await для получения результата
Если корутина должна вернуть значение, используйте async:
fun main() = runBlocking {
val deferred1 = async {
delay(1000)
"Результат 1"
}
val deferred2 = async {
delay(500)
"Результат 2"
}
println("Ожидание результатов...")
val result1 = deferred1.await()
val result2 = deferred2.await()
println("$result1 и $result2")
}
Обе корутины выполняются параллельно, а await() приостанавливает текущую корутину до получения результата.
3. Синхронизация и безопасная работа с общими данными
При работе с многопоточностью (как с потоками, так и с корутинами) возникает проблема одновременного доступа к изменяемым данным. Рассмотрим основные инструменты синхронизации в Kotlin.
Синхронизация потоков (synchronized)
Kotlin поддерживает ключевое слово synchronized из Java:
class Counter {
private var count = 0
fun increment() {
synchronized(this) {
count++
}
}
fun getCount(): Int = count
}
fun main() {
val counter = Counter()
val threads = List(100) {
Thread {
repeat(1000) { counter.increment() }
}
}
threads.forEach { it.start() }
threads.forEach { it.join() }
println("Финальное значение: ${counter.getCount()}") // Всегда 100000
}
Атомарные типы для корутин
Для корутин рекомендуется использовать атомарные классы из kotlinx.atomicfu или стандартные java.util.concurrent.atomic:
import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.*
fun main() = runBlocking {
val counter = AtomicInteger(0)
val jobs = List(100) {
launch(Dispatchers.Default) {
repeat(1000) { counter.incrementAndGet() }
}
}
jobs.forEach { it.join() }
println("Финальное значение: ${counter.get()}") // Всегда 100000
}
Mutex для корутин
Если нужно защитить критическую секцию внутри корутин, используйте Mutex:
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
fun main() = runBlocking {
val mutex = Mutex()
var sharedCounter = 0
massivelrishkaunch {
repeat(1000) {
launch(Dispatchers.Default) {
mutex.withLock {
sharedCounter++
}
}
}
}
println("Финальное значение: $sharedCounter")
}
withLock автоматически захватывает и освобождает мьютекс, гарантируя