Чем отличаются runBlocking и coroutineScope
Запуск корутин
В этой статье мы рассмотрим разницу между двумя способами запуска корутин: runBlocking
и coroutineScope
Эти функции - билдеры корутин (coroutine builders), то есть они запускают корутины. Но их используют для разных целей.
Когда мы вызываем suspendCoroutine чтобы создать и запустить корутину, мы создаем точку приостановки (suspension point). Но мы можем делать точки приостановки только там, где приостановка возможна: внутри другой корутины или suspend функции.
Иногда нам нужно запустить корутину там, где нет другой корутины или suspend функции. Например, в функции main или из юнит теста. Тогда нам следует использовать runBlocking. Название билдера подсказывает нам, что корутины, которые он запускает, блокируют текущий поток. Он не создает точку приостановки, даже если мы вызываем его внутри другой корутины.
Приостановка корутин
Посмотрим могут ли приостанавливаться корутины созданные этими двумя билдерами.
Приостановка корутин coroutineScope
Корутины созданные coroutineScope
приостанавливаются. Чтобы проверить это, для начала создадим диспетчер (coroutine dispatcher) из пула (thread pool) в 2 потока:
val dispatcher = Executors.newFixedThreadPool(2).asCoroutineDispatcher()
Далее напишем функцию, которая запустит 6 корутин, каждая из которых запустит дочернюю корутину через coroutineScope:
fun demoCoroutineScope() = runBlocking {
repeat(6) {
launch(dispatcher) {
coroutineScope {
println("start ${Thread.currentThread().name}")
delay(1000)
println("end ${Thread.currentThread().name}")
}
}
}
}
Мы начали с runBlocking
, чтобы использовать корутины в блокирующем коде. Внутри runBlocking
мы используем launch чтобы выполнять корутины на свободных потоках из пула в диспетчере. А затем мы используем coroutineScope
, чтобы запустить корутину и в ней вызовем delay на 1000 миллисекунд. delay()
- это suspend функция и создаст точку приостановки внутри корутины, запущенной через coroutineScope.
Вызовем эту функцию и посмотрим как Котлин приостанавливает эти корутины.
fun main() {
val millis = measureTimeMillis {
demoWithCoroutineScope()
}
println("millis = $millis")
}
И мы получим примерно такой результат:
start pool-1-thread-1 @coroutine#2
start pool-1-thread-2 @coroutine#3
start pool-1-thread-1 @coroutine#5
start pool-1-thread-2 @coroutine#4
start pool-1-thread-1 @coroutine#6
start pool-1-thread-2 @coroutine#7
end pool-1-thread-1 @coroutine#2
end pool-1-thread-2 @coroutine#3
end pool-1-thread-1 @coroutine#5
end pool-1-thread-2 @coroutine#4
end pool-1-thread-1 @coroutine#6
end pool-1-thread-2 @coroutine#7
millis = 1082
Несмотря на то что каждая корутина вызывала delay()
на 1000 мс, общее время выполнения получилось не сильно больше данного значения. Мы также видим, что некоторые корутины, которые начались на pool-1-thread-1, завершились на pool-1-thread-2, и наоборот.
Котлин приостанавливал каждую корутину на точке приостановки, где был вызов функции delay()
. Так как приостановленная корутина освобождала свой поток, другая корутина запускалась на нем, вызывала свой delay()
и так же приостанавливалась. После 1000 мс. задержки каждая корутина возобновляла свое выполнение на свободном потоке из пула.
Приостановка корутин runBlocking
Теперь заменим coroutineScope
на runBlocking
:
fun demoRunBlocking() = runBlocking {
repeat(6) {
launch(dispatcher) {
runBlocking {
println("start ${Thread.currentThread().name}")
delay(1000)
println("end ${Thread.currentThread().name}")
}
}
}
}
И мы получим другой результат:
start pool-1-thread-1 @coroutine#4
start pool-1-thread-2 @coroutine#9
end pool-1-thread-2 @coroutine#9
end pool-1-thread-1 @coroutine#4
start pool-1-thread-2 @coroutine#10
start pool-1-thread-1 @coroutine#11
end pool-1-thread-2 @coroutine#10
end pool-1-thread-1 @coroutine#11
start pool-1-thread-2 @coroutine#12
start pool-1-thread-1 @coroutine#13
end pool-1-thread-1 @coroutine#13
end pool-1-thread-2 @coroutine#12
millis = 3088
Каждая корутина завершилась на том же потоке, на котором она начиналась. А общее время выполнения заняло более 3000 секунд. Корутины, запущенные через runBlocking
, игнорировали точку приостановки при вызове delay()
. Значит корутины runBlocking
не приостанавливаются.
Отмена корутин
Эти два билдера корутин также отличаются по возможности отменять их корутины.
Отмена корутин coroutineScope
Попробуем отменить корутину спустя 100 миллисекунд после ее запуска. Для этого понадобится объект Job, который нам возвращает launch.
private fun cancelCoroutineScope() = runBlocking {
val job = launch {
coroutineScope {
println("Start")
delay(500)
println("End")
}
}
delay(100)
job.cancel()
}
Вызов cancel()
пытается отменить корутину. Проверим это:
fun main() = runBlocking {
val time = measureTimeMillis {
cancelCoroutineScope()
}
println("time = $time")
}
И получаем результат:
Start
time = 164
Мы видим, что корутина отменилась на точке приостановки при вызове delay(500) немного позже задержки в 100мс.
Отмена корутин runBlocking
Повторим то же самое, используя runBlocking:
private fun cancelRunBlocking() = runBlocking {
val job = launch {
runBlocking {
println("Start")
delay(500)
println("End")
}
}
delay(100)
job.cancel()
}
Проверим:
val time = measureTimeMillis {
cancelRunBlocking()
}
println("time = $time")
И мы получаем другой результат:
Start
End
time = 514
Мы видим, что корутины запущенные с помощью runBlocking
нельзя отменить. Так как отмена происходит на точках приостановки, а корутины runBlocking
не приостанавливаются и не имеют точек приостановки, корутина смогла завершить выполнение.