Hello guys. I’m trying to escape callback hell in ...
# coroutines
d
Hello guys. I’m trying to escape callback hell in my current application, and it seems I can’t find proper coroutine-based approach. Almost all existing code is following this pattern:
Copy code
element.call() {
   service.run() {
      element2.call() { ... }
   }
}
there is 2 requirements: 1. every element or service is invoking callback on proper thread already 2. every call shouldn’t block its own thread, i.e. each callback should exit after invoking next element/service (i can’t use
suspendCoroutine
because of that) what is proper coroutine way to do that?
s
With coroutines, you’d be able to flatten this. Would it look something like this?:
Copy code
...
element.call() // suspends until it is done.
service.run() // suspends until it is done.
element2.call() // suspends until it is done
...
(i assume from your example that your callbacks don’t provide a result, just a callback for when stuff is done…)
d
nah, they actually do provide a result.. just omitted it from the code
and i can’t suspend after making call - current thread should continue, callback will be invoked on another thread (decided by service)
s
Then something like this?
Copy code
...
val result = element.call() // suspends until it is done.
val runResut = service.run() // suspends until it is done.
val result2 = element2.call() // suspends until it is done
...
d
like i said - i can’t suspend current thread
s
No, I mean, you’d like your code to look something like ths?
d
ideally i’d liked it as imperative as possible, so yes. i’m not sure what’s the proper coroutine-way though
s
Copy code
suspend fun Element.call_() = suspendCancelableCoroutine { cont -> call() { cont.resume(it } }

suspend fun Service.run_() = suspendCancelableCoroutine { cont -> run() { cont.resume(it) } }

...

scope.launch {
    val result = element.call_() // suspends until it is done.
    val runResut = service.run_() // suspends until it is done.
    val result2 = element2.call_() // suspends until it is done
}
Just gave the suspend variations of your functions the same name with an extra
_
. None of your threads will block, not the calling ones, nor your callback-threads.
d
the very first call should be done in the same thread it was executed from
i can’t really switch threads myself here. every call should be done in current thread, including first one
launch will create a new thread, i guess
s
Install a dispatcher in your
scope
, a dispatcher that has only one thread in its thread-pool
scope.launch(mySingleThreadedDispatcher) { … }
d
sorry for lame question, what can i use as a value of
scope
here?
s
It depends. It is an instance of a CoroutineScope. It really depends on the lifecycle of your component that needs the
result2
in the end.
d
like this? `GlobalScope.launch(newSingleThreadContext("new`context”)) { `
problem is can’t control on which thread calls are made, including very first call
with suggestion above, new thread is created for the scope and very first call fails because of being made on new thread, not on existing one
s
You can’t make just any already existing thread be part of a Coroutine dispatcher…. On what thread needs the first call to
element.call()
be made?
Sometimes, it works, because the calling thread is part of a pool that is also used for a coroutine dispatcher. E.g. the Android main UI thread is the same one that is used for the ‘Dispatchers.Main’. But this situation is not always there….
d
very first call should be made on existing thread. because its a callback itself.. that’s intelllij sdk, not android
z
If your callbacks need to be executed on the thread they’re invoked from, use
Dispatchers.Unconfined
.
Copy code
val scope = Dispatchers.Unconfined + parentJob
scope.launch {
    // Invoked on calling thread.
    val result = element.call_()
    // Invoked on whatever thread invoked the first callback.
    val runResult = service.run_()
    // Invoked on whatever thread invoked the second callback.
    val result2 = element2.call_()
    // Calling function will resume on whatever thread invoked the third callback.
}
d
Just use
runBlocking
?
@Zach Klippenstein (he/him) [MOD] that's not what
Dispatchers.Unconfined
does. If one function changes thread, it will stay on that thread when it returns.
z
Yes, that’s what I was trying to show with the comments. If the thing invoking the callbacks invokes them on different threads, that’s the thread that the coroutine will resume on.
d
If all callbacks are already invoked on the right thread, you can use
Unconfined
.
Copy code
val scope = CoroutineScope(Dispatchers.Unconfined)
scope.launch(start = CoroutineStart.UNDISPATCHED) {
    ... code here
}
s
Yup. The only way to I have found to safely use Unconfined is in tests, where I know that callback are happening on the same thread that invoked the function/request.
d
runBlocking { launch(Dispatchers.Unconfined) {
will run on current thread but also will block it
z
I understood that to be the behavior that @Dimitri Fedorov was asking for – the thing invoking the callbacks is already invoking them on the “correct” threads.
👍🏻 1
s
And that is generally not possible if the original calling thread is not already part of a pool that is part of some dispatcher.
For Android, and other UIs, you are lucky when invoking a
launch
from the Main UI thread and using the Dispatchers.Main, because that is the same thread, since the Main UI thread is part of the pool of Dispatchers.Main.
d
actually suggestion by Dico (with
CoroutineScope(Dispatchers.Unconfined).launch(start = CoroutineStart.UNDISPATCHED)
) works fine
z
CoroutineStart.UNDISPATCHED
is redundant with
Unconfined
dispatcher.
s
Then you have to assume that the callbacks (the lambdas of
run
and
call
) will never be called on another thread. That would make for a leaky abstraction. Also, if that is the case, why use callbacks at all?
d
because of external APIs providing callbacks only
z
Then you have to assume that the callbacks (the lambdas of
run
and
call
) will never be called on another thread.
Not if you don’t care which threads your code is running on – in this case, it sounds like the intention is for those callbacks to be invoked on different threads.
s
I understood they all must be called on the same original thread that called the initial function …
z
every element or service is invoking callback on proper thread already
d
yep, every element or service is providing thread itself
so far
CoroutineScope(Dispatchers.Unconfined).launch
seems to work, thanks a lot guys!
👍 1
s
Ah… understood those two points incorrectly. The first one is not a requirement, it is a pre-condition, a given 🙂
d
@streetsofboston Thats perfectly correct. Sorry for my English, kind Sir 🙂
s
No problem. Glad we figured out your issue! 🙂
d
Yup. Many thanks again!
g
i can’t suspend current thread
@Dimitri Fedorov It’s impossible to suspend thread, you can only block thread. And suspend function should not block thread (if you have some blocking thread in suspend function, you should wrap it with withCotntext(IO/Default)
d
@gildor Yep, thanks Andrey. That was a typo, I meant “block” thread, not “suspend”. Sorry, still getting used to terminology
g
just wrap blocking call to withContext, also I prefer to extract such blocking function to own non-blocking fucnction and switch context thtere
d
actually blocking is exactly what I’d liked to avoid. i have no blocking calls, just callback-based api invocations
g
oh, callback, that it’s not blocking, yeah, you should write coroutines adapters for your API. And I also don’t understand what you mean and why you cannot use suspendCoroutine
every call shouldn’t block its own thread, i.e. each callback should exit after invoking next element/service (i can’t use
suspendCoroutine
because of that)
d
disregard that, I can use them fine, just wasnt using them in proper context. can’t grasp those coroutines easily, sometimes I feel myself particularly stupid 🙂
g
just wasnt using them in proper context
What do you mean?
d
I was using them with runBlocking, so suspendCoroutine was blocking thread it was run on. Now I’m running it with
CoroutineScope(Dispatchers.Unconfined).launch
, works as it should
g
It doesn’t look correct for me.
if suspend function have contract on which it should be called, switching to this thread should be done inside of suspend function
you said that your functions are asynchronous already, why do you have problem than with thread? because suspendCoroutine doesn’t switch threads and don’t use any particular dispatcher
Of course it may be a case when you need current thread, but it looks a bit strange for me. As ad-hoc solution is fine, but not if you want to expose this API, maybe you need own implementation of dispatcher that dispatch coroutine to particular thread explicitly (like with Dispatchers.Main)