【问题标题】:Android Kotlin Coroutines FreezeAndroid Kotlin 协程冻结
【发布时间】:2022-02-17 05:37:13
【问题描述】:

我遇到了一个有趣的协程冻结,我将其简化为以下问题:

//running on main thread
runBlocking {
   lifecycleScope.launch {
      delay(1000)
   }.join()
}

这会导致主线程无限期地冻结。我认为这是因为以下一系列事件:

  1. 要启动的队列
  2. 调用加入,将主线程传递给协程池
  3. 调用启动
  4. 调用延迟,将主线程传递给协程池
  5. 线程移回加入并等待
  6. 延迟永远不会结束,因为它没有可用的线程?

如果我对上述逻辑有误解,请纠正我。避免这种情况发生的合理模式是什么?我知道在主线程上运行阻塞不是一个好主意,但是在代码的更深层次上,您会以这种方式意外冻结单线程协程似乎很奇怪。

【问题讨论】:

  • 使用runBlocking 会意外死锁线程并不奇怪。奇怪的是使用runBlocking。除了 JVM 应用程序的 main() 函数之外,它的用例非常狭窄。
  • @Tenfour04 这似乎是一个合理的答案,但对我来说仍然很奇怪,这最终会在某个地方保留主线程
  • 为什么你觉得奇怪?甚至runBlocking() 函数的名称都表示它阻塞了线程。

标签: android kotlin kotlin-coroutines


【解决方案1】:

下面是关于究竟是什么导致死锁的解释。

应用中任何在主线程上运行的代码实际上都是从已发送到 Main Looper 的消息队列以在主线程上处理的消息中运行的。

Dispatchers.Main 的工作方式本质上是将协程片段作为 Runnable 消息发送到由 Main Looper 支持的 Android Handler。一次只能处理一个发送到主循环器的消息。

在您的 runBlocking 调用中,您的 join() 调用将暂停,直到其关联的协程完成。该协程已提交给主 Looper。在当前消息返回之前,Looper 无法处理其队列中的任何消息。当前消息是在您调用 runBlocking 的主线程上运行该方法的那个。

runBlocking 正在等待join() 返回。 join() 正在等待它的协程被 Looper 处理。 Looper 正在等待runBlocking 返回。

我看到您在评论中提到它适用于 GlobalScope。这是因为 GlobalScope 使用Dispatchers.DefaultlifecycleScope 使用Dispatchers.Main(除非您在启动协程时修改了默认上下文)。

【讨论】:

  • 是的,这也是我的理解。我只是担心,因为我的回答下方的 OP 说他们观察到 delay() 被调用。我认为这不应该发生。
  • @broot,从技术上讲,lifecycleScope 使用Dispatchers.Main.immediate,所以它可能在实际提交给循环器之前执行代码直到第一个挂起函数调用,launch 返回一个@ 987654338@.
  • 啊,有道理!
  • 我觉得这是最简洁的解释,因为它深入到实现细节。
【解决方案2】:

它比你想象的还要简单。由于runBlocking()join() 不会将线程返回到事件循环,因此launch() 块永远不会开始执行 - 死锁。

实际上……这并不完全正确。 join() 将线程返回到池中,但不是我们考虑的那个。 runBlocking() 使用调用者线程启动它自己的事件循环。从runBlocking() 的外部看,线程似乎一直被阻塞,但在内部它循环并可以挂起。不管怎样,从lifecycleScope的角度来看,主线程被阻塞了,它不能在上面启动任何东西。

避免这种情况发生的合理模式是什么?

不要在主线程上调用runBlocking()。协程在这里也不例外。我们不应该在主线程上运行阻塞 IO 或其他类型的阻塞操作,包括runBlocking()

【讨论】:

  • 我想我仍然想知道它在哪里阻塞以及为什么?启动确实开始了,我们可以记录并看到delay 已启动但从未完成。
  • 这是因为生命周期永远不会失效。做奇怪的事情会给你带来奇怪的结果。
  • @MartinZeitler 啊,当我用 GlobalScope 替换生命周期范围时很好,这更符合我的期望。我仍然对什么机制导致生命周期范围像这样冻结感兴趣?
  • @HaydenKai 嗯,你确定delay()之前的日志吗?我尝试使用单线程调度程序模拟相同的情况,它甚至没有进入launch()(代码:pl.kotl.in/DnhbHYPSb)。我可能在这里错过了一些东西。
【解决方案3】:

这是因为在主线程上调用了runBlocking(这违背了甚至使用协程的想法);当最上面的指令已经停止线程时,事件的顺序可能无关紧要。总是GlobalScope vs. CoroutineScope vs. lifecycleScope ...其中lifecycleScope.launch可以与不同的调度程序一起使用:

  • lifecycleScope.launch(Dispatchers.IO):在 AndroidX 提供的lifecycleScope 内启动协程。 一旦生命周期失效,协程就会被取消(即用户离开片段)。使用Dispatchers.IO 作为线程池。

  • lifecycleScope.launch:与​​上述相同,但如果未指定,则使用Dispatchers.Main

因此我假设,该行为也可能源于使用Dispatchers.Main 进行调度。

【讨论】:

    猜你喜欢
    • 2021-10-18
    • 1970-01-01
    • 2020-11-04
    • 1970-01-01
    • 1970-01-01
    • 2021-08-13
    • 2020-10-07
    • 2021-05-13
    • 1970-01-01
    相关资源
    最近更新 更多