Kotlin

Чем отличаются 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 не приостанавливаются и не имеют точек приостановки, корутина смогла завершить выполнение.
 

Теги: