Kotlin Coroutines Cancellation

Kotlin coroutines provide a robust cancellation mechanism that differs significantly from forcefully terminating a process. Coroutines actively participate in their own cancellation by verifying if a request has been made.

When a coroutine is initiated using launch() or async(), it produces a Job object (Deferred inherits from Job) that can be canceled by calling its cancel() method. This action triggers the Job to enter a cancellation state.

Every coroutine possesses a CoroutineContext, which holds a reference to its Job. Suspend functions can access their context through the coroutineContext property. While seemingly magical, this property’s implementation is intrinsically tied to the compiler.

Examining the compiled bytecode of a Kotlin suspend function using this property reveals its inner workings. The generated Java code, ((Continuation)$continuation).getContext()), showcases how each suspend function is linked with a continuation object. This continuation object references the coroutine’s CoroutineContext, granting access to the Job and cancellation status.

Unlike Kotlin’s coroutines, JavaScript promises lack inherent cancellation support, although libraries like bluebird offer alternative implementations. The contrasting approaches to building “async call stacks”—outwards for Promises and inwards for Continuations—highlight why cancellation is more straightforward in Kotlin.

While coroutines must cooperate with the cancellation mechanism, this is often handled implicitly. The documentation emphasizes: “All the suspending functions in kotlinx.coroutines are cancellable. They check for cancellation of coroutine and throw CancellationException when cancelled.”

Consider a scenario involving CPU-bound operations within a coroutine dispatched to the ThreadPool using Dispatchers.Default. Here’s an example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
class Message(
    val header: String,
    val content: String,
    val footer: String,
    ) {}

// Example 1, as our suspend functions invoke delay, that is cancellable, our functions are also cancellable
suspend fun decryptHeader(txt: String): String {
    println("decryptHeader started")
    Thread.sleep(2000)
    return "[[$txt]]"
}

suspend fun decryptContent(txt: String): String {
    println("decryptContent started")
    Thread.sleep(2000)
    return "[[$txt]]"
}

suspend fun decryptFooter(txt: String): String {
    println("decryptFooter started")
    Thread.sleep(2000)
    return "[[$txt]]"
}

suspend fun decryptMessage(): String {
    val message = Message("Title", "Main", "notes")
    println("decryptMessage started")
    val header = decryptHeader(message.header)
    println("header obtained: $header")
    coroutineContext.ensureActive()

    val content = decryptContent(message.content)
    println("content obtained: $content")
    coroutineContext.ensureActive()

    val footer = decryptFooter(message.footer)
    println("footer obtained: $footer")
    coroutineContext.ensureActive()

    return "$header - $content - $footer"
}

suspend fun cancelComputation(c1: Deferred) {
    val elapsed = measureTimeMillis {
        delay(1000)
        c1.cancel()
        println("after invoking cancel")
        val res = try {
            c1.await()
        } catch (ex: Exception) {
            ex.message
        }
        println("result: $res")
    }
    println("elapsed time: $elapsed")
}

fun runCancellableComputation() {
    println("started")
    runBlocking {
        // this runs in the eventloop
        val c1 = async (Dispatchers.Default) {
            // this runs in the ThreadPool
            decryptMessage()
        }
        cancelComputation(c1)

    }
    println("Finished, current thread: ${Thread.currentThread().name}")
}

/*
started
decryptMessage started
decryptHeader started
after cancelling
header obtained: [[Title]]
result: DeferredCoroutine was cancelled
elapsed time: 2023
Finished, current thread: main
*/

In this example, coroutineContext.ensureActive() efficiently checks for cancellation requests after each CPU-bound operation. While alternatives like yield() exist, they serve different purposes.

yield() is a cooperative mechanism that allows other coroutines in the same dispatcher to execute. While beneficial when numerous coroutines compete for resources, it might not be ideal when prioritizing the swift completion of a specific task.

A Stack Overflow discussion clarifies these concepts:

I would answer the question in the context of 4 related things:

  • Sequence yield(value: T) is totally unrelated to coroutine yield().
  • isActive is just a flag to identify if the coroutine is still active or cancelled. You can check this flag periodically and decide to stop the current coroutine or continue. Of course, normally, we only continue if it’s true. Otherwise, don’t run anything or throw an exception, e.g., CancellationException.
  • ensureActive() checks the isActive flag above and throws CancellationException if it’s false.
  • Coroutine yield() not only calls ensureActive() first, but then also politely tells other coroutines in the same dispatcher that: “Hey, you guys could go first, then I will continue.” The reason could be “My job is not so important at the moment.” or “I am sorry to block you guys for so long. I am not a selfish person, so it’s your turn now.” You can understand here exactly like this meaning in the dictionary: “yield (to somebody/something): to allow vehicles on a bigger road to go first.” SYNONYM: give way.

An alternative implementation could involve implicit cancellation checks after each suspend function call. As a suspension function concludes, the coroutine invokes continuation.resume (or resumeWith) to proceed. This resume() method could then incorporate the cancellation check.

Cancellation is Cooperative

its source code

means this

Ktor requests

ensureActive()

yield()

yield

SequenceScope.yield

a pretty nice explanation

Licensed under CC BY-NC-SA 4.0
Last updated on Apr 29, 2023 20:39 +0100