为什么不使用GlobalScope.launch?

52

我看到使用 Globalscope 被强烈不推荐,可以在这里查看

我的需求很简单。对于每个 Kafka 消息(假设是 ID 列表),我需要将其拆分并同时为每个 ID 调用一个 REST 服务,并等待它完成后继续其他同步任务。应用程序中没有其他需要协程的地方。在这种情况下,我能否只使用 Globalscope ?

注意:这不是 Android 应用程序。它是在服务器端运行的 Kafka 流处理器。它是一个短暂的、无状态的、容器化的(Docker)应用程序,在 Kubernetes 中运行(如果您愿意,也可以称为流行词)。

4个回答

31

使用结构化并发适当地限定您的并发范围,这样可以避免协程泄漏。如果不这样做,您的协程可能会泄漏。在您的情况下,将它们限定在处理单个消息上似乎是合适的。

以下是一个示例:

/* I don't know Kafka, but let's pretend this function gets 
 * called when you receive a new message
 */
suspend fun onMessage(msg: Message) {
    val ids: List<Int> = msg.getIds()    

    val jobs = ids.map { id ->
        GlobalScope.launch { restService.post(id) }
    }

    jobs.joinAll()
}

如果restService.post(id)中的一次调用出现异常,示例将立即重新抛出异常,并且尚未完成的所有作业都将泄漏。它们将继续执行(可能无限期地),如果它们失败,您将不会知道它们失败了。
为解决此问题,您需要对协程进行范围限定。以下是没有泄漏的相同示例:
suspend fun onMessage(msg: Message) = coroutineScope {
    val ids: List<Int> = msg.getIds()    

    ids.forEach { id ->
        // launch is called on "this", which is the coroutineScope.
        launch { restService.post(id) }
    }
}

在这种情况下,如果其中一个对restService.post(id)的调用失败,则协程范围内所有其他未完成的协程都将被取消。当您离开该范围时,可以确保您没有泄漏任何协程。
此外,由于coroutineScope将等待所有子协程完成,因此您可以省略jobs.joinAll()调用。
附注: 编写启动一些协程的函数时的常规做法是让调用者使用接收器参数来决定协程范围。对于onMessage函数,可以像这样进行操作:
fun CoroutineScope.onMessage(msg: Message): List<Job> {
    val ids: List<Int> = msg.getIds()    

    return ids.map { id ->
        // launch is called on "this", which is the coroutineScope.
        launch { restService.post(id) }
    }
}

4
@so-random-dude发布了一篇关于同一主题的文章,Roman Elizarov也发表了一篇相关的文章:https://medium.com/@elizarov/the-reason-to-avoid-globalscope-835337445abc - marstran
1
@Killer 但是这样就无法知道是否有任何异常抛出了。这个的目的是让异常像正常情况下一样冒泡,以便您能够处理它。在GlobalScope中使用并忽略内部异常确实不是一个好的模式。 - marstran
5
我整天都在努力理解为什么GlobalScope不好,以及每个人所说的作用域并发是什么意思,最后我终于明白了:“……它们将继续执行(可能无限期),如果它们失败,你就不会知道它们已经失败了……”。谢谢。 - Tarmo
@marstran,谢谢你的提示,我已经做到了...也就是说,启动块是按顺序执行的。 - undefined
@IrinaS。好的,那可能是你正在使用的调度程序有问题。尝试使用Dispatchers.Default,例如。 - undefined
显示剩余4条评论

27

根据文档,不推荐在GlobalScope实例上使用async或launch,应用代码通常应该使用应用定义的CoroutineScope

如果我们查看GlobalScope的定义,我们会发现它被声明为对象

object GlobalScope : CoroutineScope { ... }

对象代表一个单例静态实例。在Kotlin/JVM中,当类加载到JVM中时,静态变量就会存在,并且当该类被卸载时,它也会随之消亡。第一次使用GlobalScope时,它将被加载到内存中,并一直保留在那里,直到发生以下情况之一:

  1. 该类被卸载
  2. JVM关闭
  3. 进程终止

因此,在您的服务器应用程序运行时,它将占用一些内存。即使您的服务器应用程序运行完毕但进程未被销毁,启动的协程仍可能在运行并消耗内存。

使用GlobalScope.asyncGlobalScope.launch从全局作用域开始一个新的协程将创建一个顶级"独立"协程。

提供协程结构的机制称为结构化并发。现在我们来看看结构化并发相对于全局范围有哪些优势:

  
      
  • 该作用域通常负责子协程及其生命周期与作用域的生命周期相关联。
  •   
  • 如果出现问题或用户改变主意并决定撤销操作,该作用域可以自动取消子协程。
  •   
  • 该作用域会自动等待所有子协程完成。因此,如果该作用域对应于一个协程,则父协程在其作用域中启动的所有协程完成之前都不会完成。
  •   

当使用GlobalScope.async时,没有结构将多个协程绑定到较小的作用域中。从全局作用域开始的协程都是独立的;它们的生命周期仅受整个应用程序的生命周期限制。可以存储对从全局范围启动的协程的引用,并等待其完成或明确取消它,但这不会像使用结构化方式那样自动发生。如果我们想要取消范围内的所有协程,则使用结构化并发只需要取消父协程,这会自动传播到所有子协程的取消操作。

如果您不需要将协程限定在特定的生命周期对象中,并且希望启动一个顶级独立协程,该协程正在整个应用程序生命周期上运行,并且不会过早取消,也不想使用结构化并发的优势,则可以使用全局范围


3
亲爱的点踩者,请详细说明一下以启发我。 - so-random-dude
4
这个回答被踩可能是因为它与文档相矛盾,文档明确表示“在GlobalScope实例上使用asynclaunch是极不推荐的”。然而,对于那些确实希望协程生命周期等同于JVM生命周期的情况,反对使用GlobalScope的理由确实薄弱。 - Marko Topolnik

5
在您的链接中指出:

应用程序代码通常应使用应用程序定义的CoroutineScope,高度不建议在GlobalScope实例上使用asynclaunch

我的回答解决了这个问题。

一般来说,GlobalScope可能是一个不好的主意,因为它没有绑定到任何作业。你应该将其用于以下情况:

全局范围用于启动顶级协程,这些协程在整个应用程序生命周期中运行,并且不会过早取消。

这似乎不是您的用例。


有关更多信息,请参见官方文档中的结构化并发部分

There is still something to be desired for practical usage of coroutines. When we use GlobalScope.launch we create a top-level coroutine. Even though it is light-weight, it still consumes some memory resources while it runs. If we forget to keep a reference to the newly launched coroutine it still runs. What if the code in the coroutine hangs (for example, we erroneously delay for too long), what if we launched too many coroutines and ran out of memory? Having to manually keep a reference to all the launched coroutines and join them is error-prone.

There is a better solution. We can use structured concurrency in our code. Instead of launching coroutines in the GlobalScope, just like we usually do with threads (threads are always global), we can launch coroutines in the specific scope of the operation we are performing.

In our example, we have main function that is turned into a coroutine using runBlocking coroutine builder. Every coroutine builder, including runBlocking, adds an instance of CoroutineScope to the scope of its code block. We can launch coroutines in this scope without having to join them explicitly, because an outer coroutine (runBlocking in our example) does not complete until all the coroutines launched in its scope complete. Thus, we can make our example simpler:

import kotlinx.coroutines.*

fun main() = runBlocking { // this: CoroutineScope
    launch { // launch new coroutine in the scope of runBlocking   
        delay(1000L)   
        println("World!")    
    }   
    println("Hello,")  
}

因此,从本质上讲,这是不鼓励的,因为它强制你保留引用并使用join,而可以通过结构化并发来避免这一点。(请参见上面的代码示例。)该文章涵盖了许多微妙之处。


我希望有更多的文档可以突出 runBlocking - 这正是我所需要的,但作为一个新手 Android 开发者正在构建我的第一个应用程序,我以前从未见过这个。 - Scott Salyer

3
我们经常看到一些回答,说明为什么我们不应该使用全局作用域。
我将给你举几个例子,在这些情况下使用GlobalScope是可以的。
日志记录。
private fun startGlobalThread() {
    GlobalScope.launch {
        var count = 0
        while (true) {
            try {
                delay(100)
                println("Logging some Data")
            }catch (exception: Exception) {
                println("Global Exception")
            }
        }
    }
}

在数据库中保存数据 这是我们应用程序中的一个特殊情况,我们需要将数据存储在数据库中,然后以顺序方式将其更新到服务器。因此,当用户在表单中按下保存按钮时,我们不会等待数据库更新,而是使用GlobalScope进行更新。

/**
 * Don't use another coroutine inside GlobalScope
 * DB update may fail while updating
 */
private fun fireAndForgetDBUpdate() {
    GlobalScope.launch {
        val someProcessedData = ...
        db.update(someProcessedData)
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接