1
fun main() = runBlocking {

    var i = 1

    var job = launch (Dispatchers.Default){

        println("Thread name  =  ${Thread.currentThread().name}")
        while (i < 10) { // replace i < 10 to isActive to make coroutine cancellable
            delay(500L)
//            runBlocking { delay(500L) }
            println("$isActive ${i++}")
        }
    }

    println("Thread name  =  ${Thread.currentThread().name}")
    delay(2000L) // 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.")

}

output

Thread name  =  main
Thread name  =  DefaultDispatcher-worker-1
true 1
true 2
true 3
main: I'm tired of waiting!
main: Now I can quit.

if i use runBlocking { delay(500L) } then the above co-routine is not cancelable. So, it will print all values upto 9.

but when i use delay(500L) automatically the co-routine can be cancelled. Why ?

Vikas Acharya
  • 3,550
  • 4
  • 19
  • 52
  • Delay doesn't make coroutine cancellable, it is cancellable by default. You can make a coroutine non-cancellable by `launch (Dispatchers.Default + NonCancellable) {}`, while runBlocking blocks the thread so cancellation message wouldn't even have reached to the coroutine, so is not cancelled. It is not preferred way to block a thread in coroutines and use of runBlocking is discouraged in production. – Animesh Sahu May 02 '20 at 09:32

3 Answers3

6

delay doesn't actually do anything on its own, it just schedules the coroutine to be resumed at a later point in time. Continuations can, of course, be cancelled at any time.

runBlocking, on the other hand, actually blocks a thread (which is why the compiler will complain about a blocking operation within a coroutine, and why you should never use runBlocking outside of e.g. unit tests).

Note: Since main can now be a suspending function, there's generally no need to use it whatsoever in your core application code.

This function should not be used from a coroutine
runBlocking

Coroutines, like threads, are not truly interruptible; they have to rely on cooperative cancellation (see also why stopping threads is bad). This means that cancellation doesn't actually do anything either; when you cancel a context, it simply notifies all of its child contexts to cancel themselves, but any code that is still running will continue to run until a cancellation check is reached.

It is important to realize that runBlocking is not a suspend function. It cannot be paused, resumed, or cancelled. The parent context is not passed to it by default (it receives an EmptyCoroutineContext), so the coroutine used for the execution of runBlocking won't react to anything that happens upstream.

When you write

while (i < 10) {
    runBlocking { delay(500L) }
    println("$isActive ${i++}")
}

there are no operations here that are cancellable. Therefore, the code never checks whether its context has been cancelled, so it will continue until it finishes.

delay, however, is cancellable; as soon as its parent context is cancelled, it resumes immediately and throws an exception (i.e., it stops.)

Take a look at the generated code:

@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
    switch (this.label) {
    case 0:
        while (i.element < 10) {
            BuildersKt.runBlocking$default( ... );
            ...
            System.out.println(var3);
        }

        return Unit.INSTANCE;
    default:
        throw new IllegalStateException( ... );
    }
}

Contrast this

while (i.element < 10) {
    BuildersKt.runBlocking$default( ... );
    ...
    System.out.println(var3);
}

with

do {
    ...
    System.out.println(var3);
    if (i.element >= 10) {
        return Unit.INSTANCE;
    }
    ...
} while (DelayKt.delay(500L, this) != var5);

Variable declarations and arguments omitted (...) for brevity.


runBlocking will terminate early if the current thread is interrupted, but again this is the exact same cooperative mechanism, except that it operates at the level of the thread rather than on a coroutine.

Salem
  • 13,516
  • 4
  • 51
  • 70
2

The official documentation states:

All the suspending functions in kotlinx.coroutines are cancellable.

and delay is one of them.

You can check that here.

I think the real question should be: Why a nested runBlocking is not cancellable? at least an attempt to create a new coroutine with runBlocking when isActive is false should fail, altough making a coroutine cooperative is your responsability. Besides runBlocking shouldn't be used in the first place.

Turns out if you pass this.coroutineContext as CoroutineContext to runBlocking, it gets cancelled:

fun main() = runBlocking {

    var i = 1

    var job = launch (Dispatchers.Default){

        println("Thread name  =  ${Thread.currentThread().name}")
        while (i < 10) { // replace i < 10 to isActive to make coroutine cancellable
            runBlocking(this.coroutineContext) { delay(500L) }
            println("$isActive ${i++}")
        }
    }

    println("Thread name  =  ${Thread.currentThread().name}")
    delay(2000L) // 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.")

}
Glenn Sandoval
  • 3,455
  • 1
  • 14
  • 22
1

I modified your code a little

try {
    while (i < 10) { // replace i < 10 to isActive to make coroutine cancellable
        delay(500L)
        println("$isActive ${i++}")
    }
} catch (e : Exception){
    println("Exception $e")
    if (e is CancellationException) throw e
}

the output

Thread name  =  main
Thread name  =  DefaultDispatcher-worker-1
true 1
true 2
true 3
main: I'm tired of waiting!
Exception kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@40bcb892
main: Now I can quit.

can you see the exception StandaloneCoroutine was cancelled its because,

  • If the Job of the current coroutine is cancelled or completed while this suspending function is waiting i.e delay(500L), this function immediately resumes with CancellationException.

  • So, the point is if you add a suspending function inside your launch it can be cancellable.

you can try that with user defined suspend fun also

Animesh Sahu
  • 7,445
  • 2
  • 21
  • 49
Vikas Acharya
  • 3,550
  • 4
  • 19
  • 52
  • 1
    I think this answer is practical and easy for understanding especially for beginners like me. – Vikas Acharya May 02 '20 at 09:37
  • 1
    Its a bad practise to stop the propagation of CancellationException. After catching the exception be sure to rethrow it. – Animesh Sahu May 02 '20 at 09:41
  • 1
    @AnimeshSahu what do mean by rethrowing it ? – Vikas Acharya May 02 '20 at 09:46
  • 1
    @naanu There is a detailed info about the [Structured Concurrency](https://medium.com/@elizarov/structured-concurrency-722d765aa952), you should follow the rules, a CancellationException is different from any another type of Exception it won't make your app fail, it just keeps track of lifecycle of the coroutines. It should be propagated till the top-level coroutine receives it. – Animesh Sahu May 02 '20 at 09:49
  • 1
    @naanu `CancellationException` should never be ignored because it breaks [structured concurrency](https://kotlinlang.org/docs/reference/coroutines/basics.html#structured-concurrency). It should always be rethrown (`throw e` inside `catch`) so it can be handled by the parent context. – Salem May 02 '20 at 09:50
  • 1
    @Moira I tried and didn't found any difference. it's working with or without `throw e` perfectly. – Vikas Acharya May 02 '20 at 09:59
  • 1
    @naanu You're only using a single job. Take a look at the docs linked by myself and @Animesh; if you have a hierarchy of jobs and ignore the failure of a child job, the parent job won't get cancelled. It's generally good practice to do this so that when you actually need it, you won't forget about it. – Salem May 02 '20 at 10:02
  • @naanu i just tried to print and show the exception for your understanding. how to handle that exception is out of scope of this question. add `throw e` after the print statement if you want. and to explain that concept, you must create so many jobs and workaround. try to follow some videos on udemy or youtube. Frankly, Moira has answered perfectly, just because of a modified program and its output you marked my answer. Thank you – Vikas Acharya May 02 '20 at 10:10