Kotlinx.coroutines: When creating a new job, it should be easier (default) to create child instead of a new root job

Created on 28 Aug 2019  Â·  10Comments  Â·  Source: Kotlin/kotlinx.coroutines

When creating a new Job, the simplest way to do this, and the way most people do it by default is with Job() or SupervisorJob(). However, in most cases what is actually intended is to create a job that is a child of some context or scope. The current state of things makes it really easy to accidentally create a job that breaks out of the hierarchy, which can lead to leaks of coroutines and exceptions, and broken cancellation.

A simple extension function could improve things:

fun CoroutineScope.createJob(): Job = Job(parent = coroutineContext[Job])
fun CoroutineScope.createSupervisorJob(): SupervisorJob = SupervisorJob(parent = coroutineContext[Job])
enhancement

All 10 comments

I usually create child scopes, an exception could look like this:

fun CoroutineScope.childScope(): CoroutineScope = plus(Job(coroutineContext[Job]))
fun CoroutineScope.childSupervisorScope(): CoroutineScope = plus(SupervisorJob(coroutineContext[Job]))

It would be nice if contexts had a change to perform custom merge operations. That way a Job would attach itself to the parent by default when merging

launch returns a Job, which appears to be initialized to have a parent via coroutineContext[Job], and I think if you pass lazy, you get a Job that hasn't been started yet. Is there a difference between that and the proposed createJob()?

@benasher44 I'm not sure exactly what you're asking – launch creates an entirely new coroutine, not just a Job, so it's a bit heavyweight if you only need the Job. The Job() factory function also creates an already-active job, according to the docs, unlike lazy launch (unstarted jobs are not considered "active"), and if you're creating a new job to use as a parent you probably want it to be active.

With launch you would have to keep track of all of the jobs you are launching to cancel them which is the job of a scope

launch creates an entirely new coroutine

I guess I don't understand when this distinction is important compared to creating a Job that isn't a new coroutine. I think that's where I'm lost.

I think a typical use case is creating a SupervisorJob to act as a parent for coroutines launched from a view or something outside of coroutine world.

Ah! Gotcha. Thanks!

There was an attempt to add such helpers in #688.
We decided to postpone it because creating a child out of thin air is a bit dangerous.
For example, the following code

launch {
    childJob() // leftover after refactoring
}.join()

never completes because childJob will never be completed and without a proper debugger it may be really hard to pin down the problem.

Though writing something like SupervisorJob(coroutineContext[Job]) is not the best experience as well. We want to prototype #1406 to address this issue, for example, instead of

scope.launch(SupervisorJob(coroutineContext[Job])) {}

we will provide a flexible "BindJob" (name TBD, just to show the idea):

scope.launch(Bind(mode = Supervising)) {}
// or, TBD
scope.launch(Bind.Supervising)

Bind(mode = Supervising) knows how to bind itself as a supervising job between a parent scope and child job (launch).

It would be really helpful if you could elaborate on why you need such extensions because they have multiple use-cases:

1) Launching a child with a specific cancellation mode (launch(createSupervisorJob))
2) Creating an intermediate child in the hierarchy with multiple children:

fun createSubFragment() {
   val fragment = Fragment(createChildJob()) // Will be used as a field in the fragment
}

3) ...probably something else :)

Understanding the most common patterns will help us a lot in designing the best possible solution in #1406

That binding API looks really nice for (1).

My main use case is (2) in your list however. I'm working on a library that uses Jobs under the hood to scope and cancel concurrent work (effectively a hierarchical worker pool). The work is configured in a tree, and so we create a parallel tree of Jobs. Multiple coroutines may be started later using that job as the parent. We're creating Jobs explicitly, and not as part of an immediate launch or other new coroutine, so I don't think this binding approach, as described above, wouldn't solve my use case.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

ZakTaccardi picture ZakTaccardi  Â·  3Comments

elizarov picture elizarov  Â·  3Comments

sky87 picture sky87  Â·  3Comments

iTanChi picture iTanChi  Â·  3Comments

ScottPierce picture ScottPierce  Â·  3Comments