【问题标题】:Coroutines: runBlocking vs coroutineScope协程:runBlocking 与 coroutineScope
【发布时间】:2023-03-07 06:42:01
【问题描述】:

我正在阅读 Coroutine Basics 试图理解和学习它。

这里有一段代码:

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 看起来像它的阻塞,因为它只在完成后才到达最后一行。

有人能告诉我吗?

提前致谢。

【问题讨论】:

  • 你还可以找到关于coroutineScopeinsightful...的文档。
  • 应该更新文档以更好地解释这一点,因为您不是唯一一个以这种方式解释它的人。
  • Kotlin 的文档如此糟糕,不断抛出概念,解释混乱。

标签: kotlin kotlin-coroutines


【解决方案1】:

我不明白 coroutineScope 和 runBlocking 在这里有何不同? coroutineScope 看起来像它的阻塞,因为它只在完成后才到达最后一行。

从block中的代码来看,你的理解是正确的。 runBlockingcoroutineScope 之间的区别发生在较低级别:协程阻塞时线程发生了什么?

  • runBlocking 不是suspend fun。调用它的线程会一直留在其中,直到协程完成。

  • coroutineScope 是一个suspend fun。如果你的协程挂起,coroutineScope 函数也会被挂起。这允许顶层函数,一个创建协程的非挂起函数,继续在同一个线程上执行。线程已经“逃脱”了coroutineScope 块并准备好做一些其他的工作。

在您的具体示例中:当您的coroutineScope 挂起时,控制权返回到runBlocking 内的实现代码。这段代码是一个事件循环,它驱动你在其中启动的所有协程。在您的情况下,将有一些协程计划在延迟后运行。时间到了会恢复相应的协程,运行一小会儿,挂起,然后控制又回到runBlocking里面。


虽然上面描述了概念上的相似之处,但它也应该向您展示runBlocking 是一个与coroutineScope 完全不同的工具

  • runBlocking 是一个低级结构,仅用于框架代码或像您这样的自包含示例。它将现有线程转换为事件循环,并使用Dispatcher 创建其协程,该Dispatcher 将恢复的协程发布到事件循环的队列中。

  • coroutineScope 是一个面向用户的构造,用于描绘在其中并行分解的任务的边界。您可以使用它方便地等待其内部发生的所有async 工作,获得最终结果,并在一个中心位置处理所有故障。

【讨论】:

  • 协程作用域结束后最后一行被调​​用是不是因为'runBlocking'?
  • 没有。两个独立的世界是 suspendable 世界(在协程内)和 non-suspendable 世界。一旦你进入runBlocking 的主体,你就进入了可暂停的世界,suspend funs 的行为就像阻塞代码,在suspend fun 返回之前你无法进入下一行。 coroutineScope 是一个 suspend fun,仅当其中的所有协程完成时才返回。因此最后一行必须打印在最后。
  • 我终于明白了!非常感谢!我将“CoroutineScope”与“coroutineScope”混淆了。现在我不明白它们是两个不同的东西。 'CoroutineScope' 启动一个协程,而 'coroutineScope' 是一个挂起函数,它连接了协程的构建和挂起函数:) 非常感谢 :)
  • CoroutineScope 只是CoroutineContext 的持有者,仅此而已。 launchasync 和其他人是 CoroutineScope 上的扩展乐趣,它们只是获取其上下文,将其与您在参数中额外提供的任何上下文结合起来,结果就是您正在构建的协程的上下文。
  • @stdout 那句话讲的是底层机制。继续执行的是顶级事件循环代码,而不是任何协程。然后该代码将选择另一个分派到同一线程的协程并恢复它。
【解决方案2】:

选择的答案很好,但未能解决所提供示例代码的其他一些重要方面。例如,启动是非阻塞的,并假设立即执行。那明显是错的。启动本身立即返回,但启动内的代码确实似乎已放入队列中,并且仅在先前放入队列中的任何其他启动完成时才执行。

这是一段类似的示例代码,其中删除了所有延迟并包含了额外的启动。不看下面的结果,看看你是否可以预测数字的打印顺序。你很有可能会失败:

import kotlinx.coroutines.*

fun main() = runBlocking {
    launch { 
        println("1")
    }

    coroutineScope {
        launch {
            println("2")
        }

        println("3") 
    }

    coroutineScope {
        launch {
            println("4")
        }

        println("5")
    }

    launch { 
        println("6")
    }

    for (i in 7..100) {
        println(i.toString())
    }

    println("101")
}

结果是:

3
1
2
5
4
7
8
9
10
...
99
100
101
6

