Marius Metzger
05/19/2021, 3:13 PM>Question about avoiding deadlocks<<My team and I are using Kotlin for the development of a Minecraft server. We’ve implemented our own coroutine dispatcher that executes on the main server thread, which basically looks like this:
private class MinecraftCoroutineDispatcher : CoroutineDispatcher() {
@InternalCoroutinesApi
override fun isDispatchNeeded(context: CoroutineContext): Boolean = !Bukkit.isPrimaryThread()
/** Dispatches the coroutine on the main server thread. */
override fun dispatch(context: CoroutineContext, block: Runnable) {
BukkitExecutor.schedule(action = block::run)
}
}
This is all fine and dandy, until we use runBlocking
to block the main server thread, which causes a deadlock when switching back to the main thread. Minimal example:
fun myFun() {
// we are already on the server thread here, so that thread is blocked by runBlocking
runBlocking(Dispatchers.Minecraft) {
val result = withContext(<http://Dispatchers.IO|Dispatchers.IO>) { /* some I/O operation */ }
// <-- deadlock RIGHT HERE when switching back to Minecraft dispatcher
// perform some code on the server thread
}
}
The reason this deadlocks is because BukkitExecutor.schedule
used by Dispatchers.Minecraft
requires the server thread to run, as scheduled tasks are executed at the end of each “tick”. If the server thread is blocked, however, as it is in our case thanks to runBlocking
, the task is never executed, meaning the switch back to Dispatchers.Minecraft
deadlocks.
In kotlinx.coroutines 1.4.0, I wrote the following code that messes with coroutine internals to determine whether the server thread is currently being blocked by a parent coroutine: