在哪些方面,奔跑比悬浮更糟糕?
官方文档中很清楚地指出,Runblocking
“不应从Coroutine使用”。我大致了解了这个想法,但是我试图找到一个示例,其中使用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> delay> 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]
所以在a
完成之前,启动了b
仍然只花了1秒钟。同时,似乎没有任何其他线程(所有内容都在thread [main,5,main]
中)。 那么这是如何工作的呢?在async
中如何调用阻止功能使其在单个线程中“同时”执行?
It's quite clearly stated in official documentation that runBlocking
"should not be used from a coroutine". I roughly get the idea, but I'm trying to find an example where using runBlocking
instead of suspend functions negatively impacts performance.
So I created an example like this:
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")
}
This gives the expected output of:
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]
From my understanding getResource(A)
was started and the moment it arrived at delay
it gave the control back and then getResource(B)
was started. Then they both waited in a single thread and when the time passed, they both were again executed - everything in one second as expected.
So now I wanted to "break" it a little and replaced getResource
with:
private fun getResourceBlocking(name: String): String = runBlocking {
log("Starting getting ${name} for ${time}...")
delay(time)
log("Finished getting ${name}!")
"Resource ${name}"
}
and called it from the main
method in place of getResource
.
and then again I got:
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]
So it still took only 1 second to run and B
was started before A
finished. At the same time there doesn't seem to be any additional threads spawned (everything is in Thread[main,5,main]
).
So how does this work? How calling blocking functions in async
makes it execute "concurrently" in a single thread anyway?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
您的推理是正确的,但是您意外地遇到了使用
Runblocking()
的特殊情况,该案例已故意优化以不降低性能。如果您在另一个dispatcher runblocking()中使用dispatcher-lessrunblocking()
,则INNER runblocking()尝试重新使用事件循环由外部创建。因此,InnerRunblocking()
实际上是在暂停而不阻止的情况下工作类似(但这不是100%准确)。在实际情况下,外部Coroutine本身不会使用
RunBlocking()
或使用某些真实的调度员创建,您会看到降级性能。您可以使用此处替换外部代码:然后,正如您所期望的那样,资源被顺序加载。但是,即使有了此更改,
getResource()
仍然同时加载资源。Your reasoning is correct, but you accidentally hit a very special case of using
runBlocking()
, which was intentionally optimized to not degrade the performance. If you use dispatcher-lessrunBlocking()
inside another dispatcher-lessrunBlocking()
, then the innerrunBlocking()
tries to re-use the event loop created by the outer one. So innerrunBlocking()
actually works similarly as it is suspending and not blocking (but this is not 100% accurate).In a real case where the outer coroutine would not be itself created with
runBlocking()
or if you use some real dispatchers, you would see the degraded performance. You can replace the outer code with this:Then resources are loaded sequentially, as you probably expected. But even with this change,
getResource()
still loads resources concurrently.暂停的目标不是更快地完成工作。暂停的目标是在等待悬挂函数返回时不要阻止当前线程。
在您的示例中,当前线程是否被阻止都没关系,因为它在等待时没有做其他事情。
在带有UI的应用程序中,您通常会担心在UI线程(又称主线程)上阻塞,因为这会冻结应用程序。在UI线程被阻止时,没有什么可以动画,滚动或单击的。
如果您从主线程调用悬挂函数,则在等待悬挂函数返回时,主线程将不会被阻止。
您的示例中没有其他线程的原因是您使用的是
Runblocking
,该默认情况下在当前线程上运行,而无需在背景线程上进行工作。在实际应用程序中,您将从CoroutinesCope而不是Runblocking
启动Coroutines。The goal with suspending is not to finish the work faster. The goal with suspending is to not block the current thread while waiting for the suspend function to return.
In your example, it doesn't matter if the current thread is blocked or not because it's not doing anything else while waiting.
In an app with a UI, you are usually concerned about blocking on the UI thread (AKA main thread), because that will freeze the app. Nothing can be animated, scrolled, or clicked while the UI thread is blocked.
If you call a suspend function from the main thread, the main thread will not be blocked while it waits for the suspend function to return.
The reason no other threads are spawned in your example is that you are using
runBlocking
which runs on the current thread by default without doing its work on a background thread. In an actual application, you would be launching coroutines from a CoroutineScope rather than from arunBlocking
.