В чем разница между runBlocking и coroutineScope в Kotlin Coroutines?
Привет, коллеги! Я начал изучать корутины в Kotlin и наткнулся на две функции, которые кажутся мне очень похожими: runBlocking и coroutineScope. Я читал документацию, но для меня их поведение до сих пор не очевидно.
Вот пример, который я написал:
fun main() = runBlocking {
launch {
delay(1000L)
println("World!")
}
println("Hello,")
}
И другой:
fun main() = runBlocking {
coroutineScope {
launch {
delay(1000L)
println("World!")
}
println("Hello,")
}
}
Оба примера выводят сначала "Hello,", а через секунду "World!". Но я подозреваю, что разница есть, и она важна. В чем она заключается?
Конкретно меня интересует:
- Почему
runBlockingблокирует текущий поток, аcoroutineScope— нет? - Как эти функции ведут себя в реальных приложениях, например, в Android или серверном коде?
- Когда какую из них лучше использовать?
Буду благодарен за подробное объяснение с примерами.
<<Ключевое различие: блокировка потока vs приостановка корутины
Разница между runBlocking и coroutineScope фундаментальна и связана с тем, как они взаимодействуют с потоком выполнения.
1. runBlocking — блокирует поток
runBlocking — это мост между блокирующим и асинхронным миром. Она запускает новую корутину и блокирует текущий поток до тех пор, пока все корутины внутри не завершатся. Это значит, что пока выполняется runBlocking, никакой другой код в этом потоке не может выполняться.
Пример с ошибкой:
fun main() {
runBlocking {
delay(1000L) // Блокирует main поток на 1 секунду
println("Inside blocking")
}
println("After runBlocking") // Выполнится только после завершения runBlocking
}
Когда использовать:
- В
main()функции, чтобы дождаться завершения корутин. - В тестах, где нужно проверить асинхронный код синхронно.
- В библиотеках, где вы не можете использовать suspend функции, но должны вызвать корутину из блокирующего кода (например, в Java-коде или в колбэках).
Никогда не используйте runBlocking в продакшене Android или серверных приложениях — это заблокирует UI-поток или поток обработки запроса, что приведет к зависанию.
2. coroutineScope — приостанавливает корутину
coroutineScope — это suspend-функция. Она не блокирует поток, а приостанавливает текущую корутину до завершения всех дочерних корутин. Поток, на котором выполняется корутина, освобождается для других задач.
Пример:
suspend fun doWork() = coroutineScope {
launch {
delay(1000L)
println("Child coroutine finished")
}
println("Scope started")
} // Здесь корутина приостановится, но поток не заблокирован
Когда использовать:
- Внутри suspend функций, чтобы выполнить несколько параллельных задач и дождаться их всех.
- Для структурированной конкурентности — все дочерние корутины автоматически отменяются, если родительская корутина отменяется.
- В Android (ViewModel, Activity) и серверных приложениях (Ktor, Spring WebFlux) — это основной способ работы с корутинами.
3. Практический пример: разница в поведении
Рассмотрим код, который показывает разницу:
fun main() = runBlocking {
println("Before coroutineScope")
coroutineScope {
launch {
delay(1000L)
println("Task 1 done")
}
launch {
delay(500L)
println("Task 2 done")
}
println("Inside coroutineScope")
}
println("After coroutineScope") // Выполнится только после завершения всех дочерних корутин
// А теперь runBlocking без coroutineScope:
runBlocking {
launch {
delay(1000L)
println("Task 3 done")
}
launch {
delay(500L)
println("Task 4 done")
}
println("Inside runBlocking")
}
println("After runBlocking")
}
Вывод будет:
Before coroutineScope
Inside coroutineScope
Task 2 done
Task 1 done
After coroutineScope
Inside runBlocking
Task 4 done
Task 3 done
After runBlocking
Обратите внимание: coroutineScope ведет себя как обычная suspend-функция — она приостанавливает выполнение дочерней корутины runBlocking, но не блокирует поток. runBlocking же блокирует поток, и второй вызов runBlocking не может начаться, пока первый не завершится.
4. Итоговая таблица
| Характеристика | runBlocking | coroutineScope |
|---|---|---|
| Тип | Обычная функция (не suspend) | Suspend-функция |
| Блокирует поток? | Да | Нет (приостанавливает корутину) |
| Где использовать | main(), тесты, bridge | Внутри suspend-функций, ViewModel, серверов |
| Отмена дочерних | Нет автоматической отмены | Да (структурированная конкурентность) |
Золотое правило: используйте coroutineScope везде, где это возможно, и runBlocking только как крайнее средство для интеграции с блокирующим кодом.
Дополнение: практические сценарии использования
Хочу добавить несколько практических советов, которые помогут избежать ошибок.
1. runBlocking в тестах
В юнит-тестах runBlocking — это стандартный способ тестировать suspend-функции:
@Test
fun testMySuspendFunction() = runBlocking {
val result = mySuspendFunction()
assertEquals(expected, result)
}
2. coroutineScope для параллельных задач
Используйте coroutineScope для выполнения нескольких задач одновременно и ожидания всех результатов:
suspend fun fetchData(): Data = coroutineScope {
val deferred1 = async { api1.fetch() }
val deferred2 = async { api2.fetch() }
Data(deferred1.await(), deferred2.await())
}
3. Ошибка: runBlocking внутри coroutineScope
Избегайте вложенных runBlocking — это может привести к блокировке потоков и проблемам с производительностью:
// ПЛОХО
suspend fun bad() = coroutineScope {
runBlocking { // Блокирует поток! Не делайте так
delay(1000)
}
}
// ХОРОШО
suspend fun good() = coroutineScope {
delay(1000) // Просто используем delay
}
4. Исключения и отмена
В coroutineScope если одна дочерняя корутина выбрасывает исключение, все остальные автоматически отменяются. В runBlocking такого не происходит — исключение распространяется на родительский поток.
Помните: coroutineScope — это инструмент для структурированной конкурентности, а runBlocking — это костыль для интеграции с блокирующим миром.
Ответов пока нет.
Войдите чтобы оставить ответ
Блоги