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

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

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

Начать курс

Введение в конкурентность и многопоточность в 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 автоматически захватывает и освобождает мьютекс, гарантируя

Блоги

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