I’m using coroutines and `withTimeoutOrNull` to ex...
# coroutines
m
I’m using coroutines and
withTimeoutOrNull
to execute blocking code with a timeout. I’m doing this using a combination of
runBlocking
and
async
but I’m not clear about why using certain scopes results in the timeout not working. Here is code to demonstrate what I’m talking about: https://pl.kotl.in/af9mTAW9z
b
The first async is executed in a single threaded dispatcher (runBlocking), and cancellation of await also happens from the same thread. That means you can't cancel the await() before sleep finishes. With global scope it's executed in a separate dispatcher (Dispatchers.Default), that's why you can cancel the await. To make it work, use
async(Dispatchers.Default)
for work1
But overall, the easiest and most correct way to do that is to do withTimeou+runInterruptible. Because currently, despite you cancelled the await, the actual work will be continued. It may be fine for that example with thread sleep, but might not be expected for file io for example
g
also choice of Dispatchers.Default vs Dispatchers.IO depends on what kind code you want to run in this blocking function
m
Thanks all, yes I just saw
runInterruptible
which looks good. In reality, the withTimeout is being executed from a suspending function. So do I need to be careful with dispatchers regarding this?
g
if you do not block thread it will work, because coroutines work in single-threaded environment fine thanks to event queue
but suspend function itself doesn’t guarantee would it be safe or not, it still depends on the dispatcher Blocking is always may cause issues if you not careful, this why it’s better to avoid it or wrap to suspend code with dispatcher
m
Is there some way to ensure my
block
is run on a different thread to the timeout code?
g
you can check current thread of course, but not sure how it would help you
ah, I see what you mean. Well, you can specify dispatcher explicitly for it, it’s usually the way to go when you work with blocking code
m
And it has to be a unique dispatcher not possible to be used elsewhere?
g
No, why?
IO/Default dispatchers are good defaults, though not absolutely universal for edge cases
m
My understanding is that each dispatcher has its own pool of threads, so if you happen to run the block using a dispatcher that is being used elsewhere (e.g. Dispatchers.IO) there is a chance the timeout code is run on the same thread as the
block
?
g
no, it’s not like dispatchers work, if thread is in use (including blocked) another thread will be used. Of course there is a chance that all threads are blocked and you got dead lock, there is no perfect solution, because you very often don’t want infinite amount of threads (though you can configure default dispatchers, or use custom one)
there is a chance the timeout code is run on the same thread as the
one thread dispatches one coroutine the same time
m
Hmm, so I’m probably missing something here. Is the reason the following code (where I force
<http://Dispatchers.IO|Dispatchers.IO>
to be used for both timeout code and block, not working rather because of the fact the blocking code is not cooperative and so
withTimeoutOrNull()
cannot finish, rather than because of some dispatcher choice issue: https://pl.kotl.in/jF9hnYSjq
g
It doesn’t work because you do not use runInterruptible, as result you cannot cancel Thread.sleep Your code is written this way that it have to wait until
block
is finished, it’s not an issue of dispatchers
replace withContext with runInterruptible and it will work
m
Yes, but in my case I don’t want to cancel the block because I still want the result of the blocking code even if it comes late. So what I want to do is to ensure the blocking code is not run on the same thread as
withTimeoutOrNull
. I thought by using Dispatchers.IO this would happen automatically, but if I printout
Thread.currentThread().name
I see the same thread is being used, so it cannot work this way. So my question is why is the dispatcher using the same thread, and how to stop it from doing so?
BTW, I know I can fix all this by using a
GlobalScope.async
to run the
block
but I’m just trying to understand why.
g
It uses the same thread in case if it free, it’s not guaranteed which thread will be used
but in my case I don’t want to cancel the block
In this case you need to run it on separate scope
m
so if I wrap the block in `coroutineScope`that also doesn’t work, presumably because it is a child scope?
g
it’s not only because it’s child scope (so child scope has cooperative cancellation relationship with parent, if parent is cancelled, child is cancelled too), but it also how it semantically works, it will return only when coroutineScope lambda block will finish, just beacuse it’s suspend function and it exactly what it was created for, it suspends until scope inside of it is finished (so all child coroutines are completed)
n
If you have a thread pool with limited size that is running blocking code, it's possible for that thread pool to reach that max size and then it can't run any code, including code that resumes coroutines.
runBlocking
creates a
CoroutineScope
with a
CoroutineDispatcher
that uses the current thread. It's essentially a thread pool of limit 1. While that thread pool is running
Thread.sleep
it can't resume any coroutines.
GlobalScope
uses
Dispatchers.Default
which is essentially a thread pool with a limit equal to your CPU count. If
Dispatchers.Default
is running
Thread.sleep
then the
runBlocking
thread pool is not blocked, it can resume coroutines so
withTimeoutOrNull
can immediately cancel its children.
GlobalScope
also has no
Job
. It creates coroutines that are "roots" and are not connected to any other existing coroutine. You need to cancel these individually as they aren't part of an existing scope with a
Job
. This makes it easy to start coroutines that "leak" meaning you don't need them to run any more but they are still going because they weren't explicitly cancelled. All scoping suspend functions like
withTimeoutOrNull
or
withContext
or
coroutineScope
(little c) wait for child coroutines to finish before they resume. Even if
withTimeoutOrNull
is able to immediately cancel it's children, coroutine cancellation is asynchronous and cooperative. If that child take awhile to finish,
withTimeoutOrNull
will suspend until then. So in one of your examples, the one thread is blocked sleeping so
withTimeoutOrNull
can't even try to
cancel
its child coroutines (thread is blocked sleeping). In another example,
withTimeoutOrNull
succeeds in asking its children to cancel but the child doesn't cooperate so
withTimeoutOrNull
suspends until its child is finished. In the examples where `withTimeoutOrNull resumes before the
Thread.sleep
finishes, you are both sleeping on a different thread pool AND you are sleeping in a coroutine that is not a child of
withTimeoutOrNull
.
243 Views