Hey, I have two suspending functions and want to g...
# coroutines
f
Hey, I have two suspending functions and want to garauntee the first completes before the second is invoked. What is the best way to do this? 🧵
The first invokes KStore flow settings, setting a key which is then observed by the sound manager through a flow. Then I invoke the sound manager which should have received the key and will "speak" using the set voice. I am having timing issues where the sound manager speaks with the previously set voice.
alertSettingsManager.setTTSVoice(settingsIntent.voiceId)
soundManager.textToSpeech("Testing testing")
I have tried many different mechanisms with async and different dispatchers, getting a working solution but with hacky looking code. Is there a prescribed way to guarantee the execution order here?
j
Two suspend functions will not run in parallel by default:
Copy code
suspend fun foo() {
    suspendBar()
    suspendBaz() // execution starts after suspendBar finishes
}

suspend fun suspendBar() {
    // do something
}

suspend fun suspendBaz() {
    // do something
}
☝️ 2
f
if you have
Copy code
scope.launch {
    suspendFun1()
    suspendFun2()
}
then
suspendFun2
only runs after
suspendFun1
is finished. What I suspect happens here is that
setTTSVoice
is returning before the change ha been committed, so that when you read it, you get the old value. You would have to find a way to determine when the value has been committed so that you can read the new value
f
KStore must be returning and emitting the value to the observing flows at a later time.
If invoke both functions in separate withContext(Default) it works
Copy code
withContext(Dispatchers.Default) {
    alertSettingsManager.setTTSVoice(settingsIntent.voiceId)
}
withContext(Dispatchers.Default) {
    soundManager.textToSpeech("testing testing")
}
o
If the functions you're invoking
launch
coroutines under the hood without waiting for them to complete (which would be a questionable API design), what you wrote is equivalent to this:
Copy code
withContext(Dispatchers.Default) {
    coroutineScope {
        alertSettingsManager.setTTSVoice(settingsIntent.voiceId)
    }
    soundManager.textToSpeech("testing testing")
}
Otherwise, if the functions are regular suspending functions, you could just write:
Copy code
withContext(Dispatchers.Default) {
    alertSettingsManager.setTTSVoice(settingsIntent.voiceId)
    soundManager.textToSpeech("testing testing")
}
And depending on how the sound manager observes your flow, you might never have a guarantee that this happens before you invoke
textToSpeech
.
u
Check the job is completed then start the second. Val job1 = function()
Job1.wait()
Used to check the completion
f
@Oliver.O what would make that a questionable design decision?
I guess you should leave the concurrency management to the caller?
o
This is probably too large to discuss here, but for a start: https://elizarov.medium.com/coroutine-context-and-scope-c8b255d59055
👀 1
Seems like all you really want is sequential execution (1. set voice, 2. speak). Why are you using concurrency then?
f
KStore uses a suspend function to write to preferences, On JVM I need to the play the voice on a different thread or else the UI stalls.
o
Copy code
suspend fun writeToKstore(voiceId: Any) {
    // ...
}

fun CoroutineScope.speak(voiceId: Any, text: String) {
    launch(<http://Dispatchers.IO|Dispatchers.IO>) {
        // speak text with voiceId
    }
}

suspend fun doItAll(backgroundScope: CoroutineScope) {
    writeToKstore(settingsIntent.voiceId)
    backgroundScope.speak(settingsIntent.voiceId, "blabla")
}
If you have no
backgroundScope
that would cancel your background coroutines when appropriate, you could use
GlobalScope
with the usual caveats (see docs).
f
@Oliver.O what was your point about launching within a suspend function?
and it being a bad design decision?
o
You're passing an explicit scope. That's the difference. Sometimes you need both, be suspending and launch stuff. The technique I've shown makes it as explicit as possible.
1
f
I am trying this, but it only works when the context is using Dispatchers.Main, when I use default I get the speech coming out with the last voice. I am very confused. I will look into your solution @Oliver.O
Copy code
withContext(Dispatchers.Main) {
                            launch {
                                settings.alertSettingsManager.setTTSVoice(settingsIntent.voiceId)
                            }.invokeOnCompletion {
                                launch {
                                    soundManager.textToSpeech("Work. Rest. Finished.")
                                }
                            }
                        }
o
You're not doing the same thing. I was explicitly passing the voiceId directly to the speak function, so that it is there when needed. If you're doing this via two concurrent jobs with one observing a flow from the other, you have no guarantees that the flow value will be collected at the right time. You could of course add extra synchronization, but this will quickly become a complicated mess.
f
Ah I see, yeah that was my fallback plan. Would be so nice if there could be a guarantee that the collectors will get the value first.
o
If you start adding such guarantees, you'll end up with strange blocking code and a ton of problems. So if you need "happens before" guarantees, just invoke stuff sequentially (suspending is OK, launching is not).
f
I wonder why KStore launches coroutines when writing their data then. It seems a bit odd with this perspective.
o
I'm not familiar with KStore. But there is one issue where the question came up.
f
I might raise an issue there and see what they say, thanks @Oliver.O really do appreciate the help 🙂
👍 1
Just raised this, will be interesting to see what they come back with https://github.com/xxfast/KStore/issues/129
x
Hi 👋 I’m the author of KStore. KStore doesn’t
launch
any coroutines under the hood. It is using the coroutine that the call-site `launch`es. If you want parallel behaviour you can achieve this by `launch`ing your own coroutines in parallel. KStore does maintain a
Mutex
lock to guarantee the read/writes are done sequentially in the call-corder and this is why all the API signatures to the store are `suspend`ed. I’ve replied to your issue if you want more details,
@Fergus Hewson for your usecase, i think you’d have to do something like
Copy code
launch {
  store.set(settingsIntent.voiceId)
  
  withContext(<http://Dispatcher.IO|Dispatcher.IO>){ 
     soundManager.textToSpeech("Work. Rest. Finished.")
  }
}
again, i have no context as to what you are trying to achieve here - but I’m assuming your app has a setting about the voice that you want the sound manager to speak in ?
f
Hey @xxfast. Use case is to set the voiceId in settings and the soundManager is observing a flow of that value. Ideally the sound manager would collect the voiceId before textToSpeech was invoked, but it was being collected sometime later. I added a hacky little delay to make the ordering work.
🤔 1
Thanks for getting back to me, love your lib btw.
x
I see. Perhaps if you can share a minimal reproducer I can see what I can do 🙂
f
I'll have a go when I get time today.