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

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

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

Начать курс

Конкурентность и многопоточность в 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 — это не просто возможность, а мощный инструмент для создания эффективных и отзывчивых приложений. Классические потоки остаются актуальными для простых сценари

Блоги

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