Kotlinx.coroutines: Change default for global vs child coroutines by scoping coroutine builder (structured concurrency)

Created on 27 Jun 2018  ·  63Comments  ·  Source: Kotlin/kotlinx.coroutines

Background and definitions

Currently coroutine builders like launch { ... } and async { ... } start a global coroutine by default. By global we mean that this coroutine's lifetime is completely standalone just like a lifetime of a daemon thread and outlives the lifetime of the job that had started it. It is terminated only explicitly or on shutdown of the VM, so the invoker had to make extra steps (like invoking join/await/cancel) to ensure that it does live indefinitely.

In order to start a child coroutine a more explicit and lengthly invocation is needed. Something like async(coroutineContext) { ... } and async(coroutineContext) { ... } or async(parent=job) { ... }, etc. Child coroutine is different from a global coroutine in how its lifetime is scoped. The lifetime of child coroutine is strictly subordinate to the lifetime of its parent job (coroutine). A parent job cannot complete until all its children are complete, thus preventing accidental leaks of running children coroutines outside of parent's scope.

Problem

This seems to be a wrong default. Global coroutines are error-prone. They are easy to leak and they do not represent a typical use-case where some kind of parallel decomposition of work is needed. It is easy to miss the requirement of adding an explicit coroutineContext or parent=job parameter to start a child coroutine, introducing subtle and hard to debug problems in the code.

Consider the following code that performs parallel loading of two images and returns a combined result (a typical example of parallel decomposition):

suspend fun loadAndCombineImage(name1: String, name2: String): Image {
    val image1 = async { loadImage(name1) }
    val image2 = async { loadImage(name2) }
    return combineImages(image1.await(), image2.await())
}

This code has a subtle bug in that if loading of the first image fails, then the loading of the second one still proceeds and is not cancelled. Moreover, any error that would occur in the loading of the second image in this case would be lost. Note, that changing async to async(coroutineContext) does not fully solve the problem as it binds async loading of images to the scope of the larger (enclosing) coroutine which is wrong in this case. In this case we want these async operations to be children of loadAndCombineImage operation.

For some additional background reading explaining the problem please see Notes on structured concurrency, or: Go statement considered harmful

Solution

The proposed solution is to deprecate top-level async, launch, and other coroutine builders and redefine them as extension functions on CoroutineScope interface instead. A dedicated top-level GlobalScope instance of CoroutineScope is going to be defined.

Starting a global coroutine would become more explicit and lengthly, like GlobalScope.async { ... } and GlobalScope.launch { ... }, giving an explicit indication to the reader of the code that a global resource was just created and extra care needs to be taken about its potentially unlimited lifetime.

Starting a child coroutine would become less explicit and less verbose. Just using async { ... } or launch { ... } when CoroutineScope is in scope (pun intended) would do it. In particular, it means that the following slide-ware code would not need to use join anymore:

fun main(args: Array<String>) = runBlocking { // this: CoroutineScope 
    val jobs = List(100_000) { 
        launch {
            delay(1000)
            print(".")
        }
    }
    // no need to join here, as all launched coroutines are children of runBlocking automatically
}

For the case of parallel decomposition like loadAndCombineImage we would define a separate builder function to capture and encapsulate the current coroutine scope, so that the following code will work properly in all kind of error condition and will properly cancel the loading of the second image when loading of the first one fails:

suspend fun loadAndCombineImage(name1: String, name2: String): Image = coroutineScope { // this: CoroutineScope 
    val image1 = async { loadImage(name1) }
    val image2 = async { loadImage(name2) }
    combineImages(image1.await(), image2.await())
}

Additional goodies

Another idea behind this design is that it should be easy to turn any entity with life-cycle into an entity that you could start coroutines from. Consider, for example, some kind of an application-specific activity that is launch some coroutines but all of those coroutines should be cancelled when the activity is destroyed (for example). Now it looks like this:

class MyActivity {
    val job = Job() // create a job as a parent for coroutines
    val backgroundContext = ... // somehow inject some context to launch coroutines
    val ctx = backgroundContext + job // actual context to use with coroutines

    fun doSomethingInBackground() = launch(ctx) { ... }
    fun onDestroy() { job.cancel() }    
}

The proposal is to simply this pattern, by allowing an easy implementation of CoroutineScope interface by any business entities like the above activity:

class MyActivity : CoroutineScope {
    val job = Job() // create a job as a parent for coroutines
    val backgroundContext = ... // somehow inject some context to launch coroutines
    override val scopeContext = backgroundContext + job // actual context to use with coroutines

    fun doSomethingInBackground() = launch { ... } // !!! 
    fun onDestroy() { job.cancel() }    
}

Now we don't need to remember to specify the proper context when using launch anywhere in the body of MyActivity class and all launched coroutines will get cancelled when lifecycle of MyActivity terminates.

design enhancement for 1.0 release

Most helpful comment

We plan to have this done before kotlinx.coroutines reaches 1.0.

All 63 comments

I would to share some personal consideration about this proposal.

(1) The section "Automated error propagation works" in the linked article is uncovered, so I consider this aspect unchanged.
(2) Instead "Automatic resource cleanup works" should work.

(3) In this proposal and "A surprise benefit: removing go statements enables new features" the Job interface and "coroutine scope" share same aspects, so differences/purposes not look really clear to me. Have these different porposes? Should both merged?
Can we replace the coroutineScope { ... } builder with a job { ... } builder?

(4) Should the sentence

For the case of parallel decomposition like loadAndCombineImage we _would_ define a separate builder function

changed to

For the case of parallel decomposition like loadAndCombineImage we _have to_ define a separate builder function

otherwise we cannot run async code in the same context (might it be the same job?)

(5) The code suspend fun loadAndCombineImage(name1: String, name2: String): Image = coroutineScope { redefines this, I consider the this ridefinition a little error prone for every class's suspending methods.

(6) Should the builder coroutineScope be equivalent to withContext(coroutineContext)?

(7) Is the builder coroutineScope a top level function?
Are GlobalScope.async {...} and coroutineScope { async { ...} } equivalent?

(8) Do the following code

coroutineScope {
    // some code
    coroutineScope {
        /// other code
    }
}

break the rules? (Is "other code" in the global scope?)

I try to reply myself: if the coroutineScope's behaviour and withContext''s behaviour are similar (6) (it looks so) then the answer to (7) is:

No, coroutineScope { async { ...} } is equivalent to withContext (DefaultContext) { ... } but discards the result/error.

the answer to (8) is

No, it create a inner scope and this acts as a barrier for other code

@fvasco Let me try to answer:

(1) We also plan for automated error propagation and handling but we don't have a very detailed plan yet. An overall idea is that any scope waits for all its children and if any of those children have _unhandled_ failures (precise definition of this is TBD), then this scope also fails and propagates error upwards.

(2) Our model is that every resource can be represented as a Job and if you create a job that is subordinate to your scope, then there is a guarantee that it gets cancelled (cleaned up) when this scope terminates. That is automated resource cleanup.

(3) We are still struggling how to present the differences between the Job, CoroutineContext, and CoroutineScope and whether some of those concepts should be merged. Current model we have is this:

  • Job is a part of CoroutineContext, but context can contain other items beyond the job.
  • CoroutineScope has a reference to the context and provides coroutine builders as extensions on it.
    So:
    CoroutinesScope - has -> CoroutineContext - has -> Job
    However, the missing part of this picture is that every time you create a new scope, a new Job gets created, too, so, in some sense, CoroutineScope and a Job are the same thing, but if you only have a reference to a Job, you cannot get neither its context nor scope. That's complicated. I don't know at the moment how to make it less confusing.

Using job { ... } as a name of the builder instead of coroutineScope { ... } is one of the options we have on the table.

(4) Good point. Indeed, we "have to"

(5) Redefinition of this is indeed not an ideal solution. We could not find a better solution in the confines of the Kotlin language, so that we can still get the desired syntactic behavior (type-classes would have helped here, but we don't have them now).

(6) Indeed, a coroutineScope builder is conceptually equivalent to withContext(coroutineContext) { .. .}, but the later does not redefine this to CoroutineScope, so you would not be able to directly use async and launch as extensions inside of withContext, but you can do it inside coroutineScope.

(7) coroutineScope is top-level function but the two expressions you gave have different semantics:

  • GlobalScope.async {...} starts a coroutine without parent. You have to explicitly manage its lifecycle, the hardest part of which is that you should not forget to cancel it if you don't need it anymore, even if you crash. You can use this code anywhere (both in suspend and non-suspend functions).
  • coroutineScope { async { ...} } start a coroutine that is subordinate to the delimited coroutineScope block. The scope block cannot terminate until its children async coroutine is working. If something crashes inside this coroutine scope, then async gets cancelled. There is zero risk of forgetting about it (zero risk of leaking it). However, you can use this code only inside suspending function, because coroutineScope is a suspending function.

(8) You can freely nest coroutineScope inside each other without problems. Nothing is going to break.

UPDATE: I've added "Additional goodies" to the top message with some additional explanations of the proposed design.

Hi @elizarov,
"Additional goodies" makes me sceptic.

For my point of view MyActivity "is not" a CoroutineScope, moreover implementing CoroutineScope makes the scope public, and this can break OO encapsulation (I can invoke myActivity[Job]?.cancel()).

Seeing the your example, as related consideration, I consider misleading the Job's parent parameter.
You use the context to define the right job, but you can also use parent to override the job (so it is possible define the parent in two ways).
But using launch(ctx, parent = null) does not create a standalone job.
So I propose you to reconsider the parent parameter.

@fvasco Ah. Here is confusion of CoroutineScope and CoroutineScope again. Let me clarify.

CoroutineScope is not a CoroutineContext so if you implement CoroutineScope in MyActivity you _cannot_ do myActivity[Job]. But since MyActivity _is_ a CoroutineScope it provide a _scope_ for coroutines. By the word "scope" we mean a "delimited lifetime", so when MyActivity terminates all the coroutines that were launched "in its scope" are cancelled.

Yes. It looks like we should deprecate launch(ctx, parent=...) as part of this proposal.

Here is confusion of CoroutineScope and CoroutineScope again.

I am not alone :)

Sorry, I mean myActivity.scopeContext[Job], becouse scopeContext is public, so -I suspect- everyone can create coroutine in this scope or cancel it.

so -I suspect- everyone can create coroutine in this scope or cancel it.

It seems reasonable if the lifecycle of your activity is public to observers, which is usually the case. If the lifecycle is private, then you should hide your coroutine scope, too.

With Android Architecture Components / AndroidX lifecycles, you can make a Job tied to the lifecycle of an Activity, a Fragment or any LifecycleOwner.
I made a gist for this: https://gist.github.com/LouisCAD/58d3017eedb60ce00721cb32a461980f

This doesn't need to add boilerplate to the Activity.
It should be easily adaptable to the change discussed in this issue by defining coroutine builders (launch & async only, because no runBlocking in an Android Activity) as extensions of Lifecycle and LifecycleOwner.

Hi, I had a weird idea that can target the same purpoise but using a new suspending function for child coroutine builder replacing current one (launch, async...), resulting coroutine would be automatically a child of parent Job. But it needs to rename existing global non suspending coroutine builder to avoid method name conflict (launchGlobal, asyncGlobal...).

Here an example for publish :

Existing function is renamed to publishGlobal (no change except name) :

public fun <T> publishGlobal(
    context: CoroutineContext = DefaultDispatcher,
    parent: Job? = null,
    block: suspend ProducerScope<T>.() -> Unit
): Publisher<T> = Publisher { subscriber ->
    val newContext = newCoroutineContext(context, parent)
    val coroutine = PublisherCoroutine(newContext, subscriber)
    subscriber.onSubscribe(coroutine) // do it first (before starting coroutine), to avoid unnecessary suspensions
    coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
}

And now the new child coroutine builder publish :

suspend public fun <T> publish(
        context: CoroutineContext = DefaultDispatcher,
        parent: Job? = null,
        block: suspend ProducerScope<T>.() -> Unit
): Publisher<T> {
    val parentJob = parent ?: coroutineContext[Job]
    return Publisher { subscriber ->
        val newContext = newCoroutineContext(context, parentJob)
        val coroutine = PublisherCoroutine(newContext, subscriber)
        subscriber.onSubscribe(coroutine) // do it first (before starting coroutine), to avoid unnecessary suspensions
        coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
    }
}

From a non suspending function, the only way to build a coroutine would be to call _____Global coroutine builder, because suspending functions are not available.
This would be clear that it is a global coroutine, it would also be possible to use _____Global coroutine builder when this is explicitly needed inside existing Coroutine.

But this solution does not cover the the case of parallel decomposition like loadAndCombineImage, it would bind async loading of images to the scope of the larger (enclosing) coroutine and not the suspending function only.
Maybe there is something to do to solve this case , perhaps the same solution you showed with a separate builder function to capture and encapsulate the current coroutine Context ?

What do you think about it ?

@pull-vert We've considered an option to duplication every builder xxx with xxxGlobal with suspend fun xxx and regular fun xxxGlobal.

However, we had decided that that is going to be too much duplication. Another disadvantage of this xxx and xxxGlobal design is that the scope that is being captured by xxx builders is implicit. Let me explain.

The code can be in the middle of some deep call hierarchy inside suspend fun foo() and when I do launch { ... } from inside of foo I don't want the new coroutine to become a child of some outer Job that had invoked foo() somewhere (and who knows how long that outer coroutine is going to run). I really want the new coroutine to become a child of foo _itself_, that is, foo shall wait until the coroutines it had launched terminate. Structured concurrency. That is what I want.

In the design that we are currently considering, we don't have any duplication. You can use xxx and Global.xxx. At the same time, we get structured concurrency. You cannot simply invoke launch { ... } from inside of suspend fun foo(). You have to clearly demarcate its scope with coroutineScope { .... }.

Thanks for clarification, I understand that launching coroutine will need a coroutineScope after this evolution : either using CoroutineScope in scope when directly in a parent coroutine, or by clearly demarcate its scope with coroutineScope { .... } in a suspend fun foo()).

A small exemple to see if I understood correctly :-)
exemple 1 : direct children coroutines

fun main(args: Array<String>) = runBlocking { // this: CoroutineScope1
    val jobs = List(100_000) { 
        launch { // extension of CoroutineScope1, child of runBlocking
            delay(1000)
            print(".")
        }
    }
    // no need to join here, as all launched coroutines are children of runBlocking automatically
}

exemple 2 : children coroutines inside suspend fun foo()

fun main(args: Array<String>) = runBlocking { // this: CoroutineScope1
    foo() // just call suspending foo
    // foo() will end only when all launched coroutines are done
}

suspend fun foo() = coroutineScope { // this: CoroutineScope2 : required for calling launch
    val jobs = List(100_000) { 
        launch { // extension of CoroutineScope2, child of suspend foo fun, but not of runBlocking
            delay(1000)
            print(".")
        }
    }
    // no need to join here, as all launched coroutines are children of suspend foo fun automatically
}

@pull-vert Yes. Your examples correctly show proposed enhancement.

@elizarov to be more consistent for coroutine start, as a coroutineScope is supposed to be the...scope of a block of code !
Instead of GlobalScope.launch { ... } maybe it would be better to define it like this ?

fun main(args: Array<String>) {
    globalScope { // this: CoroutineScope
        launch { // extension of CoroutineScope
            ....
        }
    }
}

GlobalScope.launch looks better to me since it reduces amount of indentation.

I agree that GlobalScope.launch looks better than coroutineScope, for the same reason, less indentation.

One minor thought is that I'm not in love with the name GlobalScope, as this will become the default entry point to the coroutine library. GlobalScope is descriptive of the context the coroutine is running in, not the entry point that is being used. i.e. if this were a part of the Picasso library, the call would be Picasso.xxx. In RxJava to get the io scheduler, I call Schedulers.io(). Given that the term Scope is so generic, I feel like GlobalScope answers the "where", not the "what", and intuitively I'd expect an API like this to be placed on a "what". It feels a little clunky. I don't really have an alternative to propose, just a thought.

The xxxGlobal also looks good. You mentioned it'd be too much duplication, but that doesn't seem like much duplication at all. Maybe there is something behind the scenes I'm not realizing?.

@ScottPierce The entry point is likely to be runBlocking for JVM, and Lifecycle/LifecycleOwner on Android, so GlobalScope will probably not be used that much.

@LouisCAD Not sure I agree with that.

For the JVM, I assume you reference runBlocking because you'd likely use it in your main method to stop the program from exiting? If that's what you mean, for anything more than a single file script, you'd have other files that would be creating and maintaining coroutines, and GlobalScope would need to be used.

For Android, I create coroutines all the time in my client API layer in places outside of a Lifecycle, or LifecycleOwner. I know it's been integrated into the support library as a whole, but I've actually stayed away from the Lifecycle jetpack library because of it's dependency on annotation processing.

Lifecycle can work without annotation processing.

