Android Kotlin 协程冻结
Android Kotlin Coroutines Freeze
我遇到了一个有趣的协程冻结,我将其简化为以下问题:
//running on main thread
runBlocking {
lifecycleScope.launch {
delay(1000)
}.join()
}
这会导致主线程无限期冻结。我认为这是因为以下事件序列:
- 等待启动
- 调用加入,将主线程传给协程池
- 调用启动
- 调用延迟,将主线程传给协程池
- 线程返回加入并等待
- 延迟永远不会结束,因为它没有可用的线程?
如果我误解了上述逻辑,请纠正我。避免这种情况发生的合理模式是什么?我知道 运行 在主线程上阻塞不是一个好主意,但在代码的更深处,您可能会以这种方式不小心冻结单线程协程,这似乎很奇怪。
因为在主线程上调用了runBlocking
(这违背了使用协程的想法);当 top-most 指令已经停止线程时,事件的顺序可能无关紧要。它总是 GlobalScope
vs. CoroutineScope
vs. lifecycleScope
...其中 lifecycleScope.launch
可以与不同的调度程序一起使用:
lifecycleScope.launch(Dispatchers.IO)
:在 AndroidX 提供的 lifecycleScope
中启动协程。 一旦生命周期失效,协同程序就会被取消(即用户离开片段)。使用 Dispatchers.IO
作为线程池。
lifecycleScope.launch
:同上,但不指定则使用Dispatchers.Main
。
因此我假设,该行为也可能源于 Dispatchers.Main
.
的调度
它比你想象的还要简单。由于 runBlocking()
、join()
没有 return 事件循环的线程,因此 launch()
块永远不会开始执行 - 死锁。
实际上...这并不完全正确。 join()
returns 线程到池,而不是我们想的那个。 runBlocking()
使用调用者线程启动自己的事件循环。从 runBlocking()
的外部看,线程似乎一直处于阻塞状态,但在内部它会循环并可以挂起。无论如何,从lifecycleScope
的角度来看,主线程被阻塞,无法在其上启动任何东西。
What is a reasonable pattern to avoid this from happening?
不要在主线程上调用 runBlocking()
。协程在这里也不例外。我们不应该 运行 阻塞主线程上的 IO 或其他类型的阻塞操作,包括 runBlocking()
.
这里解释了导致死锁的确切原因。
在主线程上 运行 您的应用程序中任何地方的任何代码实际上都是 运行 来自已发送到 Main Looper 的消息队列以在主线程上处理的消息。
Dispatchers.Main
的工作方式本质上是将协程片段作为 Runnable 消息发送到由 Main Looper
支持的 Android Handler
。发送到 Main Looper 的消息一次只能处理一个。
在您的 runBlocking
调用中,您的 join()
调用正在挂起,直到其关联的协程完成。该协程已提交给主 Looper。在当前消息 return 之前,Looper 无法处理其队列中的任何消息。当前消息是 运行 您从中调用 runBlocking
的主线程中的方法。
runBlocking
正在等待 join()
到 return。 join()
正在等待 Looper 处理其协程。 Looper 正在等待 runBlocking
到 return。
我看到你在评论中提到它适用于 GlobalScope。这是因为 GlobalScope 使用 Dispatchers.Default
而 lifecycleScope
使用 Dispatchers.Main
(除非您在启动协程时修改默认上下文)。
我遇到了一个有趣的协程冻结,我将其简化为以下问题:
//running on main thread
runBlocking {
lifecycleScope.launch {
delay(1000)
}.join()
}
这会导致主线程无限期冻结。我认为这是因为以下事件序列:
- 等待启动
- 调用加入,将主线程传给协程池
- 调用启动
- 调用延迟,将主线程传给协程池
- 线程返回加入并等待
- 延迟永远不会结束,因为它没有可用的线程?
如果我误解了上述逻辑,请纠正我。避免这种情况发生的合理模式是什么?我知道 运行 在主线程上阻塞不是一个好主意,但在代码的更深处,您可能会以这种方式不小心冻结单线程协程,这似乎很奇怪。
因为在主线程上调用了runBlocking
(这违背了使用协程的想法);当 top-most 指令已经停止线程时,事件的顺序可能无关紧要。它总是 GlobalScope
vs. CoroutineScope
vs. lifecycleScope
...其中 lifecycleScope.launch
可以与不同的调度程序一起使用:
lifecycleScope.launch(Dispatchers.IO)
:在 AndroidX 提供的lifecycleScope
中启动协程。 一旦生命周期失效,协同程序就会被取消(即用户离开片段)。使用Dispatchers.IO
作为线程池。lifecycleScope.launch
:同上,但不指定则使用Dispatchers.Main
。
因此我假设,该行为也可能源于 Dispatchers.Main
.
它比你想象的还要简单。由于 runBlocking()
、join()
没有 return 事件循环的线程,因此 launch()
块永远不会开始执行 - 死锁。
实际上...这并不完全正确。 join()
returns 线程到池,而不是我们想的那个。 runBlocking()
使用调用者线程启动自己的事件循环。从 runBlocking()
的外部看,线程似乎一直处于阻塞状态,但在内部它会循环并可以挂起。无论如何,从lifecycleScope
的角度来看,主线程被阻塞,无法在其上启动任何东西。
What is a reasonable pattern to avoid this from happening?
不要在主线程上调用 runBlocking()
。协程在这里也不例外。我们不应该 运行 阻塞主线程上的 IO 或其他类型的阻塞操作,包括 runBlocking()
.
这里解释了导致死锁的确切原因。
在主线程上 运行 您的应用程序中任何地方的任何代码实际上都是 运行 来自已发送到 Main Looper 的消息队列以在主线程上处理的消息。
Dispatchers.Main
的工作方式本质上是将协程片段作为 Runnable 消息发送到由 Main Looper
支持的 Android Handler
。发送到 Main Looper 的消息一次只能处理一个。
在您的 runBlocking
调用中,您的 join()
调用正在挂起,直到其关联的协程完成。该协程已提交给主 Looper。在当前消息 return 之前,Looper 无法处理其队列中的任何消息。当前消息是 运行 您从中调用 runBlocking
的主线程中的方法。
runBlocking
正在等待 join()
到 return。 join()
正在等待 Looper 处理其协程。 Looper 正在等待 runBlocking
到 return。
我看到你在评论中提到它适用于 GlobalScope。这是因为 GlobalScope 使用 Dispatchers.Default
而 lifecycleScope
使用 Dispatchers.Main
(除非您在启动协程时修改默认上下文)。