alexbtr
alexbtr

Reputation: 3462

How to run several Kotlin coroutines in parallel and wait for them to complete before proceeding

I need to run 2 coroutines in parallel and wait for them to finish before proceeding. The code below works but it uses GlobalScope which is not the best way to do it.

Is there a better way?

fun getInfo(onSuccess: () -> Unit, onError: () -> Unit) {
        GlobalScope.launch(Dispatchers.IO) {
            try {
                coroutineScope {
                    launch { getOne() }
                    launch { getTwo() }
                }
                onSuccess.invoke()
            } catch (e: Throwable) {
                onError.invoke()
            }
        }
    }

Upvotes: 17

Views: 24167

Answers (4)

alexbtr
alexbtr

Reputation: 3462

Actually there is even a better way based on the accepted answer:

suspend fun getInfo() = withContext(Dispatchers.IO) {
try {
    coroutineScope {
        launch { getOne() }
        launch { getTwo() }
    }
    false
} catch (e: Throwable) {
    true
}

}

Upvotes: 0

Willi Mentzel
Willi Mentzel

Reputation: 29884

I would suggest implementing getInfo as a suspend function which knows the context it is supposed to run on. This way it does not matter from which context you invoke it (*).

Additionally, I would not use callbacks to proceed afterwards. You can just decide from what getInfo() returns on how to proceed (**).

This is actually the greatest thing about coroutines, you can turn callback based code in code that reads like sequential code.

Since you don't care for the result of getOne() and getTwo(), using launch is the correct way. It returns a Job. You can suspend the coroutine until both functions are finished with joinAll() which can be invoked on Collection<Job>.

suspend fun getInfo() = withContext(Dispatchers.IO) {
    try {
        listOf(
            launch { getOne() },
            launch { getTwo() }
        ).joinAll()
        false
    } catch (e: Throwable) {
        true
    }
}

You don't need to use GlobalScope, just create your own (***).

I used Default as context to launch getInfo, but any other context would be fine too, since getInfo will run on the one it is supposed to.

val newScope = CoroutineScope(Dispatchers.Default).launch {
    val error = getInfo()
    if(error) {
        onSuccess()
    } else {
        onError()
    }
}
// "newScope" can be cancelled any time

* In case I used Dispatcher.IO to pretend that the two functions are doing some long running IO work.

** I used a simple boolean here, but of course you can return something more meaningful.

*** or hook into some scope given by the sourrouding framework which is lifecycle-aware

Upvotes: 23

Glenn Sandoval
Glenn Sandoval

Reputation: 3745

You can create a CoroutineScope inside the class you're working on and build coroutines just by calling launch builder. Something like this:

class MyClass: CoroutineScope by CoroutineScope(Dispatchers.IO) { // or [by MainScope()]

    fun getInfo(onSuccess: () -> Unit, onError: () -> Unit) {
        launch {
            try {
                val jobA = launch { getOne() }
                val jobB = launch { getTwo() }
                joinAll(jobA, jobB)
                onSuccess.invoke()
            } catch (e: Throwable) {
                onError.invoke()
            }
        }
    }

    fun clear() { // Call this method properly
        this.cancel()
    }

}

Make sure to cancel any coroutine running inside the scope by calling cancel properly.

Or if you're working inside a ViewModel just use the viewModelScope instead.

Upvotes: 2

Tenfour04
Tenfour04

Reputation: 93834

You can use any scope you like. That doesn't affect your question. GlobalScope is discouraged because it doesn't encapsulate your tasks.

Which scope to actually use to launch your coroutine is going to depend entirely on the context of what you're doing and what strategy of encapsulation you're using.

To launch multiple coroutines at once and wait for all of them, you use async and await(). You can use awaitAll() on a list of them if you simply need all of them to finish before continuing.

It seems odd that you use the IO dispatcher to run your callbacks on, but I'll leave that because I'm blind to the context of your code.

fun getInfo(onSuccess: () -> Unit, onError: () -> Unit) {
        GlobalScope.launch(Dispatchers.IO) {
            try {
                coroutineScope {
                    listOf(
                        async { getOne() }
                        async { getTwo() }
                    }.awaitAll()
                }
                onSuccess.invoke()
            } catch (e: Throwable) {
                onError.invoke()
            }
        }
    }

Upvotes: 5

Related Questions