Look at the LifecycleObserver class I wrote in the arch-lifecycle module
of my library Splitties, it works without any annotation processor.

On Mon, Jul 30, 2018, 12:53 AM Scott Pierce notifications@github.com
wrote:

@LouisCAD https://github.com/LouisCAD Not sure I agree with that.

For the JVM, I assume you reference runBlocking because you'd likely use
it in your main method to stop the program from exiting? If that's what
you mean, for anything more than a single file script, you'd have other
files that would be creating and maintaining coroutines, and GlobalScope
would need to be used.

For Android, I create coroutines all the time in my client API layer in
places outside of a Lifecycle, or LifecycleOwner. I know it's been
integrated into the support library as a whole, but I've actually stayed
away from the Lifecycle jetpack library because of it's dependency on
annotation processing.


You are receiving this because you were mentioned.

Reply to this email directly, view it on GitHub
https://github.com/Kotlin/kotlinx.coroutines/issues/410#issuecomment-408711889,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AGpvBeqRfZUlGzCFOOWu7QeoYMfTytt0ks5uLjzogaJpZM4U5eM1
.

@elizarov Roughly each day, I'm thinking about this issue and how much the proposal is better than what we currently have. is there a way to bring experimental implementations with a different name from the final one so we can start trying to use this new coroutine scope thing sooner, and without having to refactor all the code at once?

FYI, what I want to experiment with is bridging Lifecycles from AndroidX/AndroidArchComponents to sort of CoroutineScope and see how I can make the code even more readable and safer with it.

Just wondering, how does this (and all other interesting improvement discussions here) relate to 1.3 coroutines going stable? Is it good idea to deprecate some functionality (like launch) almost immediately?

We plan to have this done before kotlinx.coroutines reaches 1.0.

This is a very interesting proposal, especially when coupled with https://github.com/Kotlin/kotlinx.coroutines/issues/428

A few additions to the conversion:

On Android some coroutines will be created in a lifecycle aware class like Activity/Fragment, and others will be created in ViewModel/Repository. For most Arch Components data loading patterns, Repository is a natural place to enter a coroutine scope.

Coroutines started in the Activity/Fragment are likely going to be used to drive the UI with light processing. While coroutines started in the Repository will likely do more than a few milliseconds of data processing and blocking disk IO.

From an Android perspective, the API will need to work without this being a CoroutineScope when called from the Repository as they don't have a superclass.

Seconding the point above that GlobalScope as an entry-point doesn't feel like a natural place to go. As an API user, this spelling reads "In GlobalScope launch (on the CommonPool)" which is quite a few concepts to launch a coroutine when compared to other implementations e.g. ES6 :):

Especially in the Repository case this will be the default API so some care should go into making this line read cleanly. Some other spellings might be,

Coroutine.launch {
  // CoroutineContext + New Scope
}
IO.launch {
  // CoroutineContext + New Scope
}

Though as I worked through the API, I wasn't really clear what the intended API difference between CoroutineScope.launch and (CoroutineContext.job, CoroutineContext.coroutineScope, GlobalScope.launch) is.

(1) Let me try to distinguish, is this correct?

  • CoroutineScope allows calling of launch {} and async{} to kick off async work as well as suspend functions to wait for async tasks to finish.
  • CoroutineContext allows calling of suspend functions to wait for async tasks to finish, but cannot call launch {} or async {}

(2) It seems like the concepts can be folded together in the simple case from an API perspective? That way coroutineScope { launch { } } is the behavior of launch?

GlobalScope.launch(IO) { // scope 1
   launch { // scope 2
}}

Since scopes are composed and behave with a similar semantics to subroutines, this is probably the least surprising as an API user. Every call to launch will wait for all it's children, and I don't need to work about the distinction between scope and context.

In a degenerate case this might cause overhead:

GlobalScope.launch(IO) { // scope 1
   launch { // scope 2
     launch { // scope 3
       launch { // scope 4
         1 // for very async values of 1
}}}}

but code like this seems atypical - normally each launch would correspond to another suspend function call which would want to create a new scope- and could be handled by a parameter to launch/async like launch(waitForChildren=false)

(3) Does a simplified API like this make sense, and if not what situations is it missing?

Here is the first taste of structured concurrency: https://github.com/qwwdfsad/android-architecture/commit/a6617e389e6ee3fa1e208907cd807c7f1d3520b6

This is minimal MVP of migration without other changes in the application code.
Note how the whole layer of cancellation problems was fixed: now all launched jobs are cancelled as soon as an activity is destroyed, including requests to the data source (note: no changes in the data-source!)

What could be done to make application even "simpler":
1) Make presenters API suspend-based. Then CoroutineScope will be used only from withing activities
2) Join all launched children in destroy to proper shutdown and diagnostic

Migration towards these changes among others could be made easier if structural replace was supported: https://youtrack.jetbrains.com/issue/KT-10176

There have been a few glancing references to using CoroutineScope vs CoroutineContext as the receiver for the new builder methods, but I don't think that trade-off has been explicitly discussed and (tl;dr) I would really like to hear @qwwdfsad's and @elizarov's thinking around this.

@elizarov's initial post mentions "changing the default [context]" because it's easy to forget to pass an explicit context when calling the builders. Given that reason, using CoroutineScope only seems to go halfway to solving the problem: any code living in a suspend function without an explicit scope will still need to pass the coroutineContext to get structured concurrency behavior. Or, for brevity, people will get into the habit of just always using the global scope builders, and then we've got the same problem we started with.

