Reputation: 3462
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
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
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
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
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