为什么不使用GlobalScope.launch?

时间:2019-01-23 20:45:48

标签: kotlin kotlinx.coroutines jvm-languages

我听说强烈建议不要使用Globalscopehere

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

注意:这不是android应用程序。它只是在服务器端运行的kafka流处理器。这是一个在Kubernetes中运行的临时,无状态,容器化(Docker)应用程序(如果愿意,则符合Buzzword)

4 个答案:

答案 0 :(得分:2)

在您的link中指出:

  

应用程序代码通常应使用应用程序定义的   CoroutineScope,在async的实例上使用launchGlobalScope   强烈劝阻。

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

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

  

全局范围用于启动顶级协程,它们是   在整个应用程序生命周期内运行,并且不会被取消   过早地

这似乎不是您的用例。


有关更多信息,请参见https://kotlinlang.org/docs/reference/coroutines/basics.html#structured-concurrency

中的官方文档。
  

实际使用中还需要一些东西   协程。当我们使用GlobalScope.launch时,我们将创建一个顶级   协程。即使重量很轻,它仍然消耗一些   运行时的内存资源。如果我们忘记引用   新发布的协程仍可运行。如果代码在   协程挂起(例如,我们错误地延迟了太长时间),   如果我们发布了太多的协程并用完了内存?不得不   手动保留对所有已启动协程的引用并加入它们   容易出错。

     

有一个更好的解决方案。我们可以在我们的结构中使用结构化并发   码。就像我们一样,不要在GlobalScope中启动协程   通常使用线程(线程始终是全局的),我们可以启动   协程在我们正在执行的操作的特定范围内。

     

在我们的示例中,我们将主要功能变成了协程   使用runBlocking协程生成器。每个协程制造商   包括runBlocking,将CoroutineScope的实例添加到范围   其代码块。我们可以在此范围内启动协程   必须显式地加入它们,因为外部协程   (在我们的示例中,runBlocking)直到所有   协程在其范围内推出完成。因此,我们可以使我们的   示例更简单:

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.可以避免这种情况(请参见上面的代码示例。)本文涵盖了许多微妙之处。

答案 1 :(得分:2)

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

这是一个例子:

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

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

    jobs.joinAll()
}

如果对restService.post(id)的调用之一失败并出现异常,该示例将立即重新引发该异常,所有尚未完成的作业都将泄漏。它们将继续执行(可能无限期地执行),如果失败,您将一无所知。

要解决此问题,您需要确定协程的范围。这是没有泄漏的相同示例:

suspend fun onMessage(Message msg) = 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(Message msg): 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) }
    }
}

答案 2 :(得分:1)

强烈建议不要通过文档GlobalScope实例上使用异步或启动文档,应用程序代码通常应使用应用程序定义的CoroutineScope

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

object GlobalScope : CoroutineScope { ... }

对象表示一个单个静态实例(Singleton)。在 Kotlin / JVM 中,当JVM加载一个类时,将存在一个静态变量,而在卸载该类时,该变量将死亡。首次使用GlobalScope时,它将被加载到内存中并停留在内存中,直到发生以下情况之一:

  1. 该课程已卸载
  2. JVM关闭
  3. 过程死了

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

如果您不需要将协程的作用域限定于特定的生命周期对象,并且想要启动一个在整个应用程序生命周期内都运行且不会过早取消的顶级协程,那么请继续。

答案 3 :(得分:0)

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

我只是给你一些应该可以使用 GlobalScope

日志记录

private fun startGlobalThread() {
    GlobalScope.launch {
        var count = 0
        while (true) {
            try {
                delay(100)
                println("Logging some Data")
            }catch (exception: Exception) {
                println("Global Exception")
            }
        }
    }
}

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

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