Run Kotlin coroutines in sequence

Viewed 1769

Lets say I have these Kotlin suspend functions doing various operations:

suspend fun operation1(){ //some code }
suspend fun operation2(){ //some code }
suspend fun operation3(){ //some code }
suspend fun operation4(){ //some code }
suspend fun operation5(){ //some code }

I have a function which gets called by an external library (which we can't modify), where I make calls to these functions (of course its logic is more complex in practice). Let's assume the library can make any number of calls in a short time.

fun calledByLibrary(someParam: String){
    GlobalScope.launch{
        when(someParam){
            "someValue1" -> operation1()
            "someValue2" -> operation2()
            "someValue3" -> operation3()
            "someValue4" -> operation4()
            "someValue5" -> operation5()
        }
    }
}

Now, we had a bug, and figured that operation3 and operation4 can not be suspended to run in 'parallel'. They even need to be executed in the same order as called by the external library. For all the other combinations it's ok to do that, so we would like to keep them as suspend to support this.

What's the best way to solve this?

One simple, and easy way would be to use a Mutex in operation3 and operation4 to ensure they don't run in the same time, like this:

val mutex = Mutex()
suspend fun operation3(){ mutex.withLock(){/*some code*/} }
suspend fun operation4(){ mutex.withLock(){/*some code*/} }

But this does not guarantee that they will be executed in the order they have been requested by the library.

Also tried searching for running coroutines in a queue, but surprisingly did not find anything trivial. (Current solution is using a single threaded dispatcher and runBlocking, which is far from ideal)

2 Answers

After tying a lot of things, seems like an actor can be a solution, as a lot of people mentioned using channels, which is a part of the actors. But implementing an actor seemed a complex solution, for such a simple issue, so I tried to avoid it and find a more simple one.

Turned out that the idea with the mutex can be easily fixed to also ensure the order of execution, by using the same thread in the coroutine. The operation functions already had the logic to use the proper thread when needed (if they did not it would be a trivial thing to add). So the only change needed is to launch the coroutine in the callback on the same thread always. This can be done easily by using Dispachers.Unconfined, or if you want to avoid that, you can create your own single threaded dispacher too.

So, the of course highly simplified solution, would look like this:

fun calledByLibrary(someParam: String){
    //You can use any single threaded dispatcher here
    //if you want to avoid the unconfined
    GlobalScope.launch(Dispachers.Unconfined){
        when(someParam){
            "someValue1" -> operation1()
            "someValue2" -> operation2()
            "someValue3" -> operation3()
            "someValue4" -> operation4()
            "someValue5" -> operation5()
        }
    }
}

val mutex = Mutex()
suspend fun operation1(){ launch{/*some code*/} }
suspend fun operation2(){ launch{/*some code*/} }
suspend fun operation3(){ mutex.withLock(){launch{/*some code*/}} }
suspend fun operation4(){ mutex.withLock(){launch{/*some code*/}} }
suspend fun operation5(){ launch{/*some code*/} }

You can also do something like this from within a coroutine scope. The await() makes sure to wait for the results before continueing.

    val resultOne = async { repository.getSomethingFromApi() }
    val resultTwo = async { repository.getSomethingElseFromApi() }

    val resultOneFromApi = resultOne.await()
    val resultTwoFromApi = resultTwo.await()
Related