This is a rather typical problem encountered when using blocking operations with coroutines.
In this answer I attempt to provide a thorough yet succinct clarification on coroutines' cancellation, warn about a potential misconception when using external libraries, and in the end suggest better (alternative) ways of cancelling a scope (or a job).
If you want just the solution, skip to the solution paragraph.
Blocking (or "classical") functions are not related in any way to coroutines' cancellation mechanisms. They do not contain suspension points inside them.
The important warning
Be wary: should you use libraries like Room or Retrofit, they by design offer suspend
alternatives to their usual methods, which are implemented in a cancellable way.
Just adding suspend
modifier to the calls of methods of those libraries will work because there are both blocking and suspend
methods implemented for the same operations.
These methods from Room library would both be valid, but different implementations would be generated, the first one would be suspending, while the other one would be not:
@Query("SELECT * FROM users")
suspend fun getAllUsers(): List<User>
@Query("SELECT * FROM users")
fun getAllUsers(): List<User>
The Room library, just like many others, uses annotations for code generation.
If the annotation is placed on a method with suspend
modifier, it creates a suspending method, if there is no suspend
modifier, it creates a blocking method. You can see this in the generated code in your project.
But when you write your own code, you have to take care of transforming blocking functions into suspending yourself.
Solution
When there is only a loop of blocking functions, or just several blocking functions in sequence, there are no suspension points where the function could check for cancellation.
The coroutine is cancelled, but still there is the blocking code that goes on and on and never checks if it should actually stop. It knows nothing about coroutines at all and genuinely does not care.
Create suspension points between the blocking function calls. If you call another suspend function, it automatically introduces a suspension point.
Use coroutineContext.ensureActive
, coroutineContext.isActive
(or suspend fun yield()
, but this one should only be used for unimportant operations that could be suspended in order to release resources for other coroutines) to explicitly check for cancellation.
Any suspend
function implicitly is passed a coroutineContext
which you can access inside any suspend function.
suspend fun doSomething(){
if(!coroutineContext.isActive) {
//cleanup resources first, ensure nothing explodes if you cancel suddenly
doSomeCleanup()
throw CancellationException("Oh no, I got cancelled...")
}
//or
coroutineContext.ensureActive()
//this will check if the context was cancelled and immediately throw
//CancellationException (with no accompanying message)
}
You can read more about turning non-cancellable code into cancellable in the official Kotlin Documentation here.
Important Notes
Do not use CoroutineScope.cancel()
if you are planning to reuse the scope.
A CoroutineScope
is always created with reference to a CoroutineContext
,
if there is no Job()
added to that context manually, like in val scope = CoroutineScope(Dispatchers.IO)
, then it will be added automatically, by design:
/**
* Creates a [CoroutineScope] that wraps the given coroutine [context].
*
* If the given [context] does not contain a [Job] element, then a default `Job()` is created.
* This way, failure of any child coroutine in this scope or [cancellation][CoroutineScope.cancel] of the scope itself
* cancels all the scope's children, just like inside [coroutineScope] block.
*/
@Suppress("FunctionName")
public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
ContextScope(if (context[Job] != null) context else context + Job())
So there is always a Job
instance in the scope (unless it is GlobalScope, which is mostly undesired because you can't control coroutines' lifecycles inside it). It is the Job
(or SupervisorJob
) that is responsible for controlling lifecycle of coroutines in the scope, including cancellation. When the job is cancelled, it can not be started again. You can say, it dies.
When the scope is cancelled, if it has a Job in it, the Job will be cancelled, and the scope will practically become useless.
Sometimes it is OK to use scope.cancel()
or Job.cancel()
when it should be discarded, for example in the onCleared()
method of a viewModel.
Otherwise, there is a better method in the coroutines library.
/**
* Cancels all children of the [Job] in this context, without touching the state of this job itself
* with an optional cancellation cause. See [Job.cancel].
* It does not do anything if there is no job in the context or it has no children.
*/
public fun CoroutineContext.cancelChildren(cause: CancellationException? = null) {
this[Job]?.children?.forEach { it.cancel(cause) }
}
So, you could use, for a scope,
scope.coroutineContext.cancelChildren()
or, for a job,
job.cancelChildren()
Feel free to edit any mistakes in the post, if any, or suggest corrections in the comments.
isActive
at each iteration. – Frowstjava.net.URLConnection
. – TempliaKotlin or Java developers never invented a way to stop a Coroutine or a Thread
-- it's the opposite: they invented it a long time ago, only to realize (also a long time ago) that it's a fundamentally broken idea. You can safely kill a process thanks to process isolation and the OS doing all the cleanup automatically; you can't do the same for a thread, which shares the same process with others. – Templia