ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Follow publication

How runBlocking May Surprise You

--

Photo by mana5280 on Unsplash

When we start learning coroutines we always try something quick by using the runBlocking builder. Let’s take a look at one slightly weird thing regarding runBlocking in which, if you write this code on the UI thread, you will deadlock your Android app forever.

//somewhere in UI thread
runBlocking(Dispatchers.Main) {
println(“Hello, World!”)
}

It’s not a bug, it’s a 100% expected behavior. It may seem a little confusing and non-explicit. So let’s dive in and understand what’s going on here.

First, let’s compare this code with the old fashion android Java thread-style. You can write this on the UI thread:

//somewhere in UI thread
runOnUiThread {
println("Hello, World!") // it will run on the UI thread
}

Or even this:

//somewhere in UI thread
Handler().post {
println("Hello, World!") // it will run on the UI thread as well
}

They both work just fine (differently, but fine). So what’s the difference with runBlocking code? Let’s find out.

We start with runBlocking builder itself.

Usually, runBlocking it is used in unit tests in Android or in some other cases of synchronous code. Keep in mind that runBlocking is not recommended for production code.

runBlocking builder does almost the same thing as launch builder: it creates a coroutine and calls its start function. But runBlocking creates a special coroutine called BlockingCoroutine which has an additional function joinBlocking(). runBlocking calls joinBlocking() right away after starting a coroutine.

// runBlocking() function
// ...
val coroutine = BlockingCoroutine<T>(newContext, ...)
coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
return coroutine.joinBlocking()

This joinBlocking() function uses Java blocking mechanism LockSupport to park (block) current thread. LockSupport is a low-level and high-performance tool usually used to write your own locks. Also, BlockingCoroutine overrides afterCompletion() function which is called after coroutine is finished.

override fun afterCompletion(state: Any?) {
// wake up blocked thread
if (Thread.currentThread() != blockedThread)
LockSupport.unpark(blockedThread)
}

So this function simply unparks (unblocks if it was blocked in park() before) the blocked thread. The following diagram roughly shows what runBlocking does.

Ok, we understood what runBlocking builder does. Now let’s continue with comparing these two pieces of code:

// this code creates a deadlock
runBlocking(Dispatchers.Main) {
println(“Hello, World!”)
}
// this doesn't
runBlocking(Dispatchers.Default) {
println(“Hello, World!”)
}

Why Dispatchers.Main leads to deadlock and Dispatchers.Default doesn’t.

First, let’s remember what Dispatchers are. The Dispatcher determines what thread or threads a coroutine uses for its execution. In a sense, we can assume dispatchers as a high-level alternative to Java Executors. We can even create one using a handy extension:

public fun Executor.asCoroutineDispatcher(): CoroutineDispatcher

Dispatchers.Default implements DefaultScheduler class and delegates dispatching to coroutineScheduler. Its dispatch() function looks like this:

override fun dispatch(context: CoroutineContext, block: Runnable) =
try {
coroutineScheduler.dispatch(block)
} catch (e: RejectedExecutionException) {
// ...
DefaultExecutor.dispatch(context, block)
}

CoroutineScheduler class is responsible for distributing dispatched coroutines over worker threads in the most efficient manner. It implements the Java Executor interface.

override fun execute(command: Runnable) = dispatch(command)

CoroutineScheduler.dispatch() function:

  • Adds runnable block to the task queue. There are actually two queues: local and global. It’s a part of prioritizing external tasks mechanism.
  • Creates workers. Worker is a class inherited from the Java Thread class (in this case — daemon thread). So literally it creates worker threads. Worker also has a local queue of tasks. A Worker picks tasks from local or global queue and executes them.
  • Starts workers.

So what happens?

  1. runBlocking starts the coroutine which calls CoroutineScheduler.dispatch().
  2. dispatch() starts Workers (i.e. worker threads).
  3. BlockingCoroutine blocks current thread with LockSupport.park().
  4. A targeted block of code actually executes.
  5. afterCompletion() function is called and it unblocks the current thread using LockSupport.unpark.

This flow is illustrated in the following diagram:

Now let’s have a look at Dispatchers.Main dispatcher. The main dispatcher is an Android-specific dispatcher. Using the main dispatcher throws an exception if you don’t add the dependency:

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:*.*.*'

For Android-specific purposes, HandlerContext class was added to the coroutines package. HandlerContext is a dispatcher that executes tasks using an Android handler.

Let’s have a look at how it is created. Dispatchers.Main is created by AndroidDispatcherFactory class with createDispatcher() function.

override fun createDispatcher(...) =
HandlerContext(Looper.getMainLooper().asHandler(async = true))

Looper.getMainLooper().asHandler() means that it takes handler of android main thread. So it turns out that Dispatchers.Main is HandlerContext (dispatcher with a handler) with Android main thread handler.

Now let’s have a look at dispatch() function of the HandlerContext:

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

It just posts a runnable to the handler. In our case to the main thread handler.

So what happens?

  1. runBlocking starts the coroutine which calls CoroutineScheduler.dispatch().
  2. dispatch() posts targeted runnable block of code to main thread handler.
  3. BlockingCoroutine blocks current thread with LockSupport.park().
  4. The main looper never gets a message with a targeted runnable block because it is blocked.
  5. Because of this afterCompletion() is never called.
  6. And because of this, the current thread won’t be unparked (unblocked) in afterCompletion() function.

This flow is illustrated in the following diagram:

This is why runBlocking with Dispatchers.Main blocks UI thread forever. The UI thread is blocked and waiting for the finishing targeted code. But it never finishes because the main looper can’t start the targeted code.

Remember the Handler().post example in the beginning? It works fine and doesn’t block anything. However, we can easily change it to be pretty much similar to our code with Dispatcher.Main. We can add parking and unparking operation to the current thread. And the code begins to work pretty much the same as with runBlocking builder.

//somewhere in UI thread
val thread = Thread.currentThread()
Handler().post {
println("Hello, World!") // this will never be called
// imitates afterCompletion()
LockSupport.unpark(thread)
}
// imitates joinBlocking()
LockSupport.park()

By the way, this trick won’t work with the runOnUiThread function.

//somewhere in UI thread
val thread = Thread.currentThread()
runOnUiThread {
println("Hello, World!") // this will work just fine
LockSupport.unpark(thread)
}
LockSupport.park()

This happens because runOnUiThread uses optimization by checking the current thread. If the current thread (UI thread in this case) is the same then it will execute the block of code right away. Otherwise, it will post a message to the main thread looper.

Dispatchers.Main has similar optimization. It’s called Dispatchers.Main.immediate. It has a similar logic to runOnUiThread. And thus this block of code will work just fine:

//somewhere in UI thread
runBlocking(Dispatchers.Main.immediate) {
println(“Hello, World!”)
}

There is one last thing to understand. All we have discussed is not connected only to the main thread. We can get into this situation (blocking our execution) using not only the Dispatchers.Main. For example, we can use newSingleThreadContext to create a new dispatcher and eventually get into the same situation. UI won’t be frozen but execution will be blocked.

val singleThreadDispatcher = newSingleThreadContext("Single Thread")
GlobalScope.launch(singleThreadDispatcher) {
runBlocking(singleThreadDispatcher) {
println("Hello, World!") // won't be executed again
}
}

This is it. Now you know why a seemingly harmless runBlocking piece of code could freeze your Android app.

This is my first one on Medium. Let me know what you think. And If you have any questions or your experience to share please go to the comment section below.

--

--

Published in ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Written by Max Kach

Android Software Engineer at Dodo Brands.

Responses (9)

Write a response