runBlocking 在哪些方面比suspend 差?
In which aspects runBlocking is worse than suspend?
官方文档中明确指出runBlocking
“不应在协程中使用”。我大致明白了,但我试图找到一个示例,其中使用 runBlocking
而不是挂起函数会对性能产生负面影响。
所以我创建了一个这样的例子:
import java.time.Instant
import java.time.format.DateTimeFormatter
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlin.time.Duration.Companion.seconds
private val time = 1.seconds
private suspend fun getResource(name: String): String {
log("Starting getting ${name} for ${time}...")
delay(time)
log("Finished getting ${name}!")
return "Resource ${name}"
}
fun main(args: Array<String>) = runBlocking {
val resources = listOf("A", "B")
.map { async { getResource(it) } }
.awaitAll()
log(resources)
}
fun log(msg: Any) {
val now = DateTimeFormatter.ISO_INSTANT.format(Instant.now())
println("$now ${Thread.currentThread()}: $msg")
}
这给出了预期的输出:
2022-04-29T15:52:35.943156Z Thread[main,5,main]: Starting getting A for 1s...
2022-04-29T15:52:35.945570Z Thread[main,5,main]: Starting getting B for 1s...
2022-04-29T15:52:36.947539Z Thread[main,5,main]: Finished getting A!
2022-04-29T15:52:36.948334Z Thread[main,5,main]: Finished getting B!
2022-04-29T15:52:36.949233Z Thread[main,5,main]: [Resource A, Resource B]
据我了解,getResource(A)
已启动,当它到达 delay
时,它交还控制权,然后 getResource(B)
已启动。然后他们都在一个线程中等待,当时间过去时,他们都再次执行 - 一切都在一秒钟内完成。
所以现在我想稍微“打破”它并将 getResource
替换为:
private fun getResourceBlocking(name: String): String = runBlocking {
log("Starting getting ${name} for ${time}...")
delay(time)
log("Finished getting ${name}!")
"Resource ${name}"
}
并从 main
方法调用它代替 getResource
。
然后我又得到了:
2022-04-29T15:58:41.908015Z Thread[main,5,main]: Starting getting A for 1s...
2022-04-29T15:58:41.910532Z Thread[main,5,main]: Starting getting B for 1s...
2022-04-29T15:58:42.911661Z Thread[main,5,main]: Finished getting A!
2022-04-29T15:58:42.912126Z Thread[main,5,main]: Finished getting B!
2022-04-29T15:58:42.912876Z Thread[main,5,main]: [Resource A, Resource B]
所以 运行 仍然只用了 1 秒, B
在 A
完成之前就开始了。同时似乎没有产生任何额外的线程(一切都在 Thread[main,5,main]
中)。
那么这是如何工作的呢?在 async
中调用阻塞函数如何使其在单个线程中“同时”执行?
暂停的目的不是为了更快地完成工作。挂起的目标是在等待挂起函数到 return.
时不阻塞当前线程
在您的示例中,当前线程是否被阻塞并不重要,因为它在等待时没有做任何其他事情。
在具有 UI 的应用程序中,您通常担心阻塞 UI 线程(也称为主线程),因为这会冻结应用程序。 UI 线程被阻塞时,无法设置动画、滚动或单击任何内容。
如果从主线程调用挂起函数,主线程在等待挂起函数时不会被阻塞return。
在您的示例中没有生成其他线程的原因是您使用的 runBlocking
默认情况下在当前线程上运行,而不在后台线程上执行其工作。在实际应用程序中,您将从 CoroutineScope 而不是从 runBlocking
.
启动协程
您的推理是正确的,但您不小心遇到了使用 runBlocking()
的非常特殊的情况,这是有意优化的,不会降低性能。如果在另一个 dispatcher-less runBlocking()
中使用 dispatcher-less runBlocking()
,则内部 runBlocking()
会尝试 re-use 外部创建的事件循环。所以内部 runBlocking()
实际上工作方式类似,因为它是挂起而不是阻塞(但这不是 100% 准确)。
在实际情况下,外部协程本身不会使用 runBlocking()
创建,或者如果您使用一些真正的调度程序,您会看到性能下降。您可以用这个替换外部代码:
val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
fun main(args: Array<String>) = runBlocking(dispatcher) { ... }
然后,正如您可能预料的那样,资源将按顺序加载。但即使进行了此更改,getResource()
仍会并发加载资源。
官方文档中明确指出runBlocking
“不应在协程中使用”。我大致明白了,但我试图找到一个示例,其中使用 runBlocking
而不是挂起函数会对性能产生负面影响。
所以我创建了一个这样的例子:
import java.time.Instant
import java.time.format.DateTimeFormatter
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlin.time.Duration.Companion.seconds
private val time = 1.seconds
private suspend fun getResource(name: String): String {
log("Starting getting ${name} for ${time}...")
delay(time)
log("Finished getting ${name}!")
return "Resource ${name}"
}
fun main(args: Array<String>) = runBlocking {
val resources = listOf("A", "B")
.map { async { getResource(it) } }
.awaitAll()
log(resources)
}
fun log(msg: Any) {
val now = DateTimeFormatter.ISO_INSTANT.format(Instant.now())
println("$now ${Thread.currentThread()}: $msg")
}
这给出了预期的输出:
2022-04-29T15:52:35.943156Z Thread[main,5,main]: Starting getting A for 1s...
2022-04-29T15:52:35.945570Z Thread[main,5,main]: Starting getting B for 1s...
2022-04-29T15:52:36.947539Z Thread[main,5,main]: Finished getting A!
2022-04-29T15:52:36.948334Z Thread[main,5,main]: Finished getting B!
2022-04-29T15:52:36.949233Z Thread[main,5,main]: [Resource A, Resource B]
据我了解,getResource(A)
已启动,当它到达 delay
时,它交还控制权,然后 getResource(B)
已启动。然后他们都在一个线程中等待,当时间过去时,他们都再次执行 - 一切都在一秒钟内完成。
所以现在我想稍微“打破”它并将 getResource
替换为:
private fun getResourceBlocking(name: String): String = runBlocking {
log("Starting getting ${name} for ${time}...")
delay(time)
log("Finished getting ${name}!")
"Resource ${name}"
}
并从 main
方法调用它代替 getResource
。
然后我又得到了:
2022-04-29T15:58:41.908015Z Thread[main,5,main]: Starting getting A for 1s...
2022-04-29T15:58:41.910532Z Thread[main,5,main]: Starting getting B for 1s...
2022-04-29T15:58:42.911661Z Thread[main,5,main]: Finished getting A!
2022-04-29T15:58:42.912126Z Thread[main,5,main]: Finished getting B!
2022-04-29T15:58:42.912876Z Thread[main,5,main]: [Resource A, Resource B]
所以 运行 仍然只用了 1 秒, B
在 A
完成之前就开始了。同时似乎没有产生任何额外的线程(一切都在 Thread[main,5,main]
中)。
那么这是如何工作的呢?在 async
中调用阻塞函数如何使其在单个线程中“同时”执行?
暂停的目的不是为了更快地完成工作。挂起的目标是在等待挂起函数到 return.
时不阻塞当前线程在您的示例中,当前线程是否被阻塞并不重要,因为它在等待时没有做任何其他事情。
在具有 UI 的应用程序中,您通常担心阻塞 UI 线程(也称为主线程),因为这会冻结应用程序。 UI 线程被阻塞时,无法设置动画、滚动或单击任何内容。
如果从主线程调用挂起函数,主线程在等待挂起函数时不会被阻塞return。
在您的示例中没有生成其他线程的原因是您使用的 runBlocking
默认情况下在当前线程上运行,而不在后台线程上执行其工作。在实际应用程序中,您将从 CoroutineScope 而不是从 runBlocking
.
您的推理是正确的,但您不小心遇到了使用 runBlocking()
的非常特殊的情况,这是有意优化的,不会降低性能。如果在另一个 dispatcher-less runBlocking()
中使用 dispatcher-less runBlocking()
,则内部 runBlocking()
会尝试 re-use 外部创建的事件循环。所以内部 runBlocking()
实际上工作方式类似,因为它是挂起而不是阻塞(但这不是 100% 准确)。
在实际情况下,外部协程本身不会使用 runBlocking()
创建,或者如果您使用一些真正的调度程序,您会看到性能下降。您可以用这个替换外部代码:
val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
fun main(args: Array<String>) = runBlocking(dispatcher) { ... }
然后,正如您可能预料的那样,资源将按顺序加载。但即使进行了此更改,getResource()
仍会并发加载资源。