【问题标题】:Why not use GlobalScope.launch?为什么不使用 GlobalScope.launch?
【发布时间】:2019-06-17 12:32:49
【问题描述】:

我了解到Globalscope 的使用是非常不鼓励的,here

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

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

【问题讨论】:

    标签: kotlin kotlin-coroutines jvm-languages


    【解决方案1】:

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

    这是一个例子:

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

    【讨论】:

    • @so-random-dude Roman Elizarov 刚刚发布了一篇关于同一主题的文章:medium.com/@elizarov/…
    • 如果你在 GlobalScope.launch { tryIgnore { block() } } 中尝试捕获会怎样!
    • @Killer 但是不知道是否有任何异常。这样做的目的是让异常像往常一样冒泡,以便您能够处理它。使用GlobalScope 并忽略内部异常确实不是一个好的模式。
    • 是的。将转向结构并发以取代 GlobalLauch。
    • 花了一整天的时间试图弄清楚为什么 GlobalScope 不好以及每个人都意味着我的作用域并发,这最终击中了我:“......他们将继续执行(可能无限期地),并且如果他们失败了,你就不会知道……”。谢谢。
    【解决方案2】:

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

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

    object GlobalScope : CoroutineScope { ... }
    

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

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

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

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

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

    • 作用域一般负责子协程,它们的生命周期依附于作用域的生命周期。
    • 如果出现问题或用户只是改变主意并决定 撤销操作。
    • 作用域自动等待所有子协程完成。因此,如果作用域对应一个协程,那么 父协程直到所有协程都没有完成 在其范围内推出的都是完整的。

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

    如果您不需要将协程限定为特定生命周期对象,并且您想启动一个在整个应用程序生命周期上运行且不会提前取消的顶级独立协程,并且您不想使用结构化并发的好处,然后继续使用全局范围

    【讨论】:

    • 亲爱的投票者,请详细说明一下以启发我
    • 反对意见可能是关于这个与文档相矛盾的答案,该文档明确指出“非常不鼓励在 GlobalScope 的实例上使用 asynclaunch。”但是,对于您真正希望协程生命周期等于 JVM 生命周期的情况,针对 GlobalScope 的推理确实很薄。
    【解决方案3】:

    在您的link 中声明:

    应用程序代码通常应该使用应用程序定义的 CoroutineScope,在 GlobalScope 的实例上使用 asynclaunch 非常不鼓励。

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

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

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

    这似乎不是您的用例。


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

    在实际使用中仍有一些不足之处 协程。当我们使用GlobalScope.launch 时,我们创建了一个顶层 协程。虽然很轻,但还是会消耗一些 运行时的内存资源。如果我们忘记保留参考 新启动的协程它仍在运行。如果代码中的 协程挂起(例如我们错误地延迟了太久),什么 如果我们启动了太多协程并耗尽了内存?不得不 手动保留对所有已启动协程的引用并加入它们 容易出错。

    有一个更好的解决方案。我们可以在我们的 代码。而不是在GlobalScope 中启动协程,就像我们一样 通常用线程做(线程总是全局的),我们可以启动 我们正在执行的操作的特定范围内的协程。

    在我们的示例中,我们将 main 函数转换为协程 使用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. 可以避免这种情况(参见上面的代码示例。)本文涵盖了许多细节。

    【讨论】:

      【解决方案4】:

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

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

      【讨论】:

        猜你喜欢
        • 2019-08-28
        • 1970-01-01
        • 1970-01-01
        • 2019-07-17
        • 2014-08-26
        • 1970-01-01
        • 2011-12-21
        • 2018-10-20
        • 2018-11-06
        相关资源
        最近更新 更多