-
Notifications
You must be signed in to change notification settings - Fork 1.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Change default for global vs child coroutines by scoping coroutine builder (structured concurrency) #410
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. (3) In this proposal and "A surprise benefit: removing go statements enables new features" the (4) Should the sentence
changed to
otherwise we cannot run (5) The code (6) Should the builder (7) Is the builder (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 No, the answer to (8) is No, it create a inner scope and this acts as a barrier for |
@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 (3) We are still struggling how to present the differences between the
Using (4) Good point. Indeed, we "have to" (5) Redefinition of (6) Indeed, a (7)
(8) You can freely nest |
UPDATE: I've added "Additional goodies" to the top message with some additional explanations of the proposed design. |
Hi @elizarov, For my point of view Seeing the your example, as related consideration, I consider misleading the |
@fvasco Ah. Here is confusion of
Yes. It looks like we should deprecate |
I am not alone :) Sorry, I mean |
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 This doesn't need to add boilerplate to the |
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 ( Here an example for Existing function is renamed to 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 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 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. What do you think about it ? |
@pull-vert We've considered an option to duplication every builder However, we had decided that that is going to be too much duplication. Another disadvantage of this The code can be in the middle of some deep call hierarchy inside In the design that we are currently considering, we don't have any duplication. You can use |
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 A small exemple to see if I understood correctly :-) 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 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 ! fun main(args: Array<String>) {
globalScope { // this: CoroutineScope
launch { // extension of CoroutineScope
....
}
}
} |
|
I agree that One minor thought is that I'm not in love with the name The |
@ScottPierce The entry point is likely to be |
@LouisCAD Not sure I agree with that. For the JVM, I assume you reference For Android, I create coroutines all the time in my client API layer in places outside of a |
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 ***@***.***> 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
<#410 (comment)>,
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 |
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 |
We plan to have this done before |
@GeoffreyMetais Hmh, maybe you're right :-). Speaking of TornadoFx apps, if all View classes had |
It seems I encountered another caveat: override val coroutineContext = Dispatchers.JavaFx
launch {
launch(Dispatchers.IO) { Browser.open(it) }
} The 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 ***@***.***> 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
<#410 (comment)>,
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 😁). 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 parentScope.run {
async(IO) { ... }
} where now the |
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 😁) I migrated one of my smaller project (22 kt files, ~1.2K LOC) to the new structured coroutines and here is my overall feedback:
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:
Can you show a piece of code where you have to do that? Have you tried to use |
@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 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 |
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 ***@***.***> 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
<#410 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AGpvBbxcyu88bqiWRlXM5wFe0ibtTZT-ks5uiRHkgaJpZM4U5eM1>
.
|
@LouisCAD Thanks, found it 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 |
Btw the above implies that one of my own custom functions is not needed anymore 🤔 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 |
The evolution continues. I got rid of my previous 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 |
@LittleLightCz consider the differences between |
@fvasco Well, since for Before: suspend fun doSomething() = coroutineContext {
.
.
.
asyncIO { ... }
} After: suspend fun doSomething() = offload {
.
.
.
async { ... }
}
|
@LittleLightCz the two coroutines are different different, these use different dispatchers. |
@LittleLightCz If you told me you understood the purpose of the |
@fvasco @LouisCAD Don't worry gentlemen, I believe I use coroutines in the same way as you do |
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 ***@***.***> 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
|
@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) 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 |
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 ***@***.***> 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)
<#410 (comment)>
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
|
Background and definitions
Currently coroutine builders like
launch { ... }
andasync { ... }
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 invokingjoin
/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) { ... }
andasync(coroutineContext) { ... }
orasync(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
orparent=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):
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
toasync(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 ofloadAndCombineImage
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 onCoroutineScope
interface instead. A dedicated top-levelGlobalScope
instance ofCoroutineScope
is going to be defined.Starting a global coroutine would become more explicit and lengthly, like
GlobalScope.async { ... }
andGlobalScope.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 { ... }
orlaunch { ... }
whenCoroutineScope
is in scope (pun intended) would do it. In particular, it means that the following slide-ware code would not need to usejoin
anymore: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: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:
The proposal is to simply this pattern, by allowing an easy implementation of
CoroutineScope
interface by any business entities like the above activity:Now we don't need to remember to specify the proper context when using
launch
anywhere in the body ofMyActivity
class and all launched coroutines will get cancelled when lifecycle ofMyActivity
terminates.The text was updated successfully, but these errors were encountered: