Noob question about coroutine default context


#1

I’m curious about interface of functions launch and async, both require a CoroutineContext as first parameter.

Coroutine context is important, but I don’t understand why it must be declared explicitly and what is is the consequences to choose an default one (like EmptyContext).

Thank you.


#2

Different contexts are needed in different practical situations. We have not figured out a good default yet, so you have to spacify it explicitly for now. In particular, EmptyContext does not seem to be a good default.


#3

Thanks for response,
I supposed that.

Unfortunately my poor experience doesn’t help me to figure some examples,nor I understand why runBlocking can have a good default context and launch cannot have one.

My question remains open: why launch should not use an EmptyContext as default or produce should not use Unconfined?


#4

A proper context for coroutines shall have an access to a thread or threads to run the code on. runBlocking has a thread that it blocks and it uses the thread it was invoked on to execute coroutines in its context.

Both EmptyContext and Unconfined are essentially the same, the later’s only difference is that it is a part of kotlinx.coroutines and provides support for its coroutine debugging features. They don’t have any thread and they execute coroutines synchronously in the same thread that posted the corresponding event that the suspended coroutine was waiting for. They are not appropriate for general-purpose code and should be used only as performance optimisation for cases where it matters and where author of the code perfectly understands what’s going on.

Working in Unconfined context is very similar to the default behaviour of Rx, where long-running code in consumer blocks the producer’s thread and all the other consumers. Guide to reactive streams with coroutines has more details on that. In short, using Unconfined context for coroutines breaks the illusion that “coroutines are like light-weight threads”, thus making it inappropriate for general-purpose CSP and actor-model programming styles.


#5

Great response, thank you for your support.

Just another question: is it reasonable apply a default behaviour to produce function to make it coherent to buildSequence/buildIterator functions?
Using produce like a rendezvous between two coroutine has a dedicated thread, same as runBlocking function.
In other words: produce {...} with default context and default capacity should act as a buildChannel function.


#6

Using produce with Unconfined context is indeed quite simiar as to how buildIterator operates with the only significat difference for our discussion is that buildIterator is more lazy than produce (produce computes the element before receive is invoked, while buildIterator computes the element only when next is invoked). But if you need a synchronous iterator, then why whould you use produce in the first place?

So, they key conceptual difference is that produce is designed for asynchronous coroutines (CSP, actor-model, Go-style) and giving it a default behavoir that is synchronous does not look like an appropriate choice.

The plan is to actually implement some kind of DefaultContext (mutli-threaded, asynchronous) that is going to be better suited for this kind of workload than CommonPool (which is designed and optimized for fork-join workloads and does not work so well for general ones). This kind of the context might become an appropriate default for all asynchronous coroutine builders.


#7

Yes, it’s right.
produce starts eager, buildSequence starts lazy.
Switch produce start rule to CoroutineStart.LAZY make interface instable.

For interface constraints, like async and future.
Moreover write Iterator.toChannel() function using produce looks as good starting point (but this consideration isn’t related to this issue).

Looks interesting.
Write a good default looks difficult but write an acceptable one can make coroutines easier and less painful.

Thanks again.


#8

You definitely need produce if you are using other suspending functions like IO, delays, etc. All of the asynchronous coroutine builders (launch, async, produce, actor) are designed to behave asynchronously to the calling code. You should be using them with CommonPool or some other kind of thread-pool. You can find a good writeup on sync vs async in this blog post: https://blog.ometer.com/2011/07/24/callbacks-synchronous-and-asynchronous/ It explains why you should always stick with either fully sync or full async and this is why it is a bad idea to use async coroutine builders with a sync context like Unconfined.