Let’s say I have three functions
// The result type of somethingUsefulOneAsync is Deferred<Int>
fun somethingUsefulOneAsync() = GlobalScope.async {
delay(1000L)
return 13
}
// The result type of somethingUsefulTwoAsync is Deferred<String>
fun somethingUsefulTwoAsync() = GlobalScope.async {
delay(1000L)
return "Hello"
}
// The result type of somethingUsefulThreeAsync is Deferred<Long>
fun somethingUsefulThreeAsync() = GlobalScope.async {
delay(1000L)
return 30L
}
Is there any way to execute these functions concurrently AND assign the result of each function to a separate variable?
Something like this
var a: Int
var b: String
var c: Long
a, b, c = (somethingUsefulOneAsync, somethingUsefulTwoAsync, somethingUsefulThreeAsync).awaitAll()
I know it’s possible for multiple functions with the same return type, just not sure how I’d do it when the return types are different.
For e.g. concurrently making the call to several third party APIs over the network. Each one will have a different response structure
Just launch them and then await each independently.
val oneAsync = somethingUsefulOneAsync()
val twoAsync = somethingUsefulTwoAsync()
val threeAsync = somethingUsefulThreeAsync()
var a: Int = oneAsync.await()
var b: String = twoAsync.await()
var c: Long = threeAsync.await()
Note: your example uses GlobalSclope. From GlobalSclope docs:
Application code usually should use an application-defined CoroutineScope. Using async or launch on the instance of GlobalScope is highly discouraged.
I recommend reading up on Structured Concurrency
Isn’t “awaiting each one independently” different from using awaitAll
? Based on the documentation for awaitAll
:
awaitAll is not equivalent to deferreds.map { it.await() } which fails only when it sequentially gets to wait for the failing deferred, while this awaitAll fails immediately as soon as any of the deferreds fail.
I used GlobalSclope
for illustrative purposes only, those methods are copied over as-is from here
How about val (a, b, c) = awaitAll(somethingUsefulOneAsync(), somethingUsefulTwoAsync(), somethingUsefulThreeAsync())
?
If you want immediate detection of failure, yes, use awaitAll. To extract the result in a type-safe manner, extract separately.
val oneAsync = somethingUsefulOneAsync()
val twoAsync = somethingUsefulTwoAsync()
val threeAsync = somethingUsefulThreeAsync()
awaitAll(oneAsync, twoAsync, threeAsync)
var a: Int = oneAsync.await()
var b: String = twoAsync.await()
var c: Long = threeAsync.await()
1 Like
You shouldn’t need awaitAll if you are using structured concurrency. The failed child will cancel the parent which will then cancel all its children.
2 Likes
Yes, I was trying to avoid awaiting each one individually. This pattern doesn’t let you “fail fast”.
Scenario
- Let’s say you make three concurrent calls over the network. The first call is the longest and the third call fails instantly.
- If you call await on each method, the earliest I can know about failure is equal to the time it takes for the first call to finish.
Other use cases
Fast failure
is just one of many possible use cases.
- Let’s say I only want to wait as long as I get at least one response?
- This is possible in scala by using the
select
composer. More info here
select
returns a Future
that is complete when the first of the given Future
s complete.
It returns that Future
together with a Seq containing the remaining uncompleted Futures.
- And there are other composers too, like:
join
and collect
- The different is that the scala let’s you compose
Future
/Promise
s directly and I believe this is not a recommended practice in Kotlin (the equivalent would be a Deferred
I guess).
I’m just trying to find out if I can do all these things in kotlin without having to write functions that return a Deferred
Read the bottom half of that page where it shows how to use async properly within a the context of a coroutineScope
call.
Not when utilizing structured concurrency. If they are sibling jobs and the parent isn’t a supervisor job, then one failure will immediately cancel all the others.
You get “fail fast” for free when using structured concurrency.
Kotlin’s select
method supports the same use case, though it’s more general. I prefer using Flow
instead of Deferred
in more complicated cases where I’m combining async data.
Kotlin has a variety of operators for combining async data when using Flow.
Code written using suspend
methods is just so much easier to read.
If you do write methods that start new coroutines, take a CoroutineScope as the receiver or a parameter and use that scope to launch the new coroutines. This gives you all the benefits of structured coroutines.
Yes, you can, it’s just a new set of tools.
Here’s some tips to get you started:
- Want an async result? Write a
suspend
method.
- Want to queue async data? Use
Channel
.
- Want an expression of concurrently determined values? Use
coroutineScope
with launch/async
inside.
- Want a stream of async data (especially if processing or combining that data)? use
Flow
.
1 Like
Thanks for the suggestion Nick, I’ll explore channels and flows.