简体   繁体   English

在 Kotlin 中创建子协程 scope

[英]Create a child coroutine scope in Kotlin

Short(-ish) story简短的(-ish)故事

I wonder if there is more or less standard way of creating a coroutine context/scope such that:我想知道是否有或多或少的标准方法来创建协程上下文/范围,例如:

  • It is a child of a current coroutine for structured concurrency,它是当前结构化并发协程的子进程,
  • It can be stored in some property, etc. and later be used for running asynchronous tasks with eg launch() .它可以存储在一些属性等中,然后用于运行异步任务,例如launch()

coroutineScope() does exactly what I need, it creates a child scope, but it does not simply return it to the caller - we need to pass a lambda and coroutine lifetime is limited to the execution of this lambda. coroutineScope()完全符合我的需要,它创建了一个子 scope,但它并不简单地将其返回给调用者 - 我们需要传递一个 lambda 并且协程的生命周期仅限于执行此 Z945F3FC4492386844F5。 On the other hand, CoroutineScope() factory creates a long-running scope that I can store for a later use, but it is unrelated to the current coroutine.另一方面, CoroutineScope()工厂创建了一个长时间运行的 scope,我可以存储它以供以后使用,但它与当前的协程无关。

I was able to create such a scope manually with:我能够通过以下方式手动创建这样的 scope:

suspend fun createChildCoroutineScope(): CoroutineScope {
    val ctx = coroutineContext
    return CoroutineScope(ctx + Job(ctx.job))
}

On first sight it seems to do exactly what I need.乍一看,它似乎完全符合我的需要。 Is it an equivalent of what coroutineScope() does or maybe my solution is somehow incomplete and I should perform some additional tasks?它是否相当于coroutineScope()所做的,或者我的解决方案可能不完整,我应该执行一些额外的任务? I tried to read the source code of coroutineScope() , but it is quite complicated.我试图阅读coroutineScope()的源代码,但它相当复杂。 Is there a simpler or more standard way of just creating a child scope?是否有更简单或更标准的方法来创建子 scope?

Also, is it considered a bad practice or anti-pattern?此外,它是否被认为是一种不好的做法或反模式? I'm just concerned that if there is no such a simple function already, then maybe it is so for a reason and I shouldn't really use coroutines this way.我只是担心,如果已经没有这么简单的 function,那么可能是有原因的,我不应该真的以这种方式使用协程。

Use cases (longer story)用例(更长的故事)

Usually, I see a need for this when I'm implementing some kind of a long-running service that could asynchronously schedule background operations:通常,当我实现某种可以异步调度后台操作的长时间运行的服务时,我会发现需要这样做:

class MyService {
    fun scheduleSomeTask() {
        // start task in the background
        // return immediately
    }
}

There are several possibilities to do this with coroutines:使用协程有几种可能性:

  1. GlobalScope , but it is bad. GlobalScope ,但它很糟糕。

  2. Make scheduleSomeTask() suspendable and run background tasks using current coroutine.使scheduleSomeTask()可暂停并使用当前协程运行后台任务。 In many cases I think this approach isn't really a proper one:在许多情况下,我认为这种方法并不是真正合适的方法:

    • Background tasks are "owned" by the caller and not by the service itself.后台任务由调用者“拥有”,而不是服务本身。 If we eg stop the service, the background task will be still running.如果我们停止服务,后台任务将仍在运行。
    • It requires that the scheduling function is suspendable.它要求调度 function 是可挂起的。 I think this is wrong, because I don't really see a reason why some Java code or a code outside of coroutines context should not be allowed to schedule tasks in my service.我认为这是错误的,因为我真的不明白为什么不允许某些 Java 代码或协程上下文之外的代码在我的服务中安排任务的原因。
  3. Give my service a defined lifecycle, create scope with CoroutineScope() and cancel() it when stopping/destroying.给我的服务一个定义的生命周期,使用CoroutineScope()创建 scope 并在停止/销毁时cancel()它。 This is fine, but I think we could still benefit from coroutines's structured concurrency, so for me it is a disadvantage that my service is detached.这很好,但我认为我们仍然可以从协程的结构化并发中受益,所以对我来说,我的服务是分离的是一个缺点。

    For example, we have a file downloading service and it consists of (owns) other services, including a data caching service.例如,我们有一个文件下载服务,它由(拥有)其他服务组成,包括数据缓存服务。 With typical approach of start() / stop() services we need to control lifecycle manually and it is hard to properly handle failures.使用start() / stop()服务的典型方法,我们需要手动控制生命周期,并且很难正确处理故障。 Coroutines make it much easier: if caching service crashes, it automatically propagates to downloading service;协程让它变得更容易:如果缓存服务崩溃,它会自动传播到下载服务; and if downloading service needs to stop, it just cancels its coroutine and it can be sure it doesn't leak any of its subcomponents.如果下载服务需要停止,它只是取消它的协程,它可以确保它不会泄漏它的任何子组件。 So for me structured concurrency of coroutines could be really useful when designing an application consisting of several small services.所以对我来说,在设计一个由几个小服务组成的应用程序时,协程的结构化并发可能非常有用。

My current approach is something like:我目前的方法是这样的:

class MyService {
    private lateinit var coroutine : CoroutineScope

    suspend fun start() {
        coroutine = createChildCoroutineScope() + CoroutineName("MyService")
    }

    fun stop() {
        coroutine.cancel()
    }

    fun scheduleSomeTask() {
        coroutine.launch {
            // do something
        }
    }
}

Or, alternatively:或者,或者:

class MyService(
    private val coroutine: CoroutineScope
) {
    companion object {
        suspend fun start() = MyService(createChildCoroutineScope())
    }
}