I can see one argument where using CoroutineScope as the receiver makes some sense - since the scope defines the lifetime of a coroutine, you don't want to be able to make a bunch of coroutines from suspend function that are children of whatever coroutine the function happens to be called from, without being conscious of/explicit about which scope should actually be the parent. I feel this is actually useful behavior - much like function call stacks, called functions are always "children" of their callers by default. For the sake of structured concurrency, if I call a function that happens to spawn some coroutines, it seems reasonable to expect they will probably be created as children of the current one unless otherwise documented (eg. channel operators that create new coroutines to read/write channels).

Using CoroutineContext as the receiver for builders (or rather, making builders top-level functions that effectively use the top-level coroutineContext property as the default value for their context) seems like a much more consistent and convenient API. Any code anywhere could just call launch { … } and know that the spawned coroutine will have a reasonable parent. It is also simpler to reason about - the only time an explicit context/parent job would need to be specified would be when breaking out of the current scope (which also happens to mirror how threading works - only need to specify a thread/executor when _switching_ threads).

So to reiterate - what is the reasoning behind deciding to use CoroutineScope as the receiver for coroutine builders instead of just using the current coroutineContext?

any code living in a suspend function without an explicit scope will still need to pass the coroutineContext to get structured concurrency behavior.

Standalone builders are already deprecated and will be removed in the follow-up release after initial feedback.

Or, for brevity, people will get into the habit of just always using the global scope builders, and then we've got the same problem we started with.

There are builders such as currentScope and coroutineScope and explicit mentioning in the documentation that GlobalScope should not be used generally. We can't prevent API abuse, but at least we can make it less error-prone for the users.
Next step will be to provide more extension points such as Android activities which are CoroutoneScope by default.

I can see one argument where using CoroutineScope as the receiver makes some sense - since the scope defines the lifetime of a coroutine, you don't want to be able to make a bunch of coroutines from suspend function that are children of whatever coroutine the function happens to be called from, without being conscious of/explicit about which scope should actually be the parent

This is one of the benefits. Moreover, the recommended ("best practices" which eventually will be followed unconsiously :)) way to provide a scope is coroutineScope, which doesn't allow to ignore exceptions, having global forgotten jobs etc.
Another one is that coroutine scope is provided, users don't have to think "oh, and here I have to pass my activity's Job", everything starts to work automatically.

what is the reasoning behind deciding to use CoroutineScope as the receiver for coroutine builders instead of just using the current coroutineContext?

Separation of concerns, they have different semantics. CoroutineContext is a bag of properties, attached to a coroutine and stored in continuation (so coroutineContext intrinsic works). Every implementor has to implement three methods (fold, minusKey, get) which are irrelevant to the scope abstraction.
Additional downside is a signatures like CoroutineContext.async(context: CoroutineContext), which are completely misleading.

CoroutineContext is a general-purpose stdlib abstraction, while CoroutineScope has a well-defined meaning specific to kotlinx.coroutines, additional documentation and meaningful name.
For example, you are observing that something is a CoroutineContext. Does it have a job in it? Does it make sense to call launch on it, will anyone handle an exception? Who is a lifecycle owner?
CoroutineScope makes it at least more explicit if not obvious.

Would this change allow to load class properties in coroutines?
If my Activity implements CoroutineScope, could I do:

val settings = withContext(IO) { ... }

?
That would require any function calling settings to be suspendable, I'm not sure it's possible, but that would be awesome.

@GeoffreyMetais This is not possible. Your snippet needs to be called from a coroutine, and constructors, init blocks and member declarations are not suspend.

For now, that's the reason of my question.
Even if I guess it's too difficult to be safe.

@GeoffreyMetais You can hack with companion objects, suspend operator fun invoke and private constructors to inject values that are suspend computed into the constructor.

Here's an example for Android SharedPreferences typesafe and key-safe wrapper:
abstract class to be extended by the companion object
and the class using it.

I hadn't noticed the currentScope and coroutineScope functions before, my bad. I just read through all the commits on the structured-concurrency branch and they seem like a really elegant solution, with a nice balance of being explicit about what scope you're using and a really ergonomic API.

In particular, glad to see that all context elements are inherited. However, doesn't that change the behavior of CoroutineName? Unless I'm missing something, once you add that element to a context all coroutines started from that context without an explicit name will have the same name, which seems to me like it would defeat the purpose of giving a coroutine a name (ie. being able to distinguish it from other coroutines for debugging).

@zach-klippenstein thanks for the feedback

In particular, glad to see that all context elements are inherited. However, doesn't that change the behavior of CoroutineName?

All builders are aware of CoroutineName and owerwrite it, they wrap coroutine context in newCoroutineContext.

@zach-klippenstein let me chime in here and correct @qwwdfsad a bit

In particular, glad to see that all context elements are inherited. However, doesn't that change the behavior of CoroutineName? Unless I'm missing something, once you add that element to a context all coroutines started from that context without an explicit name will have the same name, which seems to me like it would defeat the purpose of giving a coroutine a name (ie. being able to distinguish it from other coroutines for debugging).

Conceptually, coroutines are ephemeral. In particular, when one coroutine decides to decompose the work its doing and delegate it to children coroutines, it should be absolutely transparent for everybody. Think of CoroutineName and all the other context elements as defining the whole work the coroutine is doing, so in this sense CoroutineName inheritance is perfectly desired, because you should not be noticing the fact that there is a child coroutine doing some work on behalf of its parent.

On the other hand, for debugging purposes, we have CoroutineId context element that is uniquely generated for each coroutine in debug mode. That thing is indeed unique, but that is just for convenience of debugging.

Right, and I just confirmed that is the behavior that's actually happening (child coroutines inherit their parents' names but have unique IDs). Thanks for the clarification!

We are still struggling how to present the differences between the Job, CoroutineContext, and CoroutineScope and whether some of those concepts should be merged.

In my mind a clear and clean API would look something like the following:

  • A Job object is hidden inside CoroutineScope and never exposed. Manipulating Job objects directly is considered unsafe.
  • CoroutineContext does not contain the job instance, which instead is inside the coroutineScope.
  • All the other elements of a coroutine context are there in is a dictionary-like structure as before to allow seamless framework integration.

In essence I believe that coroutine API should completely hide all Job instances as implementation details, even if we have to duplicate parts of Job API (e.i cancellation, progress, etc). The arguments for limiting the concurrency API to only scoped coroutines are well outlined in the go statements: not even once part of the original article.

Hi @elizarov, I am currently migrating to the new coroutines with 1.3.0-rc-116 and I came across this use case. Let's have a class which implements CoroutineScope and has override val coroutineContext = Dispatchers.JavaFx. Now, if somewhere in my code I do:

launch {
    val r1 = async {  }
    val r2 = async {  }
    r1.await()
    r2.await()
}

The launch launches on JavaFx thread, that is just fine, but both nested async calls launch also on the JavaFx thread, which means that although I have 2 async calls in my code, there is nothing really async happening since the used coroutine context is single-threaded, which is super-confusing.

Would it be possible to modify the default async call behaviour as follows?

  • When GlobalScope.async {} is called, it will run on IO pool by default
  • When async {} is called as a child coroutine (inside launch, withContext or another async), it will also run onIO pool by default
  • When async {} is called inside a class which defines override val coroutineContext, it will run on that coroutineContext by default

@LittleLightCz If you want to change the dispatcher, pass it to async(…) { … }. If you have suspension points into the async blocks though, you can still have parallel execution while a coroutine is suspended.

Kotlin tries to be concise, but explicit. Your proposal is not explicit, so I wouldn't want it, and I wouldn't existing code to change its behavior.

@LouisCAD I understand. The thing is, that in the previous version of coroutines I was happily writing async {}, which ran on CommonPool and it mostly was what I wanted. Now it seems I will have to get used to be more explicit :-).

Anyway it would be perfect if the coroutine builder defaults were fine-tuned in a way that the programmer's explicitness was inverse-proportional to the commonness of the problem he/she is trying to slove.

For my happines I would get by just with 1 UI thread and 1 nice IO pool for everything else :grin:

@LittleLightCz You can still write your own extension named asyncIO { … } or alike to have that default behavior. Before structured concurrency, I had a launchInUi { … } extension to launch on the UI thread on Android and was happy to be able to do this with Kotlin wonderful extensions support.

@LittleLightCz you can have the same behavior with coroutineContext = Dispatchers.IO

async with no argument would dispatch in IO dedicated threadpool, and you should have to specify Dispatchers.JavaFx when needed.

Current coroutine behavior is flexible enough to fit your case.

@GeoffreyMetais Hmh, maybe you're right :-). Speaking of TornadoFx apps, if all View classes had coroutineContext = Dispatchers.JavaFx and all Controller classes had coroutineContext = Dispatchers.IO, then perhaps I could get the behaviour I need. I will try to fiddle with the code design and I will see - thanks so far! :-)

It seems I encountered another caveat:

override val coroutineContext = Dispatchers.JavaFx

launch {
    launch(Dispatchers.IO) { Browser.open(it) }
}

The Browser.open(it) won't get executed at all. However if I change it to GlobalScope.launch(Dispatchers.IO) { Browser.open(it) } then it works. I think that compiler should give me at least some warning, because at that point I had no idea that I was doing something wrong. (Or is this a bug?)

EDIT: It seems it's a bit trickier than I thought - in some cases it works and in other cases it requires GlobalScope prefix. I will try to make a dummy code and reproduce it tomorrow.

If you add println("At least, printing works") before Browser.open(it),
does it get executed?

On Fri, Sep 28, 2018, 10:31 PM Antonín Brettšnajdr notifications@github.com
wrote:

It seems I encountered another caveat:

override val coroutineContext = Dispatchers.JavaFx

launch {
launch(Dispatchers.IO) { Browser.open(it) }
}

The Browser.open(it) won't get executed at all. However if I change it to GlobalScope.launch(Dispatchers.IO)
{ Browser.open(it) } then it works. I think that compiler should give me
at least some warning, because at that point I had no idea that I was doing
something wrong. (Or is this a bug?)


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/Kotlin/kotlinx.coroutines/issues/410#issuecomment-425557276,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AGpvBVAWQxlwrjvGq9tEiV9wbK774Z2Mks5ufocOgaJpZM4U5eM1
.

It seems it occurs only when I use one unusual design pattern (which however worked wonderfully before :grin:). Reproduced here: https://youtrack.jetbrains.net/issue/KT-27281

Regarding cancellation, having this pseudo-code:

override val coroutineContext = Dispatchers.JavaFx

val job = launch {
   anotherClass.someSuspendFunc(this)
}

job.cancel()

Do I understand it correctly that if I want to make further nested async {} calls inside that someSuspendFunc cancellable, I need to pass the current CoroutineScope to it and then do something like:

parentScope.run {
    async(IO) { ... }
}

where now the async(IO) { ... } call will be cancelled as well if the original job was cancelled?

@LittleLightCz

which means that although I have 2 async calls in my code, there is nothing really async happening since the used coroutine context is single-threaded, which is super-confusing.

Being able to do many things in the single-thread is the whole point of _asynchronous programming_ (sic!). You only need to care about threads when your code is _not_ asynchronous, that is, when your code is blocking or consuming a lot of CPU.

After a week (although actually it took me 1-2days :grin:) I migrated one of my smaller project (22 kt files, ~1.2K LOC) to the new structured coroutines and here is my overall feedback:

  • What I like the most about the new coroutines is the easiness of cancellability. It works very nice and it even seems there is no need to do the isActive checks anymore (if I tend to switch contexts more atomically)
  • On the other hand it's a pity that this parent/child relation gets broken when I call a suspend function from a different class. When I want all another nested coroutines inside that suspend function to be cancellable as well, I need to pass the CoroutineScope manually as a parameter. Perhaps in the future this could be done by the compiler automatically?
  • Inside this cancellable suspend function, when I spawn new coroutines on that passed CoroutineScope, I always need to be explicit about on which pool will the new coroutine run since any CoroutineScope can be passed there. Therefore I think it is a good idea that a child coroutine inherits the parent's scope, but not that it also inherits the parent's context/thread pool.
  • I discouraged myself of using the "class that implements CoroutineScope" approach since when I navigated to some suspend function to do some changes and saw a coroutine builder call, at the first look I couldn't tell where this couroutine will run so I always needed to scroll all the way up to check which coroutineContext is defined there. Therefore I tend more to use the GlobalScope. approach.
  • However since writing GlobalScope. is (relatively) a lot of boilerplate, then I decided to go with my own custom set of coroutine builders as @LouisCAD suggested and I've currently settled on these ones which cover almost all of my use cases (+ I am experimenting with the brand new name for the "default withContext" as well :grin:):
fun launchUI(block: suspend CoroutineScope.() -> Unit) = GlobalScope.launch(Dispatchers.JavaFx, block = block)

fun launchIO(block: suspend CoroutineScope.() -> Unit) = GlobalScope.launch(Dispatchers.IO, block = block)

fun <T> asyncIO(block: suspend CoroutineScope.() -> T) = GlobalScope.async(Dispatchers.IO, block = block)

fun <T> CoroutineScope.asyncIO(block: suspend CoroutineScope.() -> T) = async(Dispatchers.IO, block = block)

suspend fun <T> offload(block: suspend CoroutineScope.() -> T) = withContext(Dispatchers.IO, block = block)

@LittleLightCz Can you please, elaborate on this comment:

On the other hand it's a pity that this parent/child relation gets broken when I call a suspend function from a different class. When I want all another nested coroutines inside that suspend function to be cancellable as well, I need to pass the CoroutineScope manually as a parameter. Perhaps in the future this could be done by the compiler automatically?

Can you show a piece of code where you have to do that? Have you tried to use coroutineScope { ... }?

@elizarov It seems that I've finally managed to reproduce this behavior on the following code:

import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Default

class Foo() {
    suspend fun processIds() {
        (1..1000).map { id ->
            GlobalScope.async(IO) {
                Thread.sleep(200)
                println("Processed id $id")
            }
        }.awaitAll()
    }
}

fun main() {
    val job = GlobalScope.launch(Default) {
        Foo().processIds()
    }

    Thread.sleep(1000)
    job.cancel()
}

When the job.cancel() is called, it seems that async(IO) coroutines continue to run. However if I do:

class Foo() {
    suspend fun processIds(parentScope: CoroutineScope) {
        (1..1000).map { id ->
            parentScope.async(IO) {
                Thread.sleep(200)
                println("Processed id $id")
            }
        }.awaitAll()
    }
}

fun main() {
    val job = GlobalScope.launch(Default) {
        Foo().processIds(this)
    }

    Thread.sleep(1000)
    job.cancel()
}

Then it stops at around ID of 320. BTW I haven't used coroutineScope { ... } yet, so unfortunately I don't know what do you mean - can you please give me a hint how it could be used here?

Roman wrote an article about structured concurrency that covers
coroutineScope, and the docs have been updated. Wouldn't you check it out,
try again with the suggestion, and report back?

On Sat, Oct 6, 2018, 10:26 PM Antonín Brettšnajdr notifications@github.com
wrote:

@elizarov https://github.com/elizarov It seems that I've finally
managed to reproduce this behavior on the following code:

import kotlinx.coroutines.*import kotlinx.coroutines.Dispatchers.IOimport kotlinx.coroutines.Dispatchers.Default
class Foo() {
suspend fun processIds() {
(1..1000).map { id ->
GlobalScope.async(IO) {
Thread.sleep(200)
println("Processed id $id")
}
}.awaitAll()
}
}
fun main() {
val job = GlobalScope.launch(Default) {
Foo().processIds()
}

Thread.sleep(1000)
job.cancel()

}

When the job.cancel() is called, it seems that async(IO) coroutines
continue to run. However if I do:

class Foo() {
suspend fun processIds(parentScope: CoroutineScope) {
(1..1000).map { id ->
parentScope.async(IO) {
Thread.sleep(200)
println("Processed id $id")
}
}.awaitAll()
}
}
fun main() {
val job = GlobalScope.launch(Default) {
Foo().processIds(this)
}

Thread.sleep(1000)
job.cancel()

}

Then it stops at around ID of 320. BTW I haven't used coroutineScope {
... } yet, so unfortunately I don't know what do you mean - can you
please give me a hint how it could be used here?


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/Kotlin/kotlinx.coroutines/issues/410#issuecomment-427603772,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AGpvBbxcyu88bqiWRlXM5wFe0ibtTZT-ks5uiRHkgaJpZM4U5eM1
.

