-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Description
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
or produce
it is preferable to cancel
a coroutine instead of join
.
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
val secret: String? = null
val userDeferred = async(start = CoroutineStart.LAZY) { loadUser() }
if (secret != null) {
require(secret == userDeferred.await().secret)
}
the strusctured concurrency impose the implicit join
of all children.
val secret: String? = null
val userDeferred = async(start = CoroutineStart.LAZY) { loadUser() }
try {
if (secret != null) {
require(secret == userDeferred.await().secret)
}
} finally {
userDeferred.join()
}
So the try-finally
is implicit, but unfortunately the first example hangs indefinitely, moreover we MUST write the code
val secret: String? = null
val userDeferred = async(start = CoroutineStart.LAZY) { loadUser() }
try {
if (secret != null) {
require(secret == userDeferred.await().secret)
}
} finally {
userDeferred.cancel()
}
Structured 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:
val secret: String? = null
val userDeferred = async(start = CoroutineStart.LAZY, onScopeTermination = ScopeTermination.CANCEL) { loadUser() }
try {
if (secret != null) {
require(secret == userDeferred.await().secret)
}
} finally {
userDeferred.join()
}
Alternatives
This issue was already solved for ReceiveChannel
using the consume
function, we can copy the same block for Deferred
.
inline fun <E, R> Deferred<E>.consume(block: (Deferred<E>) -> R): R {
var cause: Throwable? = null
try {
return block(this)
} catch (e: Throwable) {
cause = e
throw e
} finally {
@Suppress("DEPRECATION")
cancel(cause)
}
}
So the first example becomes:
val secret: String? = null
async(start = CoroutineStart.LAZY) { loadUser() }.consume { userDeferred ->
if (secret != null) {
require(secret == userDeferred.await().secret)
}
userDeferred.cancel()
}
We have to introduce a nested block, similar to use
function that mimic the try-with-resource
block.
Future improvements:
Introducing a new termination behavior allow us to define a similar API to avoid the nested block
fun CoroutineScope.cancelOnScopeTermination(job: Job) {
launch(start = CoroutineStart.LAZY, onScopeTermination = ScopeTermination.JOIN) {
job.cancel()
}
}
val secret: String? = null
val userDeferred = async(start = CoroutineStart.LAZY) { loadUser() }
cancelOnScopeTermination(userDeferred)
if (secret != null) {
require(secret == userDeferred.await().secret)
}
This requires an extra allocation and miss of exception handling, but I consider scope finalizers a future enhancement.
What do you think about?