hmm. I'm curious about the `Job` and parent-child'...
# coroutines
g
hmm. I'm curious about the
Job
and parent-child'ing.
Copy code
suspend fun doLongRunningWork {
  val myContext = coroutineContext
  requireNotNull(myContext[Job])

  val async1 = someScope.async { 
    //...
  }
  val async2 = someScope.async(myContext) {
    //...
  }

  //maybe this code joins on async1 & async2, maybe not. should that impact my approach?
}
the latter exerts a rather strange back-pressure on the callers coroutine-entrypoint. Why the distinction?
z
what do you mean it joins?
it’s async
so the
suspend fun doLongRunningWork()
will complete (resume) potentially before the two async coroutines complete
you would need to add
awaitAll(async1, async2)
to suspend
doLongRunningWork()
until they complete
g
I know. My question is more about the purpose of
myContext
What I want is a rationale for why you might want to launch a child async process in your context and why, idiomatically (by default) you don't.
I just found the docs for
coroutineScope {}
builder...
I guess I'm just confounded by the case where a child-job completes and a grand-child (or further descendant) job is still running, keeping your
runBlocking
from completing... although, does
async
complete in such a case? does
await
give you a value but
join()
suspend? I must write tests.
ahhhh this is the
Completing
state.
z
I guess I don’t understand
you are asking why you may want to launch a coroutine that launches 2 async tasks? The answer is parallelism
oh - your point is why you would launch it in a different scope?
launch async in a different scope?
g
yeah so the key point is
async1
is not a child of the Job executing
doLongRunningWork
and
async2
is a child of same. Why do we need to distinguish between the two?
well the former will let the job executing
doLongRunningWork
complete when its own block is complete. The latter will prevent the
Job
of
doLongRunningWork
from completing, pushing it into the
Completing
state, for as long as
async2
is running.
z
I guess I’m not following why you would want to do this though
g
I also don't understand your question. This code is strange by itself and you may have different behavior depending on where this doLongRunningWork will be called, because you just may break structured concurrency easily if myContext is not belongs to someScope, because you will replace Job from someScope I just not sure what exactly you want to achieve
I see no reason for such structured code to exist, you always never should launch coroutines to different scope from suspend function, it's just a side effect, it may be needed sometimes (launch one more background task. What is semantics what you trying to achieve
g
Its not trying to accomplish something I'm trying to understand the ramifications. As you say,
you [never should] launch coroutines to different scope from suspending function
in short, what im asking is: why? Consider your the designer of "TornadoUI" UI framework. Lets say you use an annotation processing system and you want to support coroutines. Should you idiomatically ask callers to write functions like
Copy code
@Tornado suspend fun onClick(e: ActionEvent)
or should you ask them to write
Copy code
@Tornado fun CoroutineScope.onClick(e: ActionEvent)
--or something else? If you do decide that "simplicity is most important", and so you want your users to write regular UI event handlers
@Tornado suspend fun handle(e: Event)
, what should you do if they call
launch(coroutineContext) { delay(9999)
. Do you refuse to dispatch further events to that handler? Should you throw an exception?
I guess I'm looking for a general interpretation of back-pressure that doesnt exist.
l
Fixed your code:
Copy code
suspend fun doLongRunningWork = coroutineScope {
  val async1 = async { 
    //...
  }
  val async2 = async {
    //...
  }
  ... // Probably await the async values, otherwise, use launch.
} // This code definitely joins async1 & async2 if you don't await them
g
@louiscad what are your suggestions for the UI framework?
g
Example above with onClick may have sanse in some cases (like suspend on click can be useful in some cases), but if you want integration with coroutines I would expose those events as Flow
what should you do if they call
launch(coroutineContext) { delay(9999)
This is indifferent to launch and scopes, user can just call delay(9999) And now, you shouldn't throw exception I don't see any problem in this case if you use proper event stream abstraction like Flow, which has different strategies for backpressure. With suspend function you always have only 2 choices: suspend until client is processing or launch new coroutine and suspend, as you mentioned above, with Flow you can also buffer and conflate
l
@groostav What does the
@Tornado
annotation means and does?
1
g
Its a binding annotation for your UI framework. Like a swing action listener or an fxml click listener or an android tap listener. You could just as easily expose an
operator fun EntryPointAPI.plusAssign(handler: your_concurrent_handler_type)
if you wanted a winforms style ui API. The thing I’m trying to get at is: what should the lambda type be? And what should the UI framework do if it returns but silently appended a child job to the context you gave it?
l
Here's the extension I use on Android (agnostic to binding if any) to wait for clicks in coroutines: https://github.com/LouisCAD/Splitties/blob/e4c940736b00154a828d74c7cc10252686432685/modules/views-coroutines/src/androidMain/kotlin/splitties/views/coroutines/VisibilityAndClicks.kt#L35-L51 The scope of these coroutines is bound to the lifecycle of the UI of course.