我正在阅读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看起来像是它的阻塞,因为它仅在完成时才到达最后一行。
有人可以启发我吗?
谢谢。
答案 0 :(得分:15)
我不明白coroutineScope和runBlocking在这里有何不同? coroutineScope看起来像是它的阻塞,因为它仅在完成时才到达最后一行。
从代码块的角度来看,您的理解是正确的。 runBlocking
和coroutineScope
之间的区别发生在一个较低的层次:协程被阻止时线程发生了什么?
runBlocking
不是suspend fun
。调用它的线程将保留在其中,直到协程完成为止。
coroutineScope
是suspend 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之后的所有非阻塞代码。
还应注意,在此代码示例中,队列中的每个启动都按照添加到队列中的顺序依次执行,并且每个启动仅在先前的启动执行之后执行。这可能看起来似乎所有启动都共享一个公共线程。这不是真的。他们每个人都有自己的线程。但是,如果启动中的任何代码都调用了暂停功能,则在执行暂停功能时,队列中的下一次启动将立即开始。老实说,这是非常奇怪的行为。为什么不只是异步运行队列中的所有启动?虽然我不知道此队列中发生的事情的内部情况,但我的猜测是队列中的每个启动都没有自己的线程,而是共享一个共同的线程。仅当遇到暂停功能时,才会为队列中的下一次启动创建新线程。可以通过这种方式来节省资源。
总而言之,执行是按以下顺序进行的:
答案 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。它实际上显示了 coroutineScope
和 runBlocking
的行为差异(即挂起和阻塞之间的差异)