我正在使用 Kotlin 协程,结果我遇到了我不明白的情况。假设我有两个
suspend
函数:
suspend fun coroutine() {
var num = 0
coroutineScope {
for (i in 1..1000) {
launch {
delay(10)
num += 1
}
}
}
println("coroutine: $num")
}
和:
suspend fun runBlocked() = runBlocking {
var num = 0
for (i in 1..1000) {
launch {
delay(10)
num += 1
}
}
println("Run blocking: $num")
}
然后我通过
main()
方法调用它们:
suspend fun main() {
coroutine()
runBlocked()
}
coroutine()
方法打印(如预期)一个几乎不是 1000 的数字(通常在 970 到 999 之间)。我明白为什么。
我不明白的是为什么
runBlocked()
函数总是打印0。
协程:998
运行受阻:0
我又尝试了一次,这次做了一个与
runBlocked()
类似的函数,不同之处在于这次该方法返回一个值而不是打印:
suspend fun runBlockedWithReturn(): Int = runBlocking {
var num = 0
for (i in 1..1000) {
launch {
delay(10)
num += 1
}
}
return@runBlocking num
}
然后我从
main()
方法中调用它:
suspend fun main() {
val result = runBlockedWithReturn()
println("Run blocking with return: $result")
}
...但是该方法返回0。
这是为什么呢?如何修复
runBlocked()
方法以打印接近 1000 而不是 0 的数字?我错过了什么?
现有的答案集中在我们不应该做什么,但我认为他们没有抓住要点,所以我们看到差异的主要原因。
coroutineScope()
和runBlocking()
都保证退出代码块后所有协程已经完成运行。但出于某种原因,我不知道你是有意还是无意,你把这两种情况写得不同。在 coroutine()
示例中,您将 println()
放在 coroutineScope()
块下方,因此保证它会追随所有孩子。另一方面,在 runBlocked()
中,您将 println()
放入 runBlocking()
内,因此它会同时向子级运行。只需以与 runBlocked()
类似的方式重写您的 coroutine()
,因此将 println()
放在 runBlocking()
下面,您就会看到 1000
,如您所料。
两个示例之间的另一个区别是,默认情况下
runBlocking()
使用单个线程运行,而 coroutineScope()
可以使用多个线程。因此,coroutineScope()
会产生一个随机值,这是不安全共享可变状态的结果。 runBlocking()
更可预测。如果 0
在其中,它总是会生成 println()
,因为 println()
在任何子级之前运行。或者,如果 1000
低于 println()
,它总是会生成 runBlocking()
,因为孩子们实际上一次运行一个,他们不会并行修改值。
runBlocking
。由于我们通过将其置于挂起函数中来违反此合同,因此我们对其行为方式的任何解释在不同平台或将来可能会有所不同。
除此之外,永远不应该在协程中调用阻塞代码,除非您处于使用可以处理它的调度程序的 CoroutineContext 中,例如
Dispatchers.IO
。
也就是说,发生这种情况的原因是
coroutineScope
会暂停,直到其所有子协程完成,然后您在它返回后进行记录。 runBlocking
的行为类似,但您无需等待即可从块内部进行记录。
如果您想在记录之前等待所有协程启动,则需要
join()
每个协程。您可以将它们放入列表中并对其调用 joinAll()
。例如:
(1..1000).map {
launch {
delay(10)
num += 1
}
}.joinAll()
但再次强调,永远不应该在协程中调用
runBlocking
。我只是描述如果您从协程外部使用 runBlocking
来实现其预期目的,在非协程和协程代码之间建立桥梁,您将如何做到这一点。
您不应该永远从
runBlocking
内部致电suspend fun
。
无论如何,在从
runBlocking
返回值之前,您不会等待启动的协程完成,但使用 coroutineScope
会强制该函数中启动的协程在返回之前完成。