Abdur Rahman
Abdur Rahman

Reputation: 1001

How To Stop Or Cancel A Kotlin Coroutine (Stop Currently Running Code Block Instantly)?

What I want to achieve?

I have a task to download images but as the screen scrolls it will cancel previous downloads and start downloading new ones. I want that when it cancel the coroutine downloading previous image it stop instantly and free up the bandwidth so new images download faster.

What I have tried?

I have tried multiple ways to stop the coroutine but it keeps going until it finishes the downloading even after cancelling the coroutine. When I cancel the coroutine it makes a variable isActive to false and stop calling further suspended function. But the problem is if its running a loop for 1000000 times or downloading an image from network this task will not canceled unless completed. Like loop will complete it's 1000000 iterations then the coroutine will be cancelled.

I have tried these but no success:

job.cancel()
scope.cancel()

I have tried so many ways for achieving this but got no solution. I can't use any library right now in my project.

This use case not achieved by Threads, Executor Service, Coroutines. Because all behave the same.

More questions same like this :

How do I cancel a kotlin coroutine for a blocking download operation

AsyncTask is not cancelling the long running operation in android

Leaking Service held by Coroutine

Upvotes: 23

Views: 43646

Answers (7)

Tim Korelov
Tim Korelov

Reputation: 350

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.

Upvotes: 2

vladd
vladd

Reputation: 51

You can use Job.ensureActive() inside coroutine to make it cooperative.

If the job is no longer active, throws CancellationException. If the job was cancelled, thrown exception contains the original cancellation cause. This function does not do anything if there is no Job in the scope's coroutineContext.

This approach allows you to have more control over coroutine cancellation, so you can properly handle Scope/Job being cancelled using

try {
  // code which could throw CancellationException
} catch (e: CancellationException) {
  // handle cancellation if needed
}

Also, here you can find an example of using it https://developer.android.com/kotlin/coroutines/coroutines-best-practices#coroutine-cancellable

Upvotes: -1

Ravindra-Ravi Verma
Ravindra-Ravi Verma

Reputation: 535

Coroutine can't be cancelled in between the execution, you have to add isActive check, in between your code.

val job = CoroutineScope(Dispatchers.Default).launch {
    try {
        while (isActive) {
            // Your coroutine code here
            // Check for cancellation and exit the loop if needed
        }
    } finally {
        // Cleanup or finalization code
    }
}

Upvotes: 0

Abdul Muqeet
Abdul Muqeet

Reputation: 128

I tried to call job.cancel() but it didn't work. But when I called job.cancel(null). It cancel the currently running job. You can pass null or CancellationException in parameters of cancel(cause:CancellationException? = null).

I don't know the real reason why it happens but it works with job.cancel(null). Even though, in both ways, we are calling the same function underneath.

Upvotes: 0

Vasily Bogdanov
Vasily Bogdanov

Reputation: 11

Try to cancel the Context:

this.coroutineContext.cancel()

Upvotes: 0

End User
End User

Reputation: 812

Coroutine cancellation is cooperative. A coroutine code has to cooperate to be cancellable. All the suspending functions in kotlinx.coroutines are cancellable. They check for cancellation of coroutine and throw CancellationException when cancelled. However, if a coroutine is working in a computation and does not check for cancellation, then it cannot be cancelled, like the following example shows:

val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
    var nextPrintTime = startTime
    var i = 0
    while (i < 5) { // computation loop, just wastes CPU
        // print a message twice a second
        if (System.currentTimeMillis() >= nextPrintTime) {
            println("job: I'm sleeping ${i++} ...")
            nextPrintTime += 500L
        }
    }
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancelAndJoin() // cancels the job and waits for its completion
println("main: Now I can quit.")

Run it to see that it continues to print "I'm sleeping" even after cancellation until the job completes by itself after five iterations

Making computation code cancellable

like the following example shows:

val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
    var nextPrintTime = startTime
    var i = 0
    while (isActive) { // cancellable computation loop
        // print a message twice a second
        if (System.currentTimeMillis() >= nextPrintTime) {
            println("job: I'm sleeping ${i++} ...")
            nextPrintTime += 500L
        }
    }
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancelAndJoin() // cancels the job and waits for its completion
println("main: Now I can quit.")

Refer to the official docs here

Upvotes: 6

AIMIN PAN
AIMIN PAN

Reputation: 1675

A kotlin coroutine must cooperate to allow cancellation. That means it has some check points calling one suspend function. This makes sense as some procedures are atomic and should not be stopped in the middle.

One example of bad coroutine that can not be cancelled:

    var job = launch {
        var time = System.currentTimeMillis()
        var i = 0
        while (i < 1000) {
            if (System.currentTimeMillis() >= time) {
                println("Loop number ${++i} ")
                time += 500
            }
        }
    }

To make it cancellable, you can add yield() at the begining of each iteration. Following is a cancellable coroutine:

coroutineScope {
    var job = launch {
        var time = System.currentTimeMillis()
        var i = 0
        while (i<1000) {
            yield()
            if (System.currentTimeMillis() >= time) {
                println("Loop number ${++i}")
                time += 500
            }
        }
    }
    // wait some time
    delay(1300)
    println("Stopping the coroutine....")
    job.cancel()
    job.join()
    // or call job.cancelAndJoin()
}

Upvotes: 20

Related Questions