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:
|
|
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 coroutineyield()
.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 theisActive
flag above and throwsCancellationException
if it’s false.- Coroutine
yield()
not only callsensureActive()
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.