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) — лег