suspend
is a callback
Understanding suspend
doesn’t really require getting hung up on what the magic “hang” means or how threads are switched. In fact, behind suspend
is a very familiar callback.
Suppose postItem
consists of three asynchronous subtasks with dependencies: requestToken
, createPost
and processPost
, all of which are callback-based APIs.
|
|
As you can see callback-based APIs can easily cause a lot of indentation. APIs such as Promise (Future) and RxJava, which is popular in the Android community, eliminate the problem of nesting to some extent by chaining calls. For example, if the above example is implemented in RxJava.
However, RxJava requires users to master a lot of operators, and writing complex logic can be cumbersome, making you feel “trapped” in the call chain.
Kotlin’s suspend
keyword can help us to eliminate callbacks and write asynchronously in a synchronous way.
Principle of suspend
Since the createPost
methods are actually time-consuming IO asynchronous operations that need to wait until the return value is available before executing the logic that follows, but we don’t want to block the current thread (usually the main thread), we must eventually implement some kind of message passing mechanism that allows the background thread to pass the result to the main thread after doing the time-consuming operation.
Assuming we have the three callback-based APIs mentioned above, implementing suspend
would wrap the logic behind each pendant starting point 🏹 in a lambda at compile time, and then call the callback API, resulting in nested-like code. Kotlin and many other languages use a generative state machine for better performance.
Specifically, the compiler sees the suspend
keyword and removes the suspend
and adds an extra Continuation
argument to the function. This Continuation
represents a callback.
The Kotlin compiler generates a Continuation
implementation class for each suspend block, which is a state machine in which the state corresponding to each pending start point holds the context (i.e., dependent local variables) needed to continue execution next, similar to the following pseudo-code.
|
|
The compiler compiles suspend
into a method with a continuation argument called a CPS (Continuation-Passing-Style) transformation.
Using the suspend
function without caring about thread switching
suspend
provides a Convention that calls to this function will not block the currently calling thread. This is very useful for UI programming, because the main thread of the UI needs to constantly respond to various requests for graphics and user actions, so if there are time-consuming operations on the main thread, other requests will not be responded to in time, causing UI lag.
The Android community’s popular network request library Retrofit and the official database ORM Room already support Coroutine by providing the suspend
API, and Android officials also use Kotlin extended properties to provide components with lifecycle such as Activity
with the suspend
API. CoroutineScope, where the context specifies the use of Dispatchers.Main
, i.e. Coroutines started by lifecycleScope
will be dispatched to the main thread. So we can call the suspend
function and update the UI directly after we get the result without any thread switching action. Such a suspend
function is called “main safe”.
This is much better than the callback and RxJava APIs. These asynchronous APIs ultimately rely on callbacks, but the callback has to come back to the caller to figure out which thread it is in, depending on how the function is implemented. With the suspend
convention of not blocking the current thread, the caller doesn’t really need to care which thread the function is executed in internally.
For example, in the block above, we specify that this Coroutine block is scheduled to execute in the main thread, and it calls a suspend foo
method from somewhere. Inside this method may be a time-consuming CPU calculation, or it may be a time-consuming IO request, but I don’t really need to care what’s going on in there and which thread it’s running in when I write this Coroutine block. Similarly, when reading this Coroutine block, it is clear that the code in front of us will be executed in the main thread, and that the code inside suspend foo
is a potentially time-consuming operation, and the exact thread in which it is executed is an implementation detail of the function that is “transparent” to the logic of the current code.
But only if the suspend
function is implemented correctly, so that it does not block the current thread. Simply adding the suspend
keyword to a function does not magically make the function non-blocking, for example, suppose the implementation inside suspend foo
looks like this.
The internal implementation of the suspend
function here is a time-consuming CPU operation, which can similarly be thought of as a period of particularly complex code. The problem is that the implementation of the foo
function does not follow the semantics of suspend
and is wrong. The correct approach is to modify the foo
function
With withContext
we move the time-consuming operation from the current main thread to a default background thread pool. So it is said that even with Coroutine, you still end up “blocking” a thread, “all code is inherently blocking”. This understanding helps us to realize that threads are ultimately needed on Android / JVM as a vehicle to execute Coroutine, but ignores the distinction between blocking and non-blocking IO. CPU execution threads, and the above BigInteger.probablePrime
is a time-consuming CPU calculation that can only wait for the CPU to compute the result, but IO does not necessarily have to block the CPU.
There is a practical difference between blocking and non-blocking IO. For example, while Retrofit supports the suspend
function (which actually wraps the callback-based API enqueue
), the underlying dependency on OkHttp uses a blocking method, and the final execution of the request is dispatched to the thread pool. The Ktor’s HTTP client supports non-blocking IO. Try to use these two clients to make requests concurrently and you can feel the difference.
Of course, the client does not have as many “high concurrency” scenarios as the server, and does not need to initiate a large number of requests at the same time, so using a thread pool with a blocking API is usually enough. The Spring Framework provides WebFlux in addition to the traditional Servlet-based WebMvc, which provides a non-blocking Spring WebFlux natively provides a reactive programming model (similar to RxJava) with Reactive Streams to support non-blocking APIs. suspend function directly in the controller.
With Coroutine as the official recommended asynchronous solution for Android, common asynchronous scenarios such as network requests and databases already have libraries that support Coroutine, so it is conceivable that in the future, newcomers to Android development will not really need to know the details of thread switching, and will only need to call the encapsulated suspend
function directly in the main thread.
It’s not just IO that can be suspend
suspend
is not exactly a thread switch per se, but asynchronous IO in Android ultimately relies on multithreading, and asynchronous IO is the main application scenario for Coroutine. Coroutine’s suspend
does the same thing, but with the introduction of keywords and compiler support, we can write asynchronous logic in sequential, top-to-bottom code. Not only does this improve code readability, but it also makes it easy to write complex logic using familiar constructs such as conditionals, loops, and try catches.
Looking at Coroutine and suspend
as purely thread switching tools has significant limitations. Since suspend
is a callback and also provides a way to wrap the callback API, callback-based APIs can be transformed by wrapping them with suspend
functions.
Android View API
Suspending over views This article describes an example of wrapping Android view-related APIs with Coroutine. examples, such as the following extension function that waits for the end of Animator
.
|
|
Using traditional callback-based APIs to express such complex sequential code results in a lot of nesting and a significant decrease in code readability. By wrapping it in a suspend
function, we can write the code in top-down order in Coroutine, and at the same time facilitate the use of various conditions, loops and other logic control constructs to improve the expressiveness of the code.
The Animator.awaitEnd
wraps the AnimatorListenerAdapter
asynchronous callback interface, and the Kotlin Coroutine library provides the suspendCoroutine
and suspendCancellableCoroutine
functions (note that both of these functions are themselves suspend
). We can get the Continuation
instance that corresponds to the current hang
in the lambda we pass in. Calling the resume
series of methods on this instance in the appropriate callback will bridge the suspend
function with the callback-based API
|
|
Splitties is a very authentic Kotlin Android helper library that provides a suspend AlertDialog.showAndAwait
method. The following example code opens a dialog box and waits for the user to confirm that they want to delete it. This is an asynchronous operation, so it “hangs” the Coroutine and returns a boolean value when the user has finished selecting it.
Here AlertDialog.showAndAwait
wraps the DialogInterface.OnClickListener
interface using suspendCancellableCoroutine
.
Note that these examples above only involve the main thread and do not involve thread switching.
Functional exception handling
Going a step further, the suspend
function is not even necessarily limited to asynchronous scenarios.
The Kotlin Coroutine code we normally use is implemented in two packages, the standard Kotlin library kotlin-stdlib
and the Coroutine library kotlinx.coroutines
. The standard library provides Continuation
and other infrastructure related to CPS transformations, and kotlinx.coroutines
provides a concrete implementation of Coroutine. So we can actually use the CPS transformation infrastructure in the standard library to write other interesting things.
Λrrow (also written as Arrow) is a functional programming library for Kotlin that provides the Either
data type for exception handling.
The value of Either
can be both Left
and Right
. It is customary to use Right
to indicate a normal return value (think of it as right, which also means correct in English) and Left
to indicate an exception.
Assuming three interdependent subtasks takeFoodFromRefriderator
, getKnife
and lunch
, note that the example here is not an asynchronous IO but an exception
|
|
We can use Either.flatMap
to combine the three tasks together.
Does it look similar to the nested callbacks of IO? We can also eliminate the callbacks with the CPS transformation of suspend
Deep recursion
Recursion applied to recursive data structures can often result in clean and elegant code. For example, the following algorithm for calculating the height of a tree.
However, if the recursion is too deep beyond the limit, the runtime will throw a StackOverflowException
. So we need to make use of the more spacious heap memory. Usually we can maintain a stack data structure explicitly.
There is an experimental DeepRecursiveFunction
helper class in the Kotlin standard library that helps us write code that maintains the “general shape” of the recursive algorithm, but keeps the intermediate state in heap memory. The mechanism implemented there is the CPS transformation of suspend
.
|
|
DeepRecursiveFunction
is connected to a suspend
block, where the receiver is DeepRecursiveScope
, which can be analogous to CoroutineScope
. Inside this block, note that we cannot call depth
directly recursively as in the original algorithm (because it still depends on the space-limited function call stack). The DeepRecursiveScope
provides a suspend callRecursive
method. Here, we use the state machine obtained by the CPS transformation to preserve the intermediate results of the recursive function call stack. Since the Continuation
object is stored in heap memory at runtime, it bypasses the space constraints of the function call stack. (So Kotlin’s Coroutine is a so-called “stackless coroutine”.
For details, see Deep recursion with coroutines. KT-31741 has some discussions on standard library design and implementation as well as performance aspects.
As you can see from these different examples of Android UI, functional programming, and general programming, suspend
can be seen as syntactic sugar for callbacks, and is not essentially related to IO or thread switching. In retrospect, the keyword suspend
is often called async
in other languages, while Kotlin is called suspend
, perhaps suggesting that the unique design of the Kotlin Coroutine is not limited to asynchrony, but has a wider range of applications.