How coroutines switch back to the main thread.

Nazar Ivanchuk
ProAndroidDev
Published in
4 min readSep 4, 2018

--

Being an Android developer means always dealing with callback during loading same data from the Internet or from a local storage. Is it AsyncTask or RxJava it is still a callback. However, the situation changed significantly owing to coroutines as it provides a way to avoid blocking a thread and replace it with a cheaper version of the threads. Before migration to the framework I want to understand what is going on under the hood when we declare our coroutines.

I am not going to describe here how to set up project for starting to use coroutines. However, don’t forget that for the android platform we need to add dependency to the kotlinx-coroutines-android.

So let’s consider the small example of the single coroutine:

As logs show that suspend function was called from the background thread, however, it was instantly switched back to the main thread after function execution.

09-04 19:58:08 Execution thread: ForkJoinPool.commonPool-worker-1
09-04 19:58:09 Post execution thread: main

Now I want to go deeper into the process of switching threads and make the magic go away.

First of all, we should try to understand how the compiler sees our coroutine. This can be represented the following way.

The coroutine code was split into several parts:

  • The interface Continuation is declared in the Kotlin standard library, see documentation. It contains context and methods to complete continuation: resume and resumeWithException.
  • CoroutineImpl is an abstract class, which will be extended by our state-machine. It contains context and methods to create continuation.

After checking the generated bytecode, we can notice that the signature of the method loadString was transformed.

final synthetic loadString(L/Continuation;)L/Object;
L0
LINENUMBER 29 L0
LDC "TAG"
NEW StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
LDC "Execution thread: "
INVOKEVIRTUAL StringBuilder.append (String)StringBuilder;
INVOKESTATIC Thread.currentThread ()Ljava/lang/Thread;
DUP
LDC "Thread.currentThread()"
INVOKEVIRTUAL Thread.getName ()Ljava/lang/String;
INVOKEVIRTUAL StringBuilder.append (String;)/StringBuilder;
INVOKEVIRTUAL StringBuilder.toString ()/String;
INVOKESTATIC Log.d (/String;L/String;)I
POP
L1
LINENUMBER 30 L1
LDC "Some loaded string..."
ARETURN
L2
LOCALVARIABLE this Lcom/MainActivity; L0 L2 0
MAXSTACK = 5
MAXLOCALS = 2

Now it accepts continuation as an argument. In case our original function had some parameters continuation still would be added like the last parameter.

final synthetic loadString(L/Continuation;)L/Object;

This style of programming called continuation passing style . On the CPS, our continuation will be implicitly passed to the loadString function and the code will be executed after a function ends.

After reviewing bytecode for the onCreate method, I found an interesting point — creation of the onCreate$1 class.

LINENUMBER 19 L2
INVOKESTATIC HandlerContextKt.getUI ()LHandlerContext;
CHECKCAST /CoroutineContext
ACONST_NULL
ACONST_NULL
ACONST_NULL
NEW com/MainActivity$onCreate$1
DUP
ALOAD 0
ACONST_NULL
INVOKESPECIAL com/MainActivity$onCreate$1.<init>(MainActivity;Continuation;)
CHECKCAST kotlin/jvm/functions/Function2
BIPUSH 14
ACONST_NULL
INVOKESTATIC /BuildersKt.launch$default (/CoroutineContext;/CoroutineStart;Job;/Function1;Function2;/Object;)/Job;
POP

OnCreate$1 class was auto generated and it’s our coroutine, which holds state-machine. I am not going to publish the code, because it’s too big and you can review it yourselves via Android Studio: Tools -> Kotlin -> Show Kotlin Bytecode

final class MainActivity$onCreate$1 extends CoroutineImpl  implements Function2  {
....
}

In the coroutine world we have two type of the state-machines: stackful vs stackless. Kotlin coroutine based on the stackful state-machines.

Now we can sum up what is the difference between suspending function and coroutines:

  • A suspending function defines a state machine.
  • A coroutine is an instance of that state machine, created as a result of a call to the associated suspending function.

Let’s return to the launch function, where we are passing instance of HandlerContext. This is the child class of the library class CoroutineDispatcher. CoroutineDispatcher contains the declaration of the abstract method dispatch:

dispatch(context: CoroutineContext, block: Runnable) - Dispatches execution of a runnable [block] onto another thread in the given [context].

The implementation of dispatcher is straightforward. HandlerContext contains reference to the handle, which accepts main looper.

val UI = HandlerContext(Handler(Looper.getMainLooper()), "UI")

And the override dispatch function takes runnable block and passes it to the handler.

override fun dispatch(context: CoroutineContext, block: Runnable) {
handler.post(block)
}

The runnable block contains the code, which was put into the continuation block that was mentioned on the image above. Thus, the code will be able to perform on the main thread. I have missed a huge part how the continuation was delivered to the dispatcher, because it is out of the scope. Will try to describe this on the separate post.

Now it is pretty clear how the switching back to the main thread was performed. There is no magic. There are just a couple of manipulations with source code.

Conclusion

Coroutines in Kotlin are a great framework, which helps to build multithreading application without additional complexity and callbacks. Also, it’s nice that the coroutines is a part of the language. I’m looking forward to seeing coroutines passed an experimental stage.

For more information see Kotlin coroutines documentation.

Thanks for reading the article and sharing your views. Please clap and recommend as much as you can.

Please don’t hesitate to contact me: Github and Facebook

--

--