-
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
Customizable coroutine behaviour on scope termination #1065
Comments
Analogously with structured programming, this library deals with common common pattern. Function invocation become
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
fun main() {
println("Start")
runBlocking {
try {
coroutineScope {
launch { TODO() }
}
} catch (e: Exception) {
e.printStackTrace()
} finally {
launch { println("Done!") }
}
}
println("End")
} |
Thanks for this write-up. This is an interesting use-case explaining the problem of scope termination for lazy coroutines. I don't like the solution with I actually think that we can simply adjust existing behavior of lazy coroutines on scope termination, that is make cancellation of non-started lazy coroutines on scope termination a default behavior and do not provide any other configuration. You've showed a good use-case for a combination of
|
The following code is production code, we have to get a value from a val value = channel.poll() ?: run {
val requestDeferred = async { requestValue() }
try {
select<Int> {
channel.onReceive { it }
requestDeferred.onAwait { it }
}
} finally {
requestDeferred.cancel()
}
} However paying some extra allocations it is possible to rewrite it using |
making it |
However, I see the problem here. You want to cancel your |
Is there any plan to change the scope termination behaviour for lazy coroutines? It seems counter intuitive that I would have to cancel an |
Are there any plans to implement this? I like this proposal. Right now I'm doing: supervisorScope {
try {
val data1 = this.async(start = CoroutineStart.LAZY) { getData1() }
val data2 = this.async(start = CoroutineStart.LAZY) { getData2() }
maybeUseData(data1, data2)
} finally {
this.coroutineContext.cancelChildren()
}
} But it would be nice if I can just do: supervisorScope {
val data1 = this.async(start = CoroutineStart.LAZY, onScopeTermination = ScopeTermination.CANCEL) { getData1() }
val data2 = this.async(start = CoroutineStart.LAZY, onScopeTermination = ScopeTermination.CANCEL) { getData2() }
maybeUseData(data1, data2)
} |
Another use case here is to collect first emitted value between multiple flows. As far as I know, this can only be done using suspend fun getFirstEmittedFromMultipleFlows() = coroutineScope {
val channelA = flowA.produceIn(this)
val channelB = flowB.produceIn(this)
val channelC = flowC.produceIn(this)
select {
channelA.onReceive {
...
}
channelB.onReceive {
...
}
channelC.onReceive {
...
}
}
} By default, above code will hang and wait until all three flows complete on their own. If I want it to return immediately after first channel receives, I have to manually wrap the code in try finally and call |
Maybe solution is to provide some way to create a "daemon" coroutines that would not block the scope, but would still get cancelled by it? Something similar to the |
Daemon coroutines is an option we are currently exploring indeed |
We have same case: launching a service while the scope is active. For instance: @Test
fun testWithService() = runBlocking {
launch {
startService()
}
// ... do stuff
} will always freeze the test. So we have to boilerplate it into |
@e5l with a It'd look like this: raceOf({
startService(); awaitCancellation()
}, {
theCode()
}) Or you can do this: val serviceJob = launch { startService() }
try {
theCode()
} finally {
serviceJob.cancel()
} Both approaches are better than using a different scope like GlobalScope. |
@e5l maybe you could use |
@dkhalanskyjb, we can, but it's not only for tests. We have the code in other cases, like for the reading coroutine in a socket. |
@qwwdfsad are daemon coroutines bound to specific CoroutineScope or how they would differ from We are stumbled on this issue while trying to implement batching suspend DataLoader, which must get all suspend One way to do this is to launch dispatch coroutine in /**
* Asynchronous interface for wrapping [DataLoader].
*/
interface SuspendDataLoader<K, V> {
suspend fun loadMany(keys: List<K>): List<V>
}
/**
* Creates [SuspendDataLoader], that batches load requests to DataLoader and emits dispatch with debounce.
*/
@OptIn(FlowPreview::class, DelicateCoroutinesApi::class)
fun <K, V> CoroutineScope.launchDataLoader(
timeout: Duration = 1.milliseconds,
dataLoaderOptions: DataLoaderOptions = DataLoaderOptions(),
batchLoader: suspend (List<K>) -> List<V>,
): SuspendDataLoader<K, V> {
val dispatchRequests = Channel<Unit>(Channel.UNLIMITED)
val dataLoader = DataLoader({ keys -> future { batchLoader(keys) } }, dataLoaderOptions)
// Launching batching coroutine in background, so current CoroutineScope will not block and wait for it
val daemonJob = GlobalScope.launch {
dispatchRequests.consumeAsFlow()
.debounce(timeout)
.collect {
dataLoader.dispatch()
}
}
// When current CoroutineScope is done - cancel daemonJob to avoid coroutines leak
coroutineContext.job.invokeOnCompletion {
daemonJob.cancel()
}
return object : SuspendDataLoader<K, V> {
override suspend fun loadMany(keys: List<K>): List<V> {
val values = dataLoader.loadMany(keys)
dispatchRequests.send(Unit)
return values.await()
}
}
} |
Currently this library allows to define how a coroutine have to start (using
start
parameter) and its dependency fail behavior (using jobs).According to structured concurrency it assumes that all sub-coroutines have to join on scope termination.
In my experience this decision does not fit all use case, for some
async
orproduce
it is preferable tocancel
a coroutine instead ofjoin
.So I wish to consider a parameter to customize the coroutine's behavior on scope termination.
This proposal does not conflict with structured concurrency.
In the example code
the strusctured concurrency impose the implicit
join
of all children.So the
try-finally
is implicit, but unfortunately the first example hangs indefinitely, moreover we MUST write the codeStructured concurrency should avoid to handle
join
manually.Proposal
My proposal is to consider a parameter to customize the behavior on scope termination, ie: default, join, cancel.
Ie:
Alternatives
This issue was already solved for
ReceiveChannel
using theconsume
function, we can copy the same block forDeferred
.So the first example becomes:
We have to introduce a nested block, similar to
use
function that mimic thetry-with-resource
block.Future improvements:
Introducing a new termination behavior allow us to define a similar API to avoid the nested block
This requires an extra allocation and miss of exception handling, but I consider scope finalizers a future enhancement.
What do you think about?
The text was updated successfully, but these errors were encountered: