franztesca
07/14/2023, 8:48 AMwithTimeoutOrNull
val myResult = runBlocking {
withTimeoutOrNull(300) { myFunction() }
}
What we experienced in the wild, though, is that we are still blocking the main thread for too long. The only reasons that comes to our mind is that it could be because myFunction
takes some additional time to complete, even when cancelled. This could be caused by some blocking IO or any other non-prompt-when-cancelled code, and structured concurrency. This example shows what could be happening.
The only solutions that come to our mind are:
• use runInterruptible
around the blocking code: unfortunately the suspending code we invoke in myFunction
implementation is in a separate library that is out of our control.
• opt-out of structured concurrency using this:
val myResultDeferred = GlobalScope.async { myFunction() }
val myResult = try {
runBlocking {
withTimeoutOrNull(300) { myResultDeferred.await() }
}
} finally {
myResultDeferred.cancel()
}
Example of the solution.
However, this is not a very neat solution and is quite error prone. Also we cannot run myFunction
on the main thread otherwise we would get a deadlock.
Q1: Do you see any other possible cause for which we would be blocking the thread for longer than 300ms, in practice?
Q2: Do you see other possible solutions? Maybe less tricky?
thank you colorRobert Williams
07/14/2023, 9:30 AMRobert Williams
07/14/2023, 9:31 AMRobert Williams
07/14/2023, 9:33 AMDmitry Khalanskiy [JB]
07/14/2023, 10:01 AMwe have a suspending function that we need to execute synchronously (blocking) on the main threadBut the "opt-out" solution doesn't work like this. It will execute
myFunction
on Dispatchers.Default
, not on the main thread.Dmitry Khalanskiy [JB]
07/14/2023, 10:03 AMrunInterruptible
is the way to go.Robert Williams
07/14/2023, 10:08 AMfranztesca
07/14/2023, 10:58 AM@OptIn(ExperimentalStdlibApi::class)
fun <R> runBlockingWithTimeoutUnstructuredOrNull(
timeoutMillis: Long,
block: suspend CoroutineScope.() -> R
): R? = runBlocking {
val blockingDispatcher = coroutineContext[CoroutineDispatcher]!!
val fallbackDispatcher = if (Thread.currentThread().isMainThread()) Dispatchers.Main else Dispatchers.Default
val switchableDispatcher = SwitchableCoroutineDispatcher(blockingDispatcher)
val unstructuredDeferred = GlobalScope.plus(switchableDispatcher).async(block = block)
try {
withTimeoutOrNull(timeoutMillis) { unstructuredDeferred.await() }
} finally {
// Before completing, make sure that we don't use the blocking dispatcher anymore
switchableDispatcher.switchTo(fallbackDispatcher)
unstructuredDeferred.cancel()
}
}
Would this work (allowing us to run the code on the main thread, while still unstructurally skipping the awaiting of the completion) (knowing that is ugly and dangerous)
Also, if we manage to not require the main thread, is the "opt-out" solution fine?Dmitry Khalanskiy [JB]
07/14/2023, 11:06 AMblock
values, println
-ing Thread.currentThread()
, etc, and check if it actually works to get a more robust internal model. No amount of hackery will be able to help you in this case. If a thread is busy doing something, it's busy doing that, no matter which dispatcher is used, simple as that. If the main thread is busy doing some computation that doesn't know how to be interrupted, the thread can't stop doing that work, end of story.franztesca
07/14/2023, 11:21 AMIf the main thread is busy doing some computation that doesn't know how to be interruptedIn this case (and probably our case), it may be that the main thread is not actually busy, but it is suspended until another busy coroutine (on another thread) resumes it, for example:
runBlocking {
withTimeoutOrNull(300) {
withContext(<http://Dispatcher.IO|Dispatcher.IO>) {
someBlockingIO() // Busy thread = IO
}
}
}
The only reason the main thread is busy here is because runBlocking
is enforcing structured concurrency, awaiting the withTimeoutOrNull
coroutine, which awaits withContext
, which awaits a busy thread. There is no computation going on on the main thread. The hackery above seems to solve the problem indeed.Robert Williams
07/14/2023, 11:53 AMRobert Williams
07/14/2023, 11:53 AMwithContext(Dispatchers.Main)
?Robert Williams
07/14/2023, 11:54 AMfranztesca
07/15/2023, 12:12 AMRobert Williams
07/16/2023, 11:23 AMRobert Williams
07/16/2023, 11:24 AMThe default [CoroutineDispatcher] for this builder is an internal implementation of event loop that processes continuations in this blocked thread until the completion of this coroutine.