最后打印数字 6 的事实,即使在执行了将近 100 次 println 之后,也表明最后一次启动中的代码在启动后的所有非阻塞代码完成之前永远不会被执行。但这也不是真的,因为如果是这样的话,第一次启动不应该在数字 7 到 101 完成之前执行。底线?混合启动和 coroutineScope 是高度不可预测的,如果您希望事情的执行方式有一定的顺序,应该避免。

为了证明 launch 中的代码被放入队列中并且仅在所有非阻塞代码完成后执行,运行这个(不使用 coroutineScopes):

import kotlinx.coroutines.*

fun main() = runBlocking {
    launch { 
        println("1")
    }

    launch { 
        println("2")
    }

    launch { 
        println("3")
    }

    for (i in 4..100) {
        println(i.toString())
    }

    println("101")
}

这是你得到的结果:

4
5
6
...
101
1
2
3

添加 CoroutineScope 会破坏这种行为。它将导致 CoroutineScope 之后的所有非阻塞代码在 CoroutineScope 之前的所有代码都完成之前不会执行。

还应注意,在此代码示例中,队列中的每个启动都按照它们添加到队列中的顺序顺序执行,并且每次启动只会在前一次启动执行之后执行。这可能会使所有启动看起来都共享一个公共线程。这不是真的。他们每个人都有自己的线程。但是,如果启动中的任何代码调用了挂起函数,则队列中的下一个启动将在执行挂起函数时立即启动。老实说,这是很奇怪的行为。为什么不异步运行队列中的所有启动?虽然我不知道这个队列中发生了什么的内部结构,但我的猜测是队列中的每个启动都没有自己的线程,而是都共享一个公共线程。只有在遇到挂起函数时,才会为队列中的下一次启动创建一个新线程。这样做可以节省资源。

总而言之,执行是按以下顺序完成的:

  1. launch 中的代码放置在队列中,并按照添加顺序执行。
  2. 启动后的非阻塞代码会在队列中的任何内容执行之前立即执行。
  3. CoroutineScope 会阻止它后面的所有代码,但会执行队列中的所有启动协程,然后再恢复到 CoroutineScope 后面的代码。

【讨论】:

  • 一方面,CoroutineScopecoroutineScope 之间存在差异,所以要更加小心。
  • coroutineScope“阻塞”它后面的代码的原因是coroutineScope是一个挂起函数。 coroutineScope 挂起,直到所有 couroutines/functions 都返回。它基本上是一个“范围”机制。
  • 非阻塞代码 (launch) 立即返回,这就是为什么它旁边的代码会立即执行。
  • 对此我不确定,但我相信协程中没有“队列”的概念。如果协程在“队列”中处理,则应该不可能让第二个启动的协程没有延迟地在第一个延迟 2 秒的 luanch 协程上先做它的事情。
  • The second launch will not start executing its code until the first one completes --- 这只是使用单线程协程调度器的神器,不属于协程的语义。语义是“当你调用launch时立即开始并发执行”,剩下的就看内部调度了。
【解决方案3】:

runBlocking 是让你阻塞主线程。

coroutineScope 是为你阻塞 runBlocking 的。

【讨论】:

  • 哇,就这么简单! ?
【解决方案4】:

runBlocking 只是阻塞当前线程,直到内部协程完成。在这里,执行runBlocking 的线程将被阻塞,直到来自coroutineScope 的协程完成。

首先launch 只是不允许线程执行runBlocking 之后的指令,但允许继续执行此launch 块之后的指令-这就是为什么Task from coroutine scope 在@ 之前打印的原因987654328@.

但是在runBlocking 的上下文中嵌套coroutineScope 将不允许线程执行此coroutineScope 块之后的指令,因为runBlocking 将阻塞线程,直到来自coroutineScope 的协程将完全完成。这就是为什么Coroutine scope is over 总是在Task from nested launch 之后。

【讨论】:

    【解决方案5】:

    来自这篇精彩的文章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 的整个执行完成,而不是让调用者决定执行的方式。

    【讨论】:

      【解决方案6】:

      好吧,在阅读了这里的所有答案之后,我发现除了重复文档片段的措辞之外,没有一个人回答了这个问题。

      所以,我继续在别处寻找答案并找到它here。它实际上显示了coroutineScoperunBlocking 的行为差异(即暂停和阻塞之间的差异)

      【讨论】:

        猜你喜欢
        • 2019-07-17
        • 2019-02-19
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-05-29
        • 2019-11-13
        • 2021-08-17
        相关资源
        最近更新 更多