简体   繁体   中英

Create a child coroutine scope in Kotlin

Short(-ish) story

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() .

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. 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.

I was able to create such a scope manually with:

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? I tried to read the source code of coroutineScope() , but it is quite complicated. Is there a simpler or more standard way of just creating a child 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.

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.

  2. Make scheduleSomeTask() suspendable and run background tasks using current coroutine. 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. 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.
  3. Give my service a defined lifecycle, create scope with CoroutineScope() and cancel() it when stopping/destroying. 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. 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. By calling it we make current coroutine uncompletable. This might be the reason why such a function does not exist in the library. 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

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:

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. A coroutine is a piece of work that is concurrent with the rest of your code and its launch has to be explicit.

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. 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.

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". Capturing the coroutine context makes our function "hot", so the caller should be notified about this.

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. Or even better create a scope factory:

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
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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