In the last blog, I talked about several misconceptions during my Kotlin coroutines learning journey. And in the conclusion, I pointed out that Kotlin coroutines build around the concept of suspending and resuming around suspension points.

Here I’ll dive deeper into the Kotlin coroutines compilation process and code execution for a breakdown of the inner workings.

Code Blocks

The magic of Kotlin coroutines starts at the compilation stage. During compilation, the code within coroutine and body of “suspending function” is split into smaller “Code Blocks”. The compiler extracts statements from the code and groups them into the body of different “switch-cases” in the compiled Java class bytecode.

This can be demonstrated with a simple coroutine example, the code comments indicate “Code Blocks” grouping generated by the compiler:

At run time, the compiled bytecode is executed by the Java virtual machine. “Code Blocks” classes are instantiated and wrapped as Task/Runnable and executed by “Dispatcher”. The Dispatcher used is determined by the optional CoroutineContext parameter passed to coroutine builders launch and async .

In this example, I’m using the runBlocking coroutine, which uses a Blocking Event Loop as a dispatcher that executes code only within the main thread. Notice the two launch coroutine builders have no explicit Dispatcher supplied. As a result, they inherit the dispatcher from the main runBlocking coroutine.

Running the example should print the Checkpoints from 1 to 10 as demonstrated in the animated image.

It is asynchronous (non-blocking) as the code execution switches back and forth between the two child coroutines without getting blocked by the delay statements:

runBlocking starts the Event Loop, enqueues Code Block 1 Event Loop dequeues Code Block 1 and executes it Prints Checkpoint 1 The 1st launch statement is invoked, this enqueues Code Block 2 The 2nd launch statement is invoked, this enqueues Code Block 4 Prints Checkpoint 2 Finishes execution of Code Block 1, passes control back to Event Loop Event Loop dequeues Code Block 2 and executes it. Prints Checkpoint 3 Invokes mySuspendFunction1 In the compiled code of mySuspendFunction1 . It first keeps track of the code from caller to be executed after completion of mySuspendFunction1 (Code Block 3), then it executes the first Code Block of mySuspendFunction1 — Code Block 6 Prints Checkpoint 4 Function delay is invoked, this flags for code suspension. Code Block 7 of mySuspendFunction1 is enqueued as a delayed task (technically delayed task is put into another ThreadSafeHeap ) Finishes execution of Code Block 2 and Code Block 6, passes control back to Event Loop Event Loop dequeues Code Block 4 and executes it. Prints Checkpoint 5 Invokes mySuspendFunction2 In the compiled code of mySuspendFunction2 . It first keeps track of the code from caller to be executed after completion of mySuspendFunction2 (Code Block 5), then it executes the first Code Block of mySuspendFunction2 — Code Block 8 Prints Checkpoint 6 Function delay is invoked, this flags for code suspension. Code Block 9 of mySuspendFunction2 is enqueued as a delayed task (technically delayed task is put into another ThreadSafeHeap ) Finishes execution of Code Block 4 and Code Block 8, passes control back to Event Loop Event Loop waits until it’s time to dequeue and execute Code Block 7 Prints Checkpoint 7 Finishes execution of Code Block 7. As mySuspendFunction1 is completed, it triggers Code Block 3 from the caller of mySuspendFunction1 Prints Checkpoint 8 Finishes the execution of Code Block 3, passes control back to Event Loop Event Loop waits until it’s time to dequeue and execute Code Block 9 Prints Checkpoint 9 Finishes execution of Code Block 9. As mySuspendFunction2 is completed, it triggers Code Block 5 from the caller of mySuspendFunction2 Prints Checkpoint 10 Finishes execution of Code Block 5, passes control back to Event Loop Event Loop is empty and exits

You can see from the code execution that Code Blocks created by the compiler is the secret trick of coroutines’ non-blocking magic!

Code Blocks from different coroutines can be inserted into the Event Loop queue interleaving each other. So even though the Blocking Event Loop uses only the main thread to consume and run the Code Blocks sequentially based on their queuing order, it can execute half of a coroutine (that makes up a Code Block unit) and switch to another coroutine (the next Code Block unit) without blocking.

Other coroutine Dispatchers might use different threads and strategy to execute coroutines, however they all operate on smaller Code Blocks generated by the compiler, which remain the same regardless of the dispatcher used.

Coroutines Continuation

So far, we have explored the logical concept of code execution with Code Blocks. But how does that concept get implemented into the compiled Java bytecode? And what keeps track of the state of Code Block execution (i.e. which Code Block should be executed next)? To answer these questions, let’s look at a decompiled (simplified) version of mySuspendFunction1 :

There are two interesting pieces here:

The mySuspendFunction1 function takes a Continuation argument (vs the original source code signature that takes no argument) The mySuspendFunction1 function has a switch-case which each case maps to different logical “Code Block”, the construct switches on the label field of the Continuation object

The compiler adds a Continuation parameter to the original parameter list. And it is pretty clear that this Continuation object is the one that keeps track of the execution state of the Coroutine. Indeed the state is simply kept as an integer field label of the Continuation object.

So we might wonder what calls mySuspendFunction1 with the Continuation argument?

During compilation, some extra classes are generated that extend ContinuationImpl , each of those extra classes contains information which the suspend function calls, it also passes itself (which is a Continuation ) as an argument.

For example, below is the decompiled (simplified) version of the generated class DemoKt$mySuspendFunction1$1 :

We can see that mySuspendFunction1 is invoked with DemoKt$mySuspendFunction1$1 passing itself as the Continuation argument.

These Continuation objects are wrapped as Tasks and enqueued into the Event Loop I mentioned earlier. They get executed/resumed when the Task is dispatched/resumed which eventually calls invokeSuspend on the Continuation .

Conclusion

We have examined the code execution and the decompiled bytecode of Kotlin coroutines.

As a quick summary:

Coroutines are split into smaller Code Blocks during compilation

Each Code Block is a unit of execution (i.e. it should be fully executed before it returns control)

Code Blocks from different coroutines can then be enqueued and executed interleaving each other with the help of Continuation and Dispatcher

What’s next?

Are you interested in monitoring your Kotlin Application and coroutines? SolarWinds® AppOptics™ has released version 6.10.0 with out-of-the-box Kotlin coroutines support. With this new version you can easily trace operations handled by Kotlin coroutines in great detail — no code change required for a wide range of supported frameworks (Spring-MVC from Spring-boot for example)! Simply signup and download the AppOptics Java Agent (which works for Kotlin too!). Enable the agent by adding the following JVM options to your Kotlin process:

-javaagent:/usr/local/appoptics/appoptics-agent.jar

That’s it! Restart your Kotlin web application and you should be able to see useful metrics and detailed breakdown per request.

As a quick example, let’s modify our demo code a little bit and insert that into a Spring-boot Controller with coroutines:

This example uses Kotlin’s Ktor asynchronous HttpClient with Apache to make two outbound HTTP requests asynchronously with coroutines. Once installed and enabled, the AppOptics agent automatically traces the traffic to this controller and shows the two outbound requests running simultaneously without blocking:

And just for fun, let us remove the coroutines launch and see what happens:

As expected, the 2 outbound HTTP requests are no longer executed simultaneously:

This is because when the first mySuspendFunction hits suspension point request.await there are no other coroutines available for execution. Take note that the second mySuspendFunction is not launched/invoked yet, because code within the same coroutine runBlocking is executed sequentially, the first invocation of mySuspendFunction has to return first before the second call to mySuspendFunction is reached.

This concludes my adventure exploring Kotlin coroutine! Special thanks to Bruce MacNaughton, Hunter Sherman and Lin Lin for their contributions and suggestions to these blogs!

About the Author

Patson Luk is a developer with experience in a variety of domains, from large-scale enterprise banking system to lightweight mobile payment solutions. He now leads Java development for SolarWinds AppOptics, an application performance monitoring product. Patson’s focus is on using Java bytecode manipulation technologies to gain greater visibility into the full spectrum of Java-based applications.

If you want to learn more about monitoring your Kotlin Application and coroutines with AppOptics check out Java Monitoring with AppOptics.