withContext
only checks for cancellation before it starts running its block. If the coroutine withContext
is called from is cancelled after it has started, it will continue suspending until it is finished.
I would like to execute code with a different context yet still be able to cancel it, while it is running, from the original coroutine.
I wrote a function for this:
suspend inline fun <T> withContextCancellable(context: CoroutineContext,
crossinline block: suspend CoroutineScope.() -> T): T {
val listener = coroutineContext[Job]?.invokeOnCompletion(onCancelling = true) { context.cancel() }
return try {
withContext(context) { block() }
} finally {
listener?.dispose()
coroutineContext[Job]?.let {
if (it.isCancelled) context.cancel()
}
}
}
However, I’m not too comfortable with using invokeOnCompletion
to accomplish this. While this API seems to be quite stable, it is nevertheless internal.
Is there a better/already implemented way to accomplish this?
This code appears to artificially make the “different context” Job
a child of the “original coroutine” Job
but only while some block is running. I have a hard time seeing a use case for this. Is it really exactly what you want?
I kinda doubt it, but if this really is what you want, then you are working outside of structured concurrency so, yeah, you have to set up and manage relationships manually.
I’m guessing/hoping that you really want an actual parent/child Job
relationship, though.
If the “different context” job is only for the withContext
block and is intended for cancelling the withContext
block early, then just defining it as a child of the “original coroutine” will work (val childJob = Job(parentJob). If this is the case, though, I’d recommend using async
which creates the child job for you (Deferred extend Job) instead of withContext
just since I think it’d be clearer and more idiomatic.
If your “different context” Job
and “original coroutine” Job
are each used for managing groups of coroutines, and, in general, want “different context” Job to cancel when “original coroutine” is cancelled or fails, then also just define “different context” Job
as a child of “original coroutine” Job
. Any code using a child Job
(even a withContext
block) will cancel when the parent Job
is cancelled or fails.
If you just want the withContext
code block to cancel when “original coroutine” cancels don’t need to cancel it otherwise, (your goal is just to switch Dispatchers or something), then don’t pass a context with a Job into withContext
at all, just use the part of the context (like Dispatcher) that you care about. In that case the withContext
block will run as part of an internally created child Job so it will work as desired.
Do any of these situations actually match your needs? If not, then I’m curious, what is your use case for needing withContextCancellable
?
Thanks for your suggestions. I really don’t know why I tried to maintain this architecture, it doesn’t make too much sense. I suppose it’s best to look for a better solution before starting to work on one
I think what you may need here is a check on the isActive
property of the coroutine scope in your withContext
block, as the coroutine scope should be the receiver of the block. I heard about this from the coroutines guide (“Cancellation is Cooperative”). From the isActive property documentation:
Returns true
when the current Job is still active (has not completed and was not cancelled yet).
Check this property in long-running computation loops to support cancellation.
Alternatively, calling yield()
will cause your computation to suspend at a given point, allowing other coroutines to run on the same thread. If the coroutine is cancelled in the meantime, yield()
will throw CancellationException
and stop your code in the withContext()
block.