https://kotlinlang.org logo
Title
d

Dimitri Fedorov

07/08/2019, 7:33 PM
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:
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

streetsofboston

07/08/2019, 7:37 PM
With coroutines, you’d be able to flatten this. Would it look something like this?:
...
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

Dimitri Fedorov

07/08/2019, 7:37 PM
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

streetsofboston

07/08/2019, 7:38 PM
Then something like this?
...
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

Dimitri Fedorov

07/08/2019, 7:39 PM
like i said - i can’t suspend current thread
s

streetsofboston

07/08/2019, 7:39 PM
No, I mean, you’d like your code to look something like ths?
d

Dimitri Fedorov

07/08/2019, 7:40 PM
ideally i’d liked it as imperative as possible, so yes. i’m not sure what’s the proper coroutine-way though
s

streetsofboston

07/08/2019, 7:43 PM
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

Dimitri Fedorov

07/08/2019, 7:46 PM
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

streetsofboston

07/08/2019, 7:47 PM
Install a dispatcher in your
scope
, a dispatcher that has only one thread in its thread-pool
scope.launch(mySingleThreadedDispatcher) { … }
d

Dimitri Fedorov

07/08/2019, 7:50 PM
sorry for lame question, what can i use as a value of
scope
here?
s

streetsofboston

07/08/2019, 7:51 PM
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

Dimitri Fedorov

07/08/2019, 8:01 PM
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

streetsofboston

07/08/2019, 8:10 PM
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

Dimitri Fedorov

07/08/2019, 8:23 PM
very first call should be made on existing thread. because its a callback itself.. that’s intelllij sdk, not android
z

Zach Klippenstein (he/him) [MOD]

07/08/2019, 8:25 PM
If your callbacks need to be executed on the thread they’re invoked from, use
Dispatchers.Unconfined
.
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

Dico

07/08/2019, 8:35 PM
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

Zach Klippenstein (he/him) [MOD]

07/08/2019, 8:38 PM
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

Dico

07/08/2019, 8:39 PM
If all callbacks are already invoked on the right thread, you can use
Unconfined
.
val scope = CoroutineScope(Dispatchers.Unconfined)
scope.launch(start = CoroutineStart.UNDISPATCHED) {
    ... code here
}
s

streetsofboston

07/08/2019, 8:39 PM
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

Dimitri Fedorov

07/08/2019, 8:39 PM
runBlocking { launch(Dispatchers.Unconfined) {
will run on current thread but also will block it
z

Zach Klippenstein (he/him) [MOD]

07/08/2019, 8:39 PM
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

streetsofboston

07/08/2019, 8:41 PM
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

Dimitri Fedorov

07/08/2019, 8:43 PM
actually suggestion by Dico (with
CoroutineScope(Dispatchers.Unconfined).launch(start = CoroutineStart.UNDISPATCHED)
) works fine
z

Zach Klippenstein (he/him) [MOD]

07/08/2019, 8:43 PM
CoroutineStart.UNDISPATCHED
is redundant with
Unconfined
dispatcher.
s

streetsofboston

07/08/2019, 8:44 PM
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

Dimitri Fedorov

07/08/2019, 8:44 PM
because of external APIs providing callbacks only
z

Zach Klippenstein (he/him) [MOD]

07/08/2019, 8:45 PM
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

streetsofboston

07/08/2019, 8:46 PM
I understood they all must be called on the same original thread that called the initial function …
z

Zach Klippenstein (he/him) [MOD]

07/08/2019, 8:47 PM
every element or service is invoking callback on proper thread already
d

Dimitri Fedorov

07/08/2019, 8:47 PM
yep, every element or service is providing thread itself
so far
CoroutineScope(Dispatchers.Unconfined).launch
seems to work, thanks a lot guys!
👍 1
s

streetsofboston

07/08/2019, 8:51 PM
Ah… understood those two points incorrectly. The first one is not a requirement, it is a pre-condition, a given 🙂
d

Dimitri Fedorov

07/08/2019, 8:52 PM
@streetsofboston Thats perfectly correct. Sorry for my English, kind Sir 🙂
s

streetsofboston

07/08/2019, 8:54 PM
No problem. Glad we figured out your issue! 🙂
d

Dimitri Fedorov

07/08/2019, 8:55 PM
Yup. Many thanks again!
g

gildor

07/09/2019, 1:22 AM
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

Dimitri Fedorov

07/09/2019, 7:50 AM
@gildor Yep, thanks Andrey. That was a typo, I meant “block” thread, not “suspend”. Sorry, still getting used to terminology
g

gildor

07/09/2019, 7:50 AM
just wrap blocking call to withContext, also I prefer to extract such blocking function to own non-blocking fucnction and switch context thtere
d

Dimitri Fedorov

07/09/2019, 7:54 AM
actually blocking is exactly what I’d liked to avoid. i have no blocking calls, just callback-based api invocations
g

gildor

07/09/2019, 7:55 AM
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

Dimitri Fedorov

07/09/2019, 8:05 AM
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

gildor

07/09/2019, 9:57 AM
just wasnt using them in proper context
What do you mean?
d

Dimitri Fedorov

07/09/2019, 10:25 AM
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

gildor

07/10/2019, 1:31 AM
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)