what is the correct way to integrate coroutines in...
# coroutines
j
what is the correct way to integrate coroutines in a context where we don't control the API we are integrating with? For instance, in a okhttp interceptor which has a sync API, I would like to read a database entry using coroutines. My first thought was
runBLocking
, but I everybody says we shouldn't be using that, so what should I be using?
j
Who says you shouldn't be using
runBlocking
? It is correct to use it when the API calling you expects to block the thread until the result is ready. So in a callback from a library like this, this is totally fine, especially because OkHttp likely uses its own thread pool, and exposes blocking APIs itself
j
No, it seems it is not OK, because of the issues it have when you switch dispatchers from one that is single threaded. It seems to cause this https://github.com/Kotlin/kotlinx.coroutines/issues/2448 I used okhttp as an example, it is not really my current problem, but something very similar.
y
FWIW we discussed this with Jesse from Square on https://github.com/square/okhttp/issues/7164 . I don't know we have a perfect general solution yet.
I think it's runBlocking, which is fine as long as you know the operation can complete. But it's hard from framework code to do that.
He linked to https://www.billjings.com/posts/title/foot-marksmanship-with-runblocking/, which I've read but need to re-read.
j
I'm not saying it's OK in general, you have to consider why it's OK in your case. The problem you're linking to occurs because the code is blocking the main thread, and then trying to dispatch something on the main thread again while it is blocked. In your case, your code is called by OkHttp's interceptor, which runs on background threads AFAIK. This interceptor exposes a blocking API (
intercept()
). Because OkHttp needs your result when the method returns, and doesn't provide a way to give the result asynchronously, there is nothing you can do but block that thread until the result is ready. You just need to be aware that coroutines launched inside that
runBlocking
won't be able to use that blocked thread, which should be fine because it should be part of OkHttp's own thread pool
y
That makes sense. I agree with you, just wanted to point out that runBlocking doesn't just work in all cases. You also need to be able to reason about your own code that it is not getting into resource contention. If your task inside runBlocking needs to make another network call, it might deadlock.
j
as I said, the interceptor was just a sample, 3th party APIs don't care. They may provide APIs that are not thought with coroutine in mind and that trigger things in the main thread.
y
runBlocking is exactly this API for bridging a non-coroutines blocking API with coroutines functions. And has an elegant design (IIRC, correct me if wrong) that your threads is donated as the default Dispatcher for work, so your thread isn't sitting idle.
1
j
@juliocbcotta ok let me be more general, then. Here is the situation: a library that you don't control is calling you back via a synchronous function that expects a result from you via a return value (the function you implement is not
suspend
and provides no async way like a
Future
or
Deferred
or callback to return that result). In that case, the contract of that function is to block the calling thread until the result is ready. There is no way around that, you can dispatch what you want on any thread, the calling thread will still have to be blocked while waiting for the result - it's how that API was designed. In that sense, that's what
runBlocking
is for, it even uses that blocked thread to do the work, which is nice. Now, if that function is called on the main thread by the library, then that means you have to block the main thread to do whatever you have to do there. This also probably means you shouldn't be doing long work like network calls or DB access in there at all, the problem is not really
runBlocking
here, it's the semantics of what you're trying to call. If you're in this situation, there is no magic coroutine construct that will help you, you'll have to work around the design of the library on a case-by-case basis.
👍🏻 1
👎 1
👍 1
j
I don't have any issue with blocking the main thread, the issue is that if you call
runBlocking
from the main thread, you can't switch dispatchers inside the called suspended function or it will cause a dead lock in the main thread as in the issue I pointed a few comments above.
take a look at the linked issue.
n
From the linked issue:
TL;DR: Don't block the main thread with runBlocking. It produces all sorts of weird stuff.
j
yes, that is why I started this discussion saying that it was not what is recommended. I don't care of blocking the thread, but if using
runBlocking
is not an option, what do I have as option then? • Not using coroutines?
j
@Nick Allen right, but in the situation mentioned above, you just don't have a choice. The library that calls you back gives you the main thread. You could technically make use of the main thread that's given to you (like
runBlocking
does if you don't switch context), but at the moment coroutines don't allow you to go back and forth 2 levels deep
runBlocking { withContext(IO) { withContext(Main) { ... }}}
. This is something that could technically be solved by coroutines, I'm still not 100% sure it should be, but it could. That might lead to weird ordering of code execution maybe? Don't know.
j
even
Copy code
runBlocking { withContext(IO) { }}
breaks the main thread.
j
But that's expected, it doesn't hang forever. This is what you should accept because it's semantically what the library is asking for, which I tried to explain earlier by:
the contract of that function is to block the calling thread until the result is ready. There is no way around that, you can dispatch what you want on any thread, the calling thread will still have to be blocked while waiting for the result - it's how that API was designed
j
No, it does hangs forever. This is what that issue is about.
🚫 1
u
@juliocbcotta who says that? https://pl.kotl.in/iejwByOZP
The issue is about hanging if you block the main thread and then try to schedule back to the main thread. Which is magically fixed by coroutines for the first level, but not for deeper nesting.
And as Roman Elizarov mentions, it is more surprising, that, when blocking the main thread, scheduling to the main thread works on the first level, than it is surprising that it breaks at deeper levels.
j
That's just a side effect of
.immediate
AFAIU. But if you schedule it with
withContext(Dispatchers.Main)
it does hang immediately (not at the end of the block)
👍 1
However, I still believe that it is counter-intuitive for the users that
runBlocking
manages to reuse the current thread as an event-loop, and yet fails to use this event loop when using
withContext(Main)
like this. It makes sense though, because going through the main dispatcher is not the same as the
runBlocking
event loop itself, but still it's surprising
👍 2
358 Views