协同程序:runBlocking vs coroutineScope

Arc*_*nes 34 kotlin kotlin-coroutines

我正在阅读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
}
Run Code Online (Sandbox Code Playgroud)

输出如下:

Task from coroutine scope
Task from runBlocking
Task from nested launch
Coroutine scope is over
Run Code Online (Sandbox Code Playgroud)

我的问题是为什么这一行:

 println("Coroutine scope is over") // This line is not printed until nested launch completes
Run Code Online (Sandbox Code Playgroud)

被称为永远?

不应该被称为自:

coroutineScope { // Creates a new coroutine scope
    ....
}
Run Code Online (Sandbox Code Playgroud)

暂停了?

那里还有一张纸条:

runBlocking和coroutineScope之间的主要区别在于后者在等待所有子项完成时不会阻塞当前线程.

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

谁能在这里启发我?

提前致谢.

And*_*Dev 32

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

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

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")
}
Run Code Online (Sandbox Code Playgroud)

结果是:

3
1
2
5
4
7
8
9
10
...
99
100
101
6
Run Code Online (Sandbox Code Playgroud)

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

为了证明launchs里面的代码被放入一个队列并且只在所有非阻塞代码完成后才执行,运行这个(不使用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")
}
Run Code Online (Sandbox Code Playgroud)

这是你得到的结果:

4
5
6
...
101
1
2
3
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

  • “在第一个启动完成之前,第二个启动不会开始执行其代码”——这只是使用单线程协程调度程序的产物,不属于协程的语义。语义是“当你调用`launch`时立即开始并发执行”,其余的由内部调度决定。 (5认同)
  • `coroutineScope` 之所以“阻塞”它后面的代码,是因为 `coroutineScope` 是一个挂起函数。`coroutineScope` 会挂起,直到所有协程/函数都返回。它基本上是一个“范围界定”机制。 (2认同)
  • 作为一个初学者,这是令人费解的..:( (2认同)

Mar*_*nik 30

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

从块中代码的角度来看,您的理解是正确的.runBlocking和之间的区别coroutineScope发生在较低级别:协程被阻止时线程发生了什么?

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

  • coroutineScope是一个suspend fun.如果您的协程暂停,该coroutineScope函数也会被暂停.这允许顶级函数(一个创建协同程序的非挂起函数)继续在同一个线程上执行.该线程已"逃脱"该coroutineScope块,并准备做其他工作.

在您的具体示例中:当您coroutineScope暂停时,控件返回到里面的实现代码runBlocking.此代码是一个事件循环,它驱动您在其中启动的所有协同程序.在你的情况下,会有一些协同程序计划在延迟后运行.当时间到来时,它将恢复相应的协程,该协程将运行一段时间,暂停,然后再次控制内部runBlocking.


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

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

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

  • 否。这两个独立的世界是_suspendable_世界(在一个协程内)和__non-suspendable_世界。进入runBlocking主体后,您便进入了可挂起的世界,其中“悬浮乐趣”的行为就像是阻塞代码,直到“悬浮乐趣”返回之前,您无法进入下一行。coroutineScope是一种“悬浮乐趣”,仅在完成其中的所有协程后才返回。因此,最后一行必须在最后打印。 (4认同)
  • 我终于明白了!非常感谢!我将“CoroutineScope”与“coroutineScope”混淆了。现在我明白它们是两个不同的东西。“CoroutineScope”启动一个协程,而“coroutineScope”是一个挂起函数,它连接了协程的构建和挂起函数:)非常感谢:) (4认同)
  • “CoroutineScope”只是“CoroutineContext”的持有者,仅此而已。`launch`、`async` 和其他都是 `CoroutineScope` 上的扩展函数,它们只是获取其上下文,将其与您在参数中额外提供的任何上下文相结合,结果就是您正在构建的协程的上下文。 (3认同)

小智 14

runBlocking 是让你阻塞主线程。

coroutineScope 是为你阻塞 runBlocking。

  • 哇,就是这么简单吗! (2认同)

Bah*_*haa 13

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

于是,我继续在其他地方寻找答案,并在这里找到了答案。coroutineScope它实际上显示了和的行为差异runBlocking(即挂起和阻塞之间的差异)


And*_*nin 7

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

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

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