Конкурентность и многопоточность в Kotlin: продвинутый уровень

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

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

Начать курс

Конкурентность и многопоточность в Kotlin: продвинутый уровень

В современной разработке на Kotlin, особенно для серверных и Android-приложений, грамотное управление конкурентностью — ключ к производительности и отзывчивости. На продвинутом уровне недостаточно просто запустить корутину. Нужно понимать, как эффективно синхронизировать доступ к данным, управлять потоками и избегать “адских” блокировок. В этой статье мы разберем продвинутые техники: от тонкостей kotlinx.coroutines до thread-safe коллекций и каналов.

1. Глубокое погружение в корутины: Structured Concurrency и Scopes

Основой продвинутой работы является Structured Concurrency. Это принцип, при котором жизненный цикл корутины привязан к области видимости (scope). Если родительская корутина отменяется, все её дочерние задачи автоматически завершаются. Это предотвращает утечки памяти и неконтролируемые процессы.

На продвинутом уровне вы должны создавать собственные скоупы, а не полагаться на глобальный GlobalScope.

import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext

class DataRepository {
    // Собственный scope с диспетчером и обработчиком исключений
    private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob() + CoroutineExceptionHandler { _, throwable ->
        println("Необработанная ошибка: ${throwable.message}")
    })

    fun loadData() {
        scope.launch {
            // дочерние корутины
            val data = async { fetchFromNetwork() }
            val cache = async { loadFromCache() }
            process(data.await(), cache.await())
        }
    }

    fun cleanup() {
        scope.cancel() // отменяем все корутины в скоупе
    }
}

Используйте SupervisorJob, когда сбой одной дочерней корутины не должен отменять остальные. Это критично для UI-слоев или параллельных независимых запросов.

2. Thread-Safe коллекции и атомарные операции

Даже с корутинами, если вы работаете с общими изменяемыми данными, нужна синхронизация. Kotlin предлагает несколько подходов:

  • Атомарные типы (AtomicInteger, AtomicReference) — для простых счетчиков и флагов.
  • Thread-Safe коллекции (ConcurrentHashMap, CopyOnWriteArrayList) — для сложных структур.
  • Mutex из kotlinx.coroutines — для критических секций внутри корутин.
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicInteger

class SafeCounter {
    private val mutex = Mutex()
    private var counter = 0

    suspend fun increment() {
        mutex.withLock {
            counter++ // критическая секция
        }
    }

    fun getCounter(): Int = counter
}

class CacheManager {
    private val cache = ConcurrentHashMap<String, String>()
    private val hitCount = AtomicInteger(0)

    fun put(key: String, value: String) {
        cache[key] = value
    }

    fun get(key: String): String? {
        val result = cache[key]
        if (result != null) hitCount.incrementAndGet()
        return result
    }
}

Важно: Mutex — suspend-функция, поэтому он не блокирует поток, а приостанавливает корутину. Это эффективнее, чем synchronized.

3. Каналы (Channels) и Flow: реактивные потоки данных

Для передачи данных между корутинами используйте Channels (аналог BlockingQueue для корутин) и Flow (холодный асинхронный поток).

Каналы: Producer-Consumer

import kotlinx.coroutines.channels.*
import kotlinx.coroutines.*

fun CoroutineScope.produceNumbers(): ReceiveChannel<Int> = produce {
    var x = 1
    while (x <= 10) {
        send(x++) // отправляем данные
        delay(100)
    }
}

fun CoroutineScope.processNumbers(channel: ReceiveChannel<Int>) = launch {
    for (number in channel) {
        println("Получено: $number")
    }
}

fun main() = runBlocking {
    val channel = produceNumbers()
    processNumbers(channel)
    delay(2000)
    channel.cancel() // закрываем канал
}

Flow: холодный асинхронный стрим

Flow идеален для обработки последовательностей данных (например, поток обновлений базы данных или результаты поиска). Он ленивый и поддерживает операторы как в коллекциях.

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun searchFlow(query: String): Flow<String> = flow {
    emit("Начинаем поиск: $query")
    delay(500) // имитация сетевого запроса
    emit("Результат 1...")
    delay(300)
    emit("Результат 2...")
}.flowOn(Dispatchers.Default) // меняем контекст выполнения

fun main() = runBlocking {
    searchFlow("Kotlin")
        .catch { e -> emit("Ошибка: ${e.message}") }
        .collect { println(it) }
}

Продвинутый трюк: используйте flatMapMerge для параллельной обработки потоков, но контролируйте количество одновременных корутин через параметр concurrency.

4. Оптимизация диспетчеров и пулов потоков

Выбор правильного CoroutineDispatcher критичен для производительности:

  • Dispatchers.Main — для UI (Android/Swing).
  • Dispatchers.IO — для блокирующих операций (чтение файлов, сеть).
  • Dispatchers.Default — для CPU-интенсивных задач.
  • newSingleThreadContext() — для последовательного доступа к разделяемым ресурсам.

На продвинутом уровне вы можете создать свой пул потоков с ограничениями:

import kotlinx.coroutines.*
import java.util.concurrent.Executors

val customDispatcher = Executors.newFixedThreadPool(4).asCoroutineDispatcher()

fun main() = runBlocking {
    val jobs = List(100) {
        launch(customDispatcher) {
            // CPU-интенсивная работа
            var sum = 0L
            for (i in 1..1_                

Блоги

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