В чем разница между потоком (Thread) и корутиной (Coroutine) в Kotlin?
Главное отличие: потоки — это ресурсы ОС, корутины — это управляемые задачи
Давайте разберем фундаментальную разницу. Поток (Thread) — это объект операционной системы. Каждый поток имеет свой стек памяти (обычно 1-2 МБ), и переключение между потоками — дорогостоящая операция, требующая вмешательства ядра ОС. Вы можете запустить максимум несколько тысяч потоков на обычном компьютере, прежде чем система начнет тормозить или выдавать ошибки.
Корутина (Coroutine) — это логическая единица работы, которая выполняется внутри потока. Корутины не привязаны к конкретному потоку. Они могут приостанавливаться (suspend) в одной точке и возобновляться в другой, возможно, на совершенно другом потоке. Ключевое слово здесь — suspend (приостановка), а не блокировка.
Почему ваш пример с корутиной не показывает переключение потоков?
В вашем примере используется runBlocking — это мост между обычным кодом и корутинами. Он блокирует текущий поток (main) и создает диспетчер корутин, который по умолчанию использует тот же поток для выполнения. Если вы хотите увидеть переключение потоков, используйте Dispatchers.Default или Dispatchers.IO:
fun main() = runBlocking {
println("Main: ${Thread.currentThread().name}")
launch(Dispatchers.Default) {
println("Coroutine on Default: ${Thread.currentThread().name}")
delay(500)
println("After delay: ${Thread.currentThread().name}")
}
delay(100)
println("Main continues: ${Thread.currentThread().name}")
}
Вы увидите, что корутина может начаться на одном потоке из пула, а после delay() продолжиться на другом. Это и есть переключение контекста без блокировки.
Ключевые различия в деталях:
- Создание и стоимость: Создать поток — дорого (аллокация стека, системный вызов). Создать корутину — дешево (просто объект в куче, ~несколько десятков байт). Можно запустить миллионы корутин без проблем.
- Блокировка vs Приостановка:
Thread.sleep()блокирует поток — ОС вытесняет его, и никакая другая работа на этом потоке невозможна.delay()— это suspend-функция: она говорит "я подожду, а поток пока может делать другую работу". Поток не блокируется, он переключается на другую корутину. - Управление: Потоками управляет ОС (вытесняющая многозадачность). Корутинами управляет ваша программа (кооперативная многозадачность) — корутина сама решает, когда приостановиться.
- Память: Каждый поток имеет свой стек (минимум 64 КБ на Android, 1 МБ на JVM). Корутины используют общий стек потока, на котором они выполняются, экономя память.
Когда что использовать?
Используйте потоки, когда:
- Вы выполняете CPU-интенсивные задачи, которые не делают пауз (циклы, вычисления).
- Работаете с библиотеками, которые блокируют поток (старые Java-библиотеки).
- Нужен настоящий параллелизм на многоядерном процессоре.
Используйте корутины, когда:
- У вас много задач, ожидающих I/O (сеть, база данных, файлы).
- Нужно обрабатывать тысячи одновременных соединений.
- Пишете асинхронный код, который должен читаться как синхронный (без callback-ада).
- Работаете с UI (Android, Swing) — корутины позволяют легко переключаться между фоновым и UI-потоком.
Практический пример: 10 000 задач
Попробуйте запустить 10 000 потоков — вероятно, получите OutOfMemoryError. А 10 000 корутин — без проблем:
fun main() = runBlocking {
val jobs = List(10_000) {
launch(Dispatchers.Default) {
delay(1000)
print(".")
}
}
jobs.joinAll()
println("\nDone! All 10,000 coroutines finished.")
}
Этот код использует всего несколько потоков (обычно 4-8, по числу ядер процессора), но обрабатывает 10 000 задач, просто переключаясь между ними.
В итоге: корутины — это не "замена" потокам, а надстройка над ними. Они позволяют эффективно использовать потоки, не блокируя их, и писать асинхронный код в императивном стиле.
<<Дополнение: структурная конкурентность и жизненный цикл
Еще одно важное отличие, которое не сразу видно из примеров — это структурная конкурентность (structured concurrency) в корутинах.
Когда вы создаете поток, он "отрывается" от родительского кода. Вы должны вручную управлять его жизненным циклом: запустить, дождаться (join()), обработать исключения. Если вы забудете это сделать, поток может работать вечно или завершиться с ошибкой, которую никто не поймает.
Корутины же живут в рамках CoroutineScope. Все дочерние корутины автоматически привязываются к родительской. Если родительская корутина отменяется — все дочерние тоже отменяются. Если дочерняя корутина выбрасывает исключение — оно всплывает к родителю. Это делает код более предсказуемым и безопасным:
fun main() = runBlocking {
// Если эта корутина будет отменена, child тоже отменится
val parent = launch {
val child = launch {
try {
delay(Long.MAX_VALUE)
} catch (e: CancellationException) {
println("Child was cancelled!")
}
}
delay(100)
// Родитель отменяется -> дочерняя тоже
}
delay(50)
parent.cancel()
println("Parent cancelled")
}
С потоками такой гарантии нет. Вы можете создать поток, который будет "висеть" в фоне, даже если программа уже завершила основную работу. Поэтому в современном Kotlin корутины — это стандартный способ работы с асинхронностью, а потоки остаются для низкоуровневых задач или интеграции с Java-кодом.