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 秒, BA 完成之前就开始了。同时似乎没有产生任何额外的线程(一切都在 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() 仍会并发加载资源。