Сопрограммы: runBlocking против coroutineScope

Я читал основы Coroutine, пытаясь понять и изучить это.

Там есть часть с этим кодом:

fun main() = runBlocking { // this: CoroutineScope
    launch { 
        delay(200L)
        println("Task from runBlocking")
    }

    coroutineScope { // Creates a new coroutine scope
        launch {
            delay(900L) 
            println("Task from nested launch")
        }

        delay(100L)
        println("Task from coroutine scope") // This line will be printed before nested launch
    }

    println("Coroutine scope is over") // This line is not printed until nested launch completes
}

Вывод выглядит так:

Task from coroutine scope
Task from runBlocking
Task from nested launch
Coroutine scope is over

Мой вопрос, почему эта строка:

 println("Coroutine scope is over") // This line is not printed until nested launch completes

называется всегда последний?

Не должен ли он быть вызван, так как:

coroutineScope { // Creates a new coroutine scope
    ....
}

приостановлено?

Там также есть примечание:

Основное различие между runBlocking и coroutineScope заключается в том, что последний не блокирует текущий поток, ожидая завершения всех дочерних элементов.

Я не понимаю, чем здесь отличаются coroutineScope и runBlocking? coroutineScope выглядит как его блокировка, так как он достигает только последней строки, когда это сделано.

Кто-нибудь может просветить меня здесь?

Заранее спасибо.

Ответ 1

Я не понимаю, чем отличаются coroutineScope и runBlocking? coroutineScope выглядит как его блокировка, так как он достигает только последней строки, когда это сделано.

С точки зрения кода в блоке, ваше понимание верно. Разница между runBlocking и coroutineScope происходит на более низком уровне: что происходит с потоком, пока сопрограмма блокируется?

  • runBlocking - это не suspend fun. Нить, вызвавшая его, остается внутри, пока сопрограмма не будет завершена.

  • coroutineScope это suspend fun. Если ваша сопрограмма приостанавливается, функция coroutineScope приостанавливается. Это позволяет функции верхнего уровня, не приостанавливающей функцию, которая создала сопрограмму, продолжить выполнение в том же потоке. Поток "вышел" из блока coroutineScope и готов выполнить другую работу.

В вашем конкретном примере: когда ваш coroutineScope приостанавливается, элемент управления возвращается к коду реализации внутри runBlocking. Этот код представляет собой цикл обработки событий, который управляет всеми сопрограммами, которые вы начали в нем. В вашем случае будут некоторые сопрограммы, запланированные для запуска после задержки. Когда время придет, оно возобновит соответствующую сопрограмму, которая будет работать в течение короткого времени, приостанавливается, и затем управление снова будет внутри runBlocking.


Хотя вышеизложенное описывает концептуальное сходство, оно также должно показать вам, что runBlocking - это совершенно другой инструмент, чем coroutineScope.

  • runBlocking - это низкоуровневая конструкция, предназначенная для использования только в коде фреймворка или автономных примерах, подобных вашему. Он превращает существующий поток в цикл событий и создает его сопрограмму с Dispatcher который отправляет возобновляемые сопрограммы в очередь цикла событий.

  • coroutineScope - это ориентированная на пользователя конструкция, используемая для определения границ задачи, которая распадается внутри нее параллельно. Вы используете его для удобного ожидания всей async работы, происходящей внутри него, получения окончательного результата и обработки всех сбоев в одном центральном месте.

Ответ 2

runBlocking просто блокирует текущий поток, пока внутренние сопрограммы не будут завершены. Здесь поток, выполняющий runBlocking будет заблокирован, пока сопрограмма из coroutineScope не будет завершена.

Первый launch просто не позволит потоку выполнить инструкции, которые идут после runBlocking, но позволит перейти к инструкциям, которые идут сразу после этого блока launch - именно поэтому Task from coroutine scope Task from runBlocking раньше, чем Task from runBlocking.

Но вложенный coroutineScope в контексте runBlocking не позволит потоку выполнять инструкции, которые следуют после этого блока coroutineScope, потому что runBlocking будет блокировать поток до тех пор, пока сопрограмма из coroutineScope не будет полностью завершена. И вот почему Coroutine scope is over, всегда будет приходить после Task from nested launch.

Ответ 3

Из этой замечательной статьи https://jivimberg.io/blog/2018/05/04/parallel-map-in-kotlin/

suspend fun <A, B> Iterable<A>.pmap(f: suspend (A) -> B): List<B> = coroutineScope {
    map { async { f(it) } }.awaitAll()
}

При использовании runBlocking мы не использовали структурированный параллелизм, поэтому вызов f может завершиться сбоем, и все остальные выполнения останутся неизменными. А также мы не очень хорошо играли с остальным кодом. Используя runBlocking, мы принудительно блокировали поток до тех пор, пока не закончится все выполнение pmap, вместо того, чтобы позволить вызывающей стороне решить, как должно выполняться выполнение.