I was pretty surprised that a coroutine scope won’...
# coroutines
a
I was pretty surprised that a coroutine scope won’t complete if there are any unstarted coroutines in it. This function will never return for example:
Copy code
coroutineScope {
  launch(start = LAZY) {}
}
b
Yep, the definition is simple
won't return until all children jobs complete
but there’s additional caveats that simple definition doesn’t spell out. You would need to do something with that lazy job so it knows what to do about being no longer lazy
a
Well what stumped me is that it wasn’t started, I would have expected the scope to complete when all started coroutines finish
b
So your expected behavior would be for it cancel all unstarted jobs?
a
Well it wouldn’t need to since it never started
I’m not saying it’s not doing a logical thing. Just saying it wasn’t what I expected (and i even had a bug regarding it)
b
But the definition isn’t
won't return until all activated jobs have completed
. I think it’s a bug to create a job and never do anything with it. Structured concurrency IMO means that because it’s structured, you have expected behavior without assumptions (including to cancel jobs that have not yet been started, because the job was created for some reason).
I’m not saying it’s not doing a logical thing
Yeah, I definitely agree
There’s additional caveats that simple definition doesn’t spell out
But I also have never had an instance where I tried to create a lazy job within such a confined scope via
coroutineScope { ... }
to cancel jobs that have not yet been started
Copy code
suspend fun CoroutineScope.cancelUnstartedJobs() {
    children.filter { 
        !it.isCancelled && !it.isActive && !it.isCompleted 
    }.forEach {
        it.cancelAndJoin()
    }
}
a
I’ll show an example of what I was doing later
l
You need to start that lazy job at some point.
Or just not make it lazy.
a
Copy code
val progressShown = launch(start = CoroutineStart.LAZY) { delay(1000) }

val showProgress = launch {
    delay(1000)
    view.showImportProgress()
    progressShown.start()
}

val hideProgress = launch(start = CoroutineStart.LAZY) {
    if (showProgress.isCompleted) {
        try {
            progressShown.join()
        } finally {
            view.hideImportProgress()
        }
    } else {
        showProgress.cancel()
        // I had forgotten this so the coroutine never completed
        progressShown.cancel()
    }
}

// Do long running task
hideProgress.start()
hideProgress.join()
The goal is to only show the progress if it takes > 1s and when shown show it for at least 1s
g
@ansman this confused me too. I wrote a whole bunch of tests up around
under cercomstances XYZ my library should hold parent scope open
Ultimately I was thinking about trying to attach some kind of listener to the completion of the parent to cancel my unstarted/long-running jobs when the parent finishes its scope block, but decided that behaviour would probably be even more confusing.
I think as debugging tools get better things will get better. I'd really like to see something like running
jstack
and/or intelliJ's own "pause" on a thread pointing at a
runBlocking
event-loop have the resulting exception contain all the jobs the
runBlocking
loop is waiting on.
a
Maybe @elizarov has some good input on this