Kotlin конкурентность: потоки, горутины и async/await для начинающих

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

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

Начать курс

Kotlin vs Python: кто кого? Битва за конкурентность

Если вы читаете этот материал на PythonLib, то, скорее всего, уже знакомы с Python и его GIL (Global Interpreter Lock), который превращает многопоточность в головную боль. Вы пишете threading, надеетесь на прирост скорости, а получаете… синхронный код с привкусом блокировок. Знакомо?

Добро пожаловать в мир Kotlin! Это язык, который решил: «А давайте сделаем конкурентность удобной, быстрой и без боли». В этой статье мы разберем три подхода к конкурентности в Kotlin: классические потоки, современные горутины и мощный дуэт async/await. Вы узнаете, как писать асинхронный код, который не взрывает мозг, и почему Kotlin — отличный выбор для backend и Android-разработки.

1. Потоки в Kotlin: старый друг лучше новых двух?

Начнем с базы. Потоки (threads) в Kotlin — это те же потоки, что и в Java. Kotlin работает на JVM, поэтому у вас есть доступ ко всей мощи java.lang.Thread. Но давайте честно: потоки — это как езда на машине без подушек безопасности. Работает, но страшно.

Как создать поток в Kotlin?

Самый простой способ:

fun main() {
    thread {
        println("Привет из потока: ${Thread.currentThread().name}")
    }
    println("Главный поток: ${Thread.currentThread().name}")
}

Выглядит знакомо, правда? Но есть нюанс: потоки тяжелые. Каждый поток — это отдельный системный ресурс. Если вы создадите 10 000 потоков, ваша программа либо упадет с OutOfMemoryError, либо будет тормозить как старый компьютер с Windows 95.

Проблемы потоков:

  • Высокое потребление памяти — каждый поток резервирует стек (~1 МБ).
  • Сложность синхронизации — блокировки, deadlocks, race conditions. Вспомните synchronized в Java и заплачьте.
  • Переключение контекста — ОС тратит время на переключение между потоками.

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

2. Горутины в Kotlin: легкие, как перышко

Горутины (coroutines) — это «потоки на стероидах». Они не привязаны к системным потокам и могут выполняться на одном потоке, переключаясь между задачами без участия ОС. Звучит сложно? На деле — просто и элегантно.

Первая горутина

Для работы с горутинами вам понадобится зависимость kotlinx-coroutines-core. Но если вы используете Kotlin 1.3+, она уже встроена.

import kotlinx.coroutines.*

fun main() = runBlocking {
    launch {
        delay(1000L)
        println("Мир после задержки!")
    }
    println("Привет,")
}

Что здесь происходит?

  • runBlocking — блокирует главный поток, пока все горутины внутри не завершатся. Для обучения — ок, для продакшена — используйте с осторожностью.
  • launch — запускает новую горутину. Она не блокирует поток, а просто приостанавливается на delay.
  • delay — аналог Thread.sleep(), но не блокирующий. Горутина просто «засыпает» и освобождает поток для других задач.

Результат: сначала выводится «Привет,», а через секунду — «Мир после задержки!». Без блокировок, без лишних потоков.

Почему горутины лучше потоков?

  • Легковесность — миллион горутин? Легко! Они не создают отдельные потоки.
  • Приостановка (suspension) — горутина может остановиться в любой момент и продолжить позже.
  • Структурированная конкурентность — горутины живут в области видимости (scope) и автоматически отменяются, если scope завершается.

Диспетчеры (Dispatchers)

Горутины выполняются на пуле потоков. Вы можете указать, какой диспетчер использовать:

launch(Dispatchers.IO) {
    // для операций ввода-вывода (сеть, файлы)
}
launch(Dispatchers.Default) {
    // для CPU-интенсивных задач
}
launch(Dispatchers.Main) {
    // для Android UI (только для Android)
}

Гибко, правда? Вы не думаете о том, сколько потоков нужно — Kotlin сам управляет пулом.

3. async/await в Kotlin: сила и контроль

Если launch — это «запустил и забыл», то async — это «запустил и жду результат». Идеально для параллельных вычислений, когда нужно собрать данные из нескольких источников.

Простой пример async/await

fun main() = runBlocking {
    val result1 = async {
        delay(1000L)
        42
    }
    val result2 = async {
        delay(1000L)
        58
    }
    println("Сумма: ${result1.await() + result2.await()}")
}

Что тут происходит?

  • async запускает горутину и возвращает объект Deferred (аналог Future/Promise).
  • await() приостанавливает текущую горутину, пока не получит результат.
  • Обе задачи выполняются параллельно, поэтому общее время — ~1 секунда, а не 2.

Это гораздо удобнее, чем в Python с его asyncio.gather() или concurrent.futures. В Kotlin всё встроено и читается как обычный последовательный код.

Обработка ошибок в async/await

Если внутри async вылетит исключение, await() пробросит его дальше. Можно ловить как обычно:

fun main() = runBlocking {
    val deferred = async {
        throw RuntimeException("Упс!")
    }
    try {
        deferred.await()
    } catch (e: Exception) {
        println("Поймали: ${e.message}")
    }
}

Всё предсказуемо и безопасно.

4. Сравнение: потоки vs горутины vs async/await

Давайте наглядно сравним подходы в таблице (мысленно, но вы запомните):

  • Потоки — тяжелые, блокирующие, сложные в синхронизации. Подходят для простых задач или когда нужно использовать старые Java-библиотеки.
  • Горутины (launch) — лег

Блоги

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