This way the service "intercepts" the coroutine that started it and attaches its background operations to it.这样,服务“拦截”启动它的协程并将其后台操作附加到它。 But as I said, I'm not sure if this isn't considered an anti-pattern for some reason.但正如我所说,我不确定这是否由于某种原因不被视为反模式。

Also, I understand my createChildCoroutineScope() is potentially dangerous.另外,我了解我的createChildCoroutineScope()具有潜在危险。 By calling it we make current coroutine uncompletable.通过调用它,我们使当前的协程无法完成。 This might be the reason why such a function does not exist in the library.这可能是库中不存在此类 function 的原因。 On the other hand, it is not really different than doing something like:另一方面,它与执行以下操作并没有什么不同:

launch {
    while (true) {
        socket.accept() // assume it is suspendable, not blocking
        // launch connection handler
    }
} 

In fact, both approaches are very similar from technical point of view.事实上,从技术角度来看,这两种方法都非常相似。 They have similar concurrency structure, but I believe "my" approach is often cleaner and more powerful.它们具有相似的并发结构,但我相信“我的”方法通常更干净、更强大。

I found a really great answer and explanation to my question.我为我的问题找到了一个非常好的答案和解释。 Roman Elizarov discussed exactly my problem in one of his articles: https://elizarov.medium.com/coroutine-context-and-scope-c8b255d59055 Roman Elizarov 在他的一篇文章中准确地讨论了我的问题: https://elizarov.medium.com/coroutine-context-and-scope-c8b255d59055

He explained that while it is technically possible to "capture" a current context of a suspending function and use it to launch background coroutines, it is highly discouraged to do this:他解释说,虽然技术上可以“捕获”暂停 function 的当前上下文并使用它来启动后台协程,但强烈建议不要这样做:

Do not do this, It makes the scope in which the coroutine is launched opaque and implicit, capturing some outer Job to launch a new coroutine without explicitly announcing it in the function signature.不要这样做,它会使启动协程的 scope 变得不透明和隐式,捕获一些外部Job以启动新的协程,而没有在 function 签名中明确宣布它。 A coroutine is a piece of work that is concurrent with the rest of your code and its launch has to be explicit.协程是与代码的 rest 并发的一项工作,它的启动必须是明确的。

If you need to launch a coroutine that keeps running after your function returns, then make your function an extension of CoroutineScope or pass scope: CoroutineScope as parameter to make your intent clear in your function signature. If you need to launch a coroutine that keeps running after your function returns, then make your function an extension of CoroutineScope or pass scope: CoroutineScope as parameter to make your intent clear in your function signature. Do not make these functions suspending.不要让这些功能暂停。

I knew I could just pass a CoroutineScope / CoroutineContext to a function, but I thought suspending function would be a shorter and more elegant approach.我知道我可以将CoroutineScope / CoroutineContext传递给 function,但我认为暂停 function 会是一种更短、更优雅的方法。 However, above explanation makes hell a lot of sense.然而,上面的解释让地狱很有意义。 If our function needs to acquire a coroutine scope/context of the caller, make it explicit about this - it can't be simpler.如果我们的 function 需要获取调用者的协程范围/上下文,请明确说明 - 它再简单不过了。

This is also related to the concept of "hot"/"cold" execution.这也与“热”/“冷”执行的概念有关。 One great thing about suspending functions is that they allow us to easily create "cold" implementations of long-running tasks.挂起函数的一大优点是它们允许我们轻松创建长时间运行任务的“冷”实现。 While I believe it is not explicitly specified in the coroutines docs that suspend functions should be "cold", it is generally a good idea to meet this requirement as callers of our suspending function could assume it is "cold".虽然我相信协程文档中没有明确规定挂起函数应该是“冷的”,但满足这个要求通常是一个好主意,因为我们挂起的 function 的调用者可能会认为它是“冷的”。 Capturing the coroutine context makes our function "hot", so the caller should be notified about this.捕获协程上下文会使我们的 function “热”,因此应该通知调用者。

IMHO it's perfectly fine to use your solution if you're bridging coroutines with some external lifecycle management , eg Spring's bean disposal methods.恕我直言,如果您将协程与一些外部生命周期管理(例如 Spring 的 bean 处理方法)连接起来,那么使用您的解决方案是非常好的。 Or even better create a scope factory:或者甚至更好地创建一个 scope 工厂:

fun Any.childScopeOf(
    parent: ApplicationScope,
    context: CoroutineContext = EmptyCoroutineContext
) = parent.childScope(this, context) { Job(it) }

fun Any.supervisorChildScopeOf(
    parent: ApplicationScope,
    context: CoroutineContext = EmptyCoroutineContext
) = parent.childScope(this, context) { SupervisorJob(it) }

@Component
class ApplicationScope {
    // supervisor to prevent failure in one subscope from failing everyting
    private val rootJob = SupervisorJob()

    internal fun childScope(
        ref: Any,
        context: CoroutineContext,
        job: (Job) -> Job
    ): CoroutineScope {
        val name = ref.javaClass.canonicalName
        log("Creating child scope '$name'")
        val job = job(rootJob)
        job.invokeOnCompletion { error ->
            when (error) {
                null, is CancellationException -> log("Child scope '$name' stopped")
                else -> logError(error, "Scope '$name' failed")
            }
        }
        return CoroutineScope(context + job + CoroutineName(name))
    }

    @PreDestroy
    internal fun stop() {
        rootJob.cancel()
        runBlocking {
            rootJob.join()
        }
    }
}

Usage example:使用示例:

@Component
MyComponent(appScope: ApplicationScope) {
    private val scope = childScopeOf(appScope) // no need to explicitly dispose this scope - parent ApplicationScope will take care of it
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM