为什么不使用 GlobalScope.launch?

Why not use GlobalScope.launch?

我读到 Globalscope 的用法是非常不鼓励的,here

我有一个简单的用例。对于我收到的每条 kafka 消息(假设是一个 ID 列表),我必须将其拆分并同时为每个 ID 调用一个休息服务,并等待它完成并继续执行其他同步任务。该应用程序中没有其他任何东西需要协程。在这种情况下,我可以使用 Globalscope 吗?

注意:这不是 android 应用程序。它是服务器端的 kafka 流处理器 运行。它是 Kubernetes 中的一个短暂的、无状态的、容器化的 (Docker) 应用程序 运行(如果你愿意的话,符合流行语)

根据文档 GlobalScope 的实例上使用异步或启动是非常不鼓励的,应用程序代码通常应该使用应用程序定义的 CoroutineScope

如果我们查看 GlobalScope 的定义,我们会看到它被声明为 object:

object GlobalScope : CoroutineScope { ... }

一个对象表示一个单个静态实例(Singleton)。在 Kotlin/JVM 中,静态变量在 JVM 加载 class 时出现,并在 class 卸载时消失。当您第一次使用 GlobalScope 时,它会被加载到内存中并留在那里直到发生以下情况之一:

  1. 卸载class
  2. JVM 关闭
  3. 进程结束

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

使用 GlobalScope.asyncGlobalScope.launch 从全局范围启动新协程将创建顶级“independent”协程。

提供协程结构的机制称为结构化并发。让我们看看 结构化并发 全局范围 有什么好处:

  • The scope is generally responsible for child coroutines, and their lifetime is attached to the lifetime of the scope.
  • The scope can automatically cancel child coroutines if something goes wrong or if a user simply changes their mind and decides to revoke the operation.
  • The scope automatically waits for completion of all the child coroutines. Therefore, if the scope corresponds to a coroutine, then the parent coroutine does not complete until all the coroutines launched in its scope are complete.

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

如果您不需要将协程限定为特定的生命周期对象,并且您想要启动一个顶级独立协程,该协程在整个应用程序生命周期内运行并且不会过早取消并且您不希望要利用 结构化并发 的优势,然后继续使用 全局范围 .

在您的 link 中指出:

Application code usually should use application-defined CoroutineScope, using async or launch on the instance of GlobalScope is highly discouraged.

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

一般来说GlobalScope可能不是个好主意,因为它不绑定任何工作。您应该将其用于以下用途:

Global scope is used to launch top-level coroutines which are operating on the whole application lifetime and are not cancelled prematurely.

这似乎不是您的用例。


有关更多信息,请参阅官方文档中的一段 https://kotlinlang.org/docs/reference/coroutines/basics.html#structured-concurrency

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,这可以通过 structured concurrency. 避免(参见上面的代码示例。)这篇文章涵盖了许多微妙之处。

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

这是一个例子:

/* 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) }
    }
}

我们看到很多关于为什么我们不应该使用全局范围的答案。

我只是给你几个应该可以使用的案例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)
    }
}