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

7

22

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

Carboni answered 8/9, 2021 at 6:8 Comment(9)
posted similar question, without answer...Zebrass
I guess no one knows the real solution for this or may be Kotlin or Java developers never invented a way to stop a Coroutine or a Thread. They call it cooperative cancellation. There should be a proper way to stop everything beneath a specific scope as we call cancel other wise the sense of canceling is dead.Carboni
Have you read the documentation? kotlinlang.org/docs/cancellation-and-timeouts.html Cancellation is cooperative, so in your loop example it would be your responsibility to check isActive at each iteration.Frowst
There are many problems with this solution. I can't check isActive if I'm downloading something. Moreover if the work is done somewhere else using a callback function or interface outside this class but running in this scope. This function may be not suspended and doing some file reading etc. Then how to check isActive for each line? or for each word i read? How can we cancel the whole computation block?Carboni
This is a technical limitation. We can't force an actively executing code to stop. We can only signal it, but it has to stop itself. This is the same for both threads and for coroutines (which use threads internally).Hainan
As @Hainan mentioned, a thread is not stopped. When it finish its work (reaching last execution line) it is stops and exits. This is true for threads in general, not only in Android.Gluteus
Also, this is not really new in coroutines and/or Kotlin. Even in Java, if you use a library for networking, file IO or for other long-running, background tasks and this lib is well designed then it probably provides you with a way to cancel its tasks. It is not true that you can't cancel downloading of an image - you just need to use tools that support it. If you use libraries that don't provide any way to cancel downloading then I say they are of bad quality.Hainan
First and foremost, you have to specify what IO library you're using for people to be able to help you. For example, there is a (hacky) way to cancel even the old-school java.net.URLConnection.Templia
Kotlin 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
W
19

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()
}
Weekly answered 8/9, 2021 at 6:50 Comment(0)
E
6

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

Enlace answered 8/9, 2021 at 6:50 Comment(2)
what approach should be taken now that isActive is deprecated?Alard
@Alard yield(), see AIMIN PAN answer.Lotson
S
0

Try to cancel the Context:

this.coroutineContext.cancel()
Shinbone answered 28/6, 2022 at 11:21 Comment(1)
Nothing will happen in this way too. It will just cancel only the next blocking call. The current blocking call which is running will not be canceled unless it get some result (Success or Fail).Carboni
S
0

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.

Sawfly answered 24/10, 2023 at 12:57 Comment(0)
S
0

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
    }
}
Spalding answered 2/11, 2023 at 8:58 Comment(0)
S
0

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

Sackman answered 15/1 at 11:13 Comment(0)
S
0

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.

Samora answered 3/3 at 15:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.