@LouisCAD Thanks, found it :relaxed: (https://medium.com/@elizarov/structured-concurrency-722d765aa952). So what I did was just simple:

    suspend fun processIds() = coroutineScope {
        (1..1000).map { id ->
            async(IO) {
                Thread.sleep(200)
                println("Processed id $id")
            }
        }.awaitAll()
    }

and now it really works indeed! That's nice :relaxed:. I recall that I read Roman's article back then when it was released but this detail sort of vanished from my mind - now I will hopefully remember :wink:.

Btw the above implies that one of my own custom functions is not needed anymore :thinking:

fun <T> asyncIO(block: suspend CoroutineScope.() -> T) = GlobalScope.async(Dispatchers.IO, block = block)

Refactored the rest of my code and it looks cleaner now - the coroutineScope { ... } was a smart design move :relaxed:. I always say that JetBrains as a company is named like that for a reason :wink: :grin:.

The evolution continues. I got rid of my previous GlobalScope extensions, where now I incline more to define my own scopes as vals + use the invoke operator overload (one of the "crazy" ideas that Roman already mentioned in different thread - I think) for launch by default. Currently I am at:

val UI = object : CoroutineScope {
    override val coroutineContext = Dispatchers.JavaFx
}

val IO = object : CoroutineScope {
    override val coroutineContext = Dispatchers.IO
}

operator fun CoroutineScope.invoke(block: suspend CoroutineScope.() -> Unit) = launch(block = block)

fun <T> CoroutineScope.asyncIO(block: suspend CoroutineScope.() -> T) = async(Dispatchers.IO, block = block)

suspend fun <T> offload(block: suspend CoroutineScope.() -> T) = withContext(Dispatchers.IO, block = block)

Hi @elizarov, in the last few days I was thinking about your comment you gave me on your article (https://medium.com/@elizarov/futures-cancellation-and-coroutines-b5ce9c3ede3a) and as a conclusion I found a way how to progress into more idiomatic coroutines code and got rid of my fun <T> CoroutineScope.asyncIO() extension function mentioned above using the combo of: withContext(IO) { async { ... }}. If I understood you right, you were talking about the async { withContext() { ... }} way, but I hope that even the reversed approach is ok (idiomatic) too :relaxed:. The main concept that I bear in my mind from now on is that the suspend function is responsible for non-blocking and it seems it works fine so far.

@LittleLightCz consider the differences between withContext(IO) { async { ... }}, async { withContext(IO) { ... }} and async(IO) { ... }

@fvasco Well, since for withContext(IO) {} I have this custom shortcut: offload {}, then taken the scenario when I want to use async inside a suspend function, my real code looks like this:

Before:

suspend fun doSomething() = coroutineContext {
    .
    .
    .
    asyncIO { ... }
}

After:

suspend fun doSomething() = offload {
    .
    .
    .
    async { ... }
}

offload is shorter than coroutineContext and async is shorter than asyncIO, so I think this is a win-win :relaxed: :clap: :sparkles:

@LittleLightCz the two coroutines are different different, these use different dispatchers.
Moreover put the async at the end of code (look at example) does not look really useful to me.
I am confused.

@LittleLightCz If you told me you understood the purpose of the async function, I would have a hard time believing you. I use it only for parallel (non sequential) coroutines execution.

@fvasco @LouisCAD Don't worry gentlemen, I believe I use coroutines in the same way as you do :relaxed:, but that wasn't my point. Let's just disregard there is no .await() etc. and if I sum it up, then all I wanted to say is that: "the second approach is more concise/better than the first one". In other words I find it slightly better practice to switch the context to IO first and then use async {}, than to use the coroutineContext and then call e.g. async(IO) {}, that is all :relaxed:.

That is why there's CoroutineScope, you can set the default dispatcher of
the scope there

On Fri, Oct 19, 2018, 6:50 PM Antonín Brettšnajdr notifications@github.com
wrote:

@fvasco https://github.com/fvasco @LouisCAD
https://github.com/LouisCAD Don't worry gentlemen, I believe I use
coroutines in the same way as you do ☺️, but that wasn't my point. Let's
just disregard there is no .await() etc. and if I sum it up, then all I
wanted to say is that: "the second approach is more concise/better than the
first one". In other words I find it slightly better practice to switch the
context to IO first and then use async {}, than to use the
coroutineContext and then call e.g. async(IO) {}, that is all ☺️.


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/Kotlin/kotlinx.coroutines/issues/410#issuecomment-431426992,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AGpvBVgCFvxMtw8O4vYFxsEcd59SsyPSks5umgK6gaJpZM4U5eM1
.

@LouisCAD If you mean to let the whole class implement the CoroutineScope, then I've already explained why I don't want to use it in one of my previous comments here: https://github.com/Kotlin/kotlinx.coroutines/issues/410#issuecomment-427562363

TL;DR: I don't want to scroll all the way up to see which scope it is each time I open any method of that class :relaxed:, that is just horrifying. Plus I am not sure if newly spawned coroutines inside suspending functions of such class would be automatically cancellable - I haven't tried it, so I can't confirm how would the coroutines behave. Anyway when coroutineContext {} or the withContext(IO} { //async/launch etc... } combo is used, they cancel like a charm :relaxed: :heart:.

CoroutineScopes don't make anything non cancellable, unless you have the
NonCancellable object put in the coroutineContext.

On Fri, Oct 19, 2018, 8:06 PM Antonín Brettšnajdr notifications@github.com
wrote:

@LouisCAD https://github.com/LouisCAD If you mean to let the whole
class implement the CoroutineScope, then I've already explained why I don't
want to use it in one of my previous comments here: #410 (comment)
https://github.com/Kotlin/kotlinx.coroutines/issues/410#issuecomment-427562363

TL;DR: I don't want to scroll all the way up to see which scope it is each
time I open any method of that class ☺️, that is just horrifying. Plus I
am not sure if newly spawned coroutines inside suspending functions of such
class would be automatically cancellable - I haven't tried it, so I can't
confirm how would the coroutines behave. Anyway when coroutineContext {}
or the withContext(IO} { //async/launch etc... } combo is used, they
cancel like a charm ☺️ ❤️.


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/Kotlin/kotlinx.coroutines/issues/410#issuecomment-431449294,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AGpvBWBtg-u_eMwccTro2y_BKu13M7SUks5umhSSgaJpZM4U5eM1
.

Was this page helpful?
0 / 5 - 0 ratings