Asynchronous Programming in Kotlin

When I began exploring Kotlin, one of my first questions was whether it used async-await keywords like some other languages. It took me some time to grasp that Kotlin’s approach to asynchronous programming is distinct from those found in JavaScript, Python, or C#.

Asynchronous Programming in Other Languages

  • JavaScript: Relies on Promises, whether using async-await or chaining them explicitly. Async functions return Promises, and awaiting a Promise pauses execution until it resolves, effectively transforming the function into a state machine.

  • C#: Utilizes Tasks. Async methods return a Task, signifying an asynchronous operation.

  • Python: Employs awaitables (Tasks, Futures, Coroutine objects). Async functions (coroutines) yield Futures to the event loop when performing asynchronous tasks. You can find a breakdown of my understanding of Python’s approach here: last year.

Kotlin’s Approach: Suspendable Functions and Continuations

In Kotlin, asynchronous programming centers around suspendable functions (declared with the suspend keyword) and the Continuation Passing Style (CSP). Suspendable functions, also used for generators, allow suspension at various points (suspension points) when calling other suspend functions. Upon completion of the invoked function, the caller resumes from the suspension point. Each suspend function operates like a state machine.

Unlike Promises/Futures, suspend functions receive Continuations. Think of a Continuation as a sophisticated callback, holding the function to be invoked, its state (variable values), and the resumption point within its state machine.

This excellent article clarifies:

Each suspendable lambda is compiled to a continuation class, with fields representing its local variables, and an integer field for current state in the state machine. Suspension point is where such lambda can suspend: either a suspending function call or suspendCoroutineUninterceptedOrReturn intrinsic call.

Essentially, each suspendable function has a corresponding Continuation class containing its code, variables, and suspension point (represented as a label in the function’s switch-case state machine). When a suspendable function f1 calls another suspendable function f2, it passes its Continuation object. f2 then references f1’s Continuation. This forms a chain: if f2 calls f3, the chain becomes f3 -> f2 -> f1. Resources like this article and this one (here) delve deeper into this concept.

It’s interesting to note that this Continuation chain is built in reverse compared to Promise chains in JavaScript (or C# or Python). In those languages, attaching a function to a Promise with .then() defines the next step in the chain.

Coroutines: The Context for Suspendable Functions

Beyond suspend functions and Continuations, coroutines are fundamental to Kotlin’s asynchronous model. A chain of suspend function calls executes within a coroutine. Suspend functions can only be invoked from other suspend functions or a Coroutine.

here describes a coroutine as:

A coroutine is an instance of a suspendable computation. It is conceptually similar to a thread, in the sense that it takes a block of code to run that works concurrently with the rest of the code. However, a coroutine is not bound to any particular thread. It may suspend its execution in one thread and resume in another one.

We don’t create coroutines (instances of classes inheriting from AbstractCoroutine) directly. Instead, we use CoroutineBuilder functions. Examples like launch or runBlocking are extension methods of the CoroutineScope interface. The documentation on coroutine builders at source code provides a deeper dive, showing how a CoroutineBuilder sets up a CoroutineContext that’s passed to the Coroutine constructor before invoking Coroutine.start().

CoroutineScopes and CoroutineContexts

here offers a clear explanation of CoroutineScopes and CoroutineContexts: a CoroutineScope holds a CoroutineContext, which contains various elements, most notably the coroutine’s Job and Dispatcher. The Dispatcher determines the thread(s) used to execute the coroutine’s suspend functions. Continuations have a reference to their Context. I came across this insightful note: “When resuming, continuations don’t invoke their functions directly, but they ask the dispatcher to do it. The Dispatcher is referenced from a CoroutineContext, that is referenced from the Continuation.”

Another worthwhile read on coroutines is available at this one.

Key Differences in Usage: Continuations vs. Promises/Tasks/Futures

A significant difference stems from the nature of Continuations versus Promises/Tasks/Futures. In JavaScript, when calling an async function, you explicitly indicate suspension until the Promise resolves using the await keyword. Kotlin doesn’t require this explicit signal; any call to a suspend function inherently suspends the caller (if the called function performs an asynchronous operation).

This might be because you can only pass your Continuation to the suspend function at the point of invocation, whereas Promises allow you to await the result immediately or later. However, this can make it harder to determine at a glance whether a suspend function suspends execution when it calls other functions, requiring you to check if those functions are “normal” or suspend functions. While an “await” keyword for suspend functions might feel redundant, it could enhance code clarity.

Kotlin’s async() and await() Functions

Interestingly, despite not needing async-await keywords for asynchronous code, Kotlin provides async() and await() functions for specific scenarios. async() (async()) is a coroutine builder that, unlike launch(), returns a Deferred (akin to a Promise/Future) that completes with the corresponding coroutine. Deferred, in turn, has an await() suspend method (await). You can find an example combining these functions at here.

If you need to call a suspend function without suspending the current function (similar to invoking an async function without await in JavaScript), you can wrap the call within a block and pass it to async().

A Linguistic Perspective on Asynchronous Operations

While reading about asynchronous programming in Raku (Raku), a descendant of Perl, I discovered they also use Promise (Promises), but with keep() and break() instead of resolve() and reject(). This resonated with me, as it aligns with how we naturally talk about promises: we keep or break them, not resolve or reject them.

Licensed under CC BY-NC-SA 4.0
Last updated on Dec 18, 2022 18:08 +0100