Reading Coroutine official guide thoroughly — Part 1 — Dive 3
What is suspend function and How do they work?
[Korean] English
(Return to the table of contents)
When you compare a normal function to a suspending function, you can find that there aren’t any differences except the suspend
prefix of suspending function; So, Apparently it’s not that big difference between the two.
However the keyword suspend
do all the magic here — Coroutine World.
Kotlin compiler treats specially the function which has suspend
prefix. More specifically, it adds Continuation as the last parameter of the suspending function and changes the return type to Any? (nullable) in compile time. All these things happen to transform a function to a CPS function. You can read more about CPS which is short for Continuation Passing Style here.
The suspending function generated by the kotlin compiler can only be called inside of a coroutine and it can call any other suspending function freely including built-in ones such as delay, yield, etc.
A suspending function can suspend a coroutine that invokes it as its name explains. In other words, a suspending function can be thought as the break point of an execution of a coroutine. You can use this suspend
keyword to almost all the types of functions such as top-level function, extension function, member function, local function, operator function, etc.
When a suspending function is called from a coroutine, it captures all the execution information (a.k.a execution context), makes them as Continuation object and cache it. After that, when the coroutine is resumed, it recovers the execution information from the cached object and resumes its execution on the environment.
Let’s take a look at the process that a suspending function changes to a Continuation object. Let’s say you make below function which adds two integer numbers.
The function takes 2 seconds because it calculates super complex numbers 😄.
This function must be called inside of a coroutine body or from other suspending function because it is suspending function.
You can create main function to test the sum()
function like below.
Kotlin compiler transform the function signature like below when you compile above code.
INVOKESTATIC com/smp/coroutinesample/basic/BasicSample4Kt.sum (IILkotlin/coroutines/Continuation;)Ljava/lang/Object;
In the above function signature,
IILkotlin/coroutines/Continuation;
the part above describes the parameter types of the function and it means it has 3 parameters — ‘I’ for Integer, and ‘L’ for Object (with package name). So, this function has two interger parameter and one object. You may noticed that the compiler adds Continuation object as the last parameter of the function.
Now, the suspending function has the power of Continuation and the coroutine which is execution environment of the suspending function can deal with this function as CPS supported function. It means the Coroutine Framework can control the execution flow of the coroutine as using the Continuation attached to the function (can be suspended or resumed).
Let’s take a look below example.
The suspending function “longRunningTask” is very simple function that calls delay() suspending function twice. Let’s consider it is very heavy function. And this function is also called twice in the coroutine which is launched in the main function. You can see the execution flow below.
The “LongRunningTask1” suspending function is suspended when it meets the first delay() function (We call it as suspension point from now on). Then, another suspended function have a chance to be executed. Now, The “LongRunningTask2” suspending function is executed and it is suspended soon when it meets delay() function. Two tasks share the execution time of a coroutine running on a thread and takes turn being executed (like time slicing).
Are there any differences if you call “LongRunningTask2” inside of “LongRunningTask1” instead of individual suspending function call like above? The answer is no. Like nested coroutine, nested suspending function also store its execution context as a form of Continuation so that it can be resumed after calling other suspending functions. As a result, the top level suspending function get the final result.
Above picture looks familiar, Doesn’t it? When function A calls function B and function B calls Function C, each function call stores the return function pointer in the stack so that callee function can go back to the caller function.
Call : Func A -> Func B -> Func C
Return : Func C -> Func B -> Func A
(Stack unwinding).
Technically, the implementations of Continuation also implement CoroutineStackFrame which has caller stack frame to go back to previous continuation.
The calling stacks of normal functions are managed by operating system. Then what manages the stacks of nested coroutines or nested suspending functions ? That is coroutine framework and it keeps calling context as a form of Continuation (CPS Style) so that it can be back to resume its execution with the restored context. If one of the functions in the call stack throws an exception, it is propagated to the first suspending function as using Continuation.
The end.