为什么不使用GlobalScope.launch?

问题描述 投票:5回答:3

我读到Globalscope的用法非常气馁,here

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

注意:这不是Android应用程序。它只是在服务器端运行的kafka流处理器。这是一个在Kubernetes中运行的短暂的,无状态的,容器化的(Docker)应用程序(如果你愿意,可以使用Buzzword)

kotlin kotlinx.coroutines jvm-languages
3个回答
4
投票

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

这是一个例子:

/* 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()调用。

旁注:编写启动某些协程的函数时的一个约定是让调用者使用receiver参数决定协程范围。使用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
投票

在你的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.避免(参见上面的代码示例。)本文涵盖了许多细微之处。


2
投票

通过使用async或在GlobalScope实例上启动的文档非常不鼓励,应用程序代码通常应该使用应用程序定义的CoroutineScope

如果我们看一下GlobalScope的定义,我们会看到它被声明为对象:

object GlobalScope : CoroutineScope { ... }

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

  1. 该类已卸载
  2. JVM关闭了
  3. 这个过程就死了

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

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

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

  • 范围通常负责子协同程序,它们的生命周期与范围的生命周期相关。
  • 如果出现问题或者用户只是改变主意并决定撤销操作,范围可以自动取消子协同程序。
  • 范围自动等待所有子协同程序的完成。因此,如果范围对应于协同程序,则在其范围内启动的所有协同程序完成之前,父协同程序不会完成。

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

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

© www.soinside.com 2019 - 2024. All rights reserved.