协程:runBlocking vs coroutineScope

时间:2018-11-29 09:43:05

标签: 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
}

输出如下:

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在等待所有子级完成时不会阻塞当前线程。

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

有人可以启发我吗?

谢谢。

6 个答案:

答案 0 :(得分:15)

  

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

从代码块的角度来看,您的理解是正确的。 runBlockingcoroutineScope之间的区别发生在一个较低的层次:协程被阻止时线程发生了什么?

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

  • coroutineScopesuspend fun。如果协程暂停,则coroutineScope函数也将暂停。这允许创建协程的顶级函数 non-suspending 函数继续在同一线程上执行。线程已“转义” coroutineScope块,并准备做其他工作。

在您的特定示例中:coroutineScope挂起时,控制权返回到runBlocking内部的实现代码。此代码是一个事件循环,可驱动您在其中启动的所有协程。对于您而言,延迟后会安排一些协程运行。时间到了,它将恢复适当的协程,协程将运行一小会儿,然后暂停,然后再次将控制权置于runBlocking内部。


尽管上面描述了概念上的相似性,但它还应该向您显示runBlocking是与coroutineScope完全不同的工具。

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

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

答案 1 :(得分:8)

runBlocking用于阻止主线程。

coroutineScope可让您阻止runBlocking。

答案 2 :(得分:4)

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

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

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

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

要证明启动内的代码已放入队列中并且仅在所有非阻塞代码完成后才执行,请运行此命令(不使用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. 启动中的代码放置在队列中,并按照添加的顺序执行。
  2. 启动后的非阻塞代码将在队列中的任何内容执行之前立即执行。
  3. CoroutineScope阻止其后的所有代码,但在恢复到CoroutineScope之后的代码之前,它将执行队列中的所有启动协程。

答案 3 :(得分:3)

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

第一个launch只是不允许线程执行runBlocking之后的指令,而是允许继续执行此launch块之后的指令-这就是{{ 1}}的打印要早于Task from coroutine scope

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

答案 4 :(得分:0)

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

答案 5 :(得分:0)

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

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