Understanding coroutine in Kotlin — 4

In previous episodes

The first article presented the idea of ​​coroutines, which was put forward by Melvin Conway in 1963. He proposed to consider functions as independent program modules that pass control to each other instead of the approach when a program contains a set of procedures calling each other. The second article, based on Simon Statham's text, described a naive implementation of the coroutines idea in C and demonstrated the ability to “suspend” and “resume” the execution of a function. The third part compared coroutines with existing mechanisms, such as threads, callback functions and answered the question: “Why use coroutines?” The idea of ​​coroutines was voiced many years ago, but began to gain popularity and be supported by many programming languages ​​in the last 15 years. Coroutines are less demanding on resources and allow you to write code that works asynchronously while looking like familiar synchronous code. Before moving on to the details of coroutines in Kotlin, we should still make a bow to the work of asynchronous engines. Firstly, coroutines in Kotlin use callback functions under the hood, and therefore it is necessary to understand how these functions are executed in the operating system. Secondly, in the previous texts I practically did not talk about the work of asynchronous engines, and this is a rather large and important layer of information that I do not want to formalize as a separate text. Therefore, before reading further, I suggest the reader to look at 13. Computer Science Center – Asynchronous I/O. Coroutines And C++ User group – Anton Polukhin — Anatomy of asynchronous engines for a general understanding of the further narrative.

Coroutine in Kotlin and the suspend modifier

Kotlin has a special modifier called suspend that can be used to mark a regular function and tell the compiler that the function will be a coroutine. Suspend does not start a coroutine, but is a hint to transform the function so that it can be suspended and resumed during execution.

suspend fun func() { }

after compilation it will be converted into a function with an additional parameter Continuation.

@Nullable  
public final Object func(@NotNull Continuation $completion) {  
   return Unit.INSTANCE;  
}

The text from the language specification succinctly describes the idea of ​​coroutineizing a regular function.

Every suspending function is associated with a generated `Continuation` subtype, which handles the suspension implementation; the function itself is adapted to accept an additional continuation parameter to support the Continuation Passing Style.

Continuation Passing Style is an academic term that is essentially a callback function. Continuation is a type, or more accurately an interface, essentially a container that contains a resumeWith callback function and a CoroutineContext context.

public interface Continuation<in T> {
    public abstract val context: CoroutineContext
    public abstract fun resumeWith(result: Result<T>)
}

We'll talk about CoroutineContext next time, it's a container that stores additional information needed for the coroutine to work during suspension and resumption. The only thing missing is a component for implementing a finite state machine, similar to the naive C implementation from the second article. Let's look at the following example.

import kotlinx.coroutines.runBlocking  

suspend fun suspendFunction(): Int = 1  
fun function(data: Int) = data  

fun main(): Unit = runBlocking {  
        val r1 = suspendFunction()  
        function(r1)  
    }
}

There is a function suspendFunction marked as suspend and there is a regular (not suspend) function function. runBlocking is the so-called coroutine builder, which we will forget about for now. It is needed to call a coroutine from a regular main function. Let's decompile the code and look at the result.

import kotlin.Metadata;
import kotlin.ResultKt;
import kotlin.Unit;
import kotlin.coroutines.Continuation;
import kotlin.coroutines.CoroutineContext;
import kotlin.coroutines.intrinsics.IntrinsicsKt;
import kotlin.coroutines.jvm.internal.Boxing;
import kotlin.jvm.functions.Function2;
import kotlin.jvm.internal.Intrinsics;
import kotlinx.coroutines.BuildersKt;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public final class MainKt {
   public static final Object suspendFunction(Continuation $completion) {
      return Boxing.boxInt(1);
   }

   public static final int function(int data) {
      return data;
   }

   public static final void main() {
      BuildersKt.runBlocking$default((CoroutineContext)null, (Function2)(new Function2((Continuation)null) {
         int label;

         public final Object invokeSuspend(Object $result) {
            Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
            Object var10000;
            switch (this.label) {
               case 0:
                  this.label = 1;
                  var10000 = MainKt.suspendFunction(this);
                  if (var10000 == var3) {
                     return var3;
                  }
                  break;
               case 1:
                  var10000 = $result;
                  break;
               default:
                  throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
            }

            int r1 = var10000.intValue();
            MainKt.function(r1);
            return Unit.INSTANCE;
         }

         public final Continuation create(Object value, Continuation completion) {
            Function2 var3 = new <anonymous constructor>(completion);
            return var3;
         }

         public final Object invoke(Object var1, Object var2) {
            return (this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
         }
      }), 1, (Object)null);
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }
}

I removed everything that is not related to coroutines and distracts, like @Metadata and @NotNull annotations.

When calling the main function, the invoke() method will be called, which will call create, which will create and return Continuation and then call invokeSuspend. invokeSuspend contains a finite state machine with two labels. When label = 0, we get to case 0 and assign label = 1 and call suspendFunction. The function either suspends and then returns control (return var3), or returns the result. Let's consider the case of function suspension. Continuation will be converted to a certain task, which will be placed in a queue and after it is executed, the resumeWith method will be called with the execution result.

    public final override fun resumeWith(result: Result<Any?>) {
        // This loop unrolls recursion in current.resumeWith(param) to make saner and shorter stack traces on resume
        var current = this
        var param = result
        while (true) {
            // Invoke "resume" debug probe on every resumed continuation, so that a debugging library infrastructure
            // can precisely track what part of suspended callstack was already resumed
            probeCoroutineResumed(current)
            with(current) {
                val completion = completion!! // fail fast when trying to resume continuation without completion
                val outcome: Result<Any?> =
                    try {
                        val outcome = invokeSuspend(param)
                        if (outcome === COROUTINE_SUSPENDED) return
                        Result.success(outcome)
                    } catch (exception: Throwable) {
                        Result.failure(exception)
                    }
                releaseIntercepted() // this state machine instance is terminating
                if (completion is BaseContinuationImpl) {
                    // unrolling recursion via loop
                    current = completion
                    param = outcome
                } else {
                    // top-level completion reached -- invoke and return
                    completion.resumeWith(outcome)
                    return
                }
            }
        }
    }

There's a lot of interesting stuff inside resumeWith, but to simplify it, invokeSuspend will be called, and label will already be 1, not 0. The case 1 branch will be selected and the code will be executed.

case 1:
    var10000 = " class="formula inline">result;
break;

var10000 will receive a value equal to the result of executing suspendFunction and then exit the switch.

int r1 = var10000.intValue();
MainKt.function(r1);

Then r1 will receive the value of var10000 and pass it to a regular function, which will be executed as a regular function and return the result of execution.

Afterword

If you add several nested calls of suspend functions to the original example, you can notice the complication in constructing the finite state machine: new labels will appear, nesting of labels will be added. The compiler is responsible for constructing the finite state machine, so all that remains is to thank the authors for the work done. And, also separately note that, in fact, by adding one suspend modifier to the language, it was possible to implement such a powerful idea as coroutines, and for me it looks like an elegant design solution.

List of literature and materials:

  1. Understanding coroutines in Kotlin – Part 3

  2. Understanding coroutines in Kotlin – part 2

  3. Understanding coroutines in Kotlin – Part 1

  4. 13. Computer Science Center – Asynchronous I/O. Coroutines

  5. C++ User group – Anton Polukhin — Anatomy of asynchronous engines

  6. Kotlin Language Specification – Coroutines

  7. What is Color's Function?

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *