Summary: - What exactly is the problem with callin...
# coroutines
g
Summary: • What exactly is the problem with calling
runBlocking
inside coroutines like
async
or
launch
? To elaborate... When using
async
, the async block in our code often calls methods with
runBlocking
in them (not intentionally, but the methods often have runBlocking to bridge between netty WebClient and synchronous code, because these methods are usually called in "normal" code and not from coroutines). Reading the docs on
runBlocking
, I found the warning that says "This function should not be used from a coroutine.", which sent me down the rabbit hole. Further explanation in thread
🧵 1
[Baseline example]
Copy code
val start = LocalDateTime.now()
runBlocking {
    for (i in 1..128) {
        async(<http://Dispatchers.IO|Dispatchers.IO>) {
            <http://logger.info|logger.info>("$i async - ${currentCoroutineContext()}")
            delay(1000)
        }
    }
}
<http://logger.info|logger.info>("finished, took ${LocalDateTime.now().toUnixMillisecond() - start.toUnixMillisecond()} ms")
-> this code takes 1 second to complete, as expected. [Let's confirm that runBlocking inside async blocks the thread]
Copy code
runBlocking {
    for (i in 1..128) {
        async(<http://Dispatchers.IO|Dispatchers.IO>) {
            <http://logger.info|logger.info>("$i async - ${currentCoroutineContext()}")
            runBlocking {
                <http://logger.info|logger.info>("$i start - ${currentCoroutineContext()}")
                delay(1000)
                <http://logger.info|logger.info>("$i end - ${currentCoroutineContext()}")
            }
        }
    }
}
-> this code takes 2 seconds to complete, because runBlocking probably blocked the thread of the parent async, and since
<http://Dispatchers.IO|Dispatchers.IO>
has 64 threads, it takes 2 seconds completing each batch of 64 async blocks every second. Understandable. [WTF?]
Copy code
runBlocking {
    for (i in 1..128) {
        async {
            <http://logger.info|logger.info>("$i async - ${currentCoroutineContext()}")
            runBlocking {
                <http://logger.info|logger.info>("$i start - ${currentCoroutineContext()}")
                delay(1000)
                <http://logger.info|logger.info>("$i end - ${currentCoroutineContext()}")
            }
        }
    }
}
-> we can expect this code to take 128 seconds now, since async only has 1 thread (of its parent runBlocking) available. But noooo it takes only 1 second. ??? [Further questionable mysteries]
Copy code
runBlocking {
    repeat(5) {
        launch {
            <http://logger.info|logger.info>("$it start")
            runBlocking {
                delay(1000L * it)
            }
            <http://logger.info|logger.info>("$it end")
        }
    }
}
one might expect this code to print like below
Copy code
0 start
...
4 start
0 end
...
4 end
but it's actually like so:
Copy code
19:48:16.805 [http-nio-11159-exec-1 @coroutine#2] INFO - 0 start
19:48:16.806 [http-nio-11159-exec-1 @coroutine#3] INFO - 1 start
19:48:16.806 [http-nio-11159-exec-1 @coroutine#4] INFO - 2 start
19:48:16.806 [http-nio-11159-exec-1 @coroutine#5] INFO - 3 start
19:48:16.806 [http-nio-11159-exec-1 @coroutine#6] INFO - 4 start
19:48:20.814 [http-nio-11159-exec-1 @coroutine#6] INFO - 4 end
19:48:20.815 [http-nio-11159-exec-1 @coroutine#5] INFO - 3 end
19:48:20.815 [http-nio-11159-exec-1 @coroutine#4] INFO - 2 end
19:48:20.816 [http-nio-11159-exec-1 @coroutine#3] INFO - 1 end
19:48:20.816 [http-nio-11159-exec-1 @coroutine#2] INFO - 0 end
which means that the 5 iterations of
runBlocking
were started normally, but their completion is blocked in a Last-In-First-Out manner. This causes unexpected delay when there's additional code after the runBlocking completes. (for example in the example above, 0 start -> 0 end was expected to have 0 second delay, but instead was delayed by 4 seconds.) I'd like to ask the following: • what exactly is the issue with runBlocking inside coroutines? Should it never be used in coroutines under any circumstances? • why does it sometimes seem to block a thread but sometimes not? • why does the runBlocking in the last example wait to complete until other runBlockings that were called in other coroutines later complete?
s
> • what exactly is the issue with runBlocking inside coroutines? Should it never be used in coroutines under any circumstances? Blocking a coroutine's thread is always bad, because the thread might be shared with other tasks. For example, it might be responsible for UI updates, or for other coroutines that were dispatched elsewhere in the application. Blocking a coroutine's thread with
runBlocking
is doubly bad, because we're specifically blocking the thread to wait for other coroutines. Inside those coroutines, there might be code that ends up being dispatched to the very same thread that we've just blocked. That creates a deadlock, where the thread is blocked waiting for itself. This situation is much easier to get into than you'd think. I wrote more about it here. > • why does it sometimes seem to block a thread but sometimes not? The situation is a bit different when you call
runBlocking
inside another call to
runBlocking
. It uses a thread-local event loop, so if the thread is already running coroutines via
runBlocking
, nested calls to
runBlocking
will cooperate with the existing event loop instead of taking over the whole thread. This stops working when you introduce other threads and dispatchers into the mix, like the
<http://Dispatchers.IO|Dispatchers.IO>
in your example. > • why does the runBlocking in the last example wait to complete until other runBlockings that were called in other coroutines later complete? Think of all these
runBlocking
calls as being nested inside one another—not inside the same coroutine, but inside the call stack of the single thread that's actually doing all the work. When task 0 yields at the
delay
, task 1 starts running, and makes its own
runBlocking
call. This is an actual stack frame, not a suspension point, and the call stack still needs to unwind in LIFO order. Relying on the dispatch order of coroutines is a bad idea in general, so I wouldn't pay too much attention to this implementation detail.
🙌🏾 1
🙌 3
a
Well explained @Sam
🙇 1
g
sorry for the late reply! thank you for the explanation, it was very helpful.
🐕 1
🍻 1