与launch 或async 不同,runBlocking 是一个非常特殊的协程构建器,因为它应该在顶层使用。因此它不会在 inside 范围内运行(如您所见,没有 CoroutineScope 作为接收者)。它应该是结构化并发的“根”。
runBlocking 实际上为您提供了一个范围,因此您可以在其中启动“子”协程。真正重要的是runBlocking 将等待您在该范围内启动的所有协程(子协程)完成后再返回。
取消 runBlocking?
如果 runBlocking 协程挂起,您不能真正从外部取消它本身(就像您通过取消 launch 或 async 的范围所做的那样),因为它不是异步任务 - 它正在阻塞线程。您可以中断运行它的线程,或者您也可以跟踪嵌套协程并显式取消 它们 以使 runBlocking 完成。
只有在所有子协程完成(或被取消)后,runBlocking 调用才会返回。抛出异常(从内部)也会取消所有子协程并使runBlocking 重新抛出该异常。所以这也是一种从内部“取消”runBlocking 的方式。
它是调用它的类的本地,并且当类被垃圾收集时协程被垃圾收集,还是如果它仍在运行会导致内存泄漏?
您可以将runBlocking 视为任何阻塞函数,例如Thread.sleep,没有更多的魔法。就像Thread.sleep 一样,runBlocking 可以从任何函数调用,甚至是顶级函数,在这种情况下不会涉及任何类实例。
让我们假设一个类中的方法调用runBlocking,而runBlocking 中的任何东西都会挂起很长时间(就像长时间的睡眠一样)。然后,调用此方法的任何人都持有对该实例的引用,直到该方法返回或失败,因此该实例无论如何都不会被垃圾回收。在这种情况下,调用者将挂起,阻塞它正在运行的任何线程——这就是可能发生泄漏的地方。
runBlocking 问题示例
弹簧控制器
如果您在 Spring MVC 控制器的方法中使用 runblocking,则 Spring 可能会为其中的每一个创建线程,如果有任何东西挂在 runBlocking 中,您可能会泄漏线程。相反,您可以使用 Spring WebFlux,它允许您直接在 Spring 控制器中使用 suspend 函数(并添加请求超时以自动取消挂起的东西)。
回调
在回调中使用runBlocking 也可能很危险。您正在使用的假设基于回调的 API 可能正在使用线程池来回调您,如果 runBlocking 挂起,它可能会阻塞该线程池。
如果该 API 支持背压,则可以阻止。 API 还可以以非阻塞方式支持这一点(例如 JDK11 的 websocket listener,它允许您从回调中返回 CompletableStage),在这种情况下,您应该构建 Future 而不是阻塞。
如果 API 不支持背压,您可能不得不求助于在其中创建自定义 CoroutineScope 和 launch-ing 协程来处理回调(而不是 runBlocking)。当您不再使用基于回调的 API 时,您必须手动取消该范围。