Scala async/await and parallelization
Solution 1
Since it's similar to async & await
in C#, maybe I can provide some insight. In C#, it's a general rule that Task
that can be awaited should be returned 'hot', i.e. already running. I assume it's the same in Scala, where the Future
returned from the function does not have to be explicitly started, but is just 'running' after being called. If it's not the case, then the following is pure (and probably not true) speculation.
Let's analyze the first case:
async {
await(slowCalcFuture) + await(slowCalcFuture)
}
We get to that block and hit the first await:
async {
await(slowCalcFuture) + await(slowCalcFuture)
^^^^^
}
Ok, so we're asynchronously waiting for that calculation to finish. When it's finished, we 'move on' with analyzing the block:
async {
await(slowCalcFuture) + await(slowCalcFuture)
^^^^^
}
Second await, so we're asynchronously waiting for second calculation to finish. After that's done, we can calculate the final result by adding two integers.
As you can see, we're moving step-by-step through awaits, awaiting Future
s as they come one by one.
Let's take a look at the second example:
async {
val future1 = slowCalcFuture
val future2 = slowCalcFuture
await(future1) + await(future2)
}
OK, so here's what (probably) happens:
async {
val future1 = slowCalcFuture // >> first future is started, but not awaited
val future2 = slowCalcFuture // >> second future is started, but not awaited
await(future1) + await(future2)
^^^^^
}
Then we're awaiting the first Future
, but both of the futures are currently running. When the first one returns, the second might have already completed (so we will have the result available at once) or we might have to wait for a little bit longer.
Now it's clear that second example runs two calculations in parallel, then waits for both of them to finish. When both are ready, it returns. First example runs the calculations in a non-blocking way, but sequentially.
Solution 2
the answer by Patryk is correct if a little difficult to follow. the main thing to understand about async/await is that it's just another way of doing Future
's flatMap
. there's no concurrency magic behind the scenes. all the calls inside an async block are sequential, including await which doesn't actually block the executing thread but rather wraps the rest of the async block in a closure and passes it as a callback on completion of the Future
we're waiting on. so in the first piece of code the second calculation doesn't start until the first await has completed since no one started it prior to that.
Solution 3
In first case you create a new thread to execute a slow future and wait for it in a single call. So invocation of the second slow future is performed after the first one is complete.
In the second case when val future1 = slowCalcFuture
is called, it effectively create a new thread, pass pointer to "slowCalcFuture" function to the thread and says "execute it please". It takes as much time as it is necessary to get a thread instance from thread pool, and pass a pointer to a function to the thread instance. Which can be considered instant. So, because val future1 = slowCalcFuture
is translated into "get thread and pass pointer" operations, it is complete in no time and the next line is executed without any delay val future2 = slowCalcFuture
. Feauture 2 is scheduled to execution without any delay too.
Fundamental difference between val future1 = slowCalcFuture
and await(slowCalcFuture)
is the same as between asking somebody to make you coffee and waiting for your coffee to be ready. Asking takes 2 seconds: which is needed to say phrase: "could you make me coffee please?". But waiting for coffee to be ready will take 4 minutes.
Possible modification of this task could be waiting for 1st available answer. For example, you want to connect to any server in a cluster. You issue requests to connect to every server you know, and the first one which responds, will be your server. You could do this with:
Future.firstCompletedOf(Array(slowCalcFuture, slowCalcFuture))
Sanete
Updated on June 06, 2022Comments
-
Sanete almost 2 years
I'm learning about the uses of async/await in Scala. I have read this in https://github.com/scala/async
Theoretically this code is asynchronous (non-blocking), but it's not parallelized:
def slowCalcFuture: Future[Int] = ... def combined: Future[Int] = async { await(slowCalcFuture) + await(slowCalcFuture) } val x: Int = Await.result(combined, 10.seconds)
whereas this other one is parallelized:
def combined: Future[Int] = async { val future1 = slowCalcFuture val future2 = slowCalcFuture await(future1) + await(future2) }
The only difference between them is the use of intermediate variables. How can this affect the parallelization?
-
Sanete over 10 yearsVery helpful explanation. Now I understand. Thanks! :-)
-
Freewind about 9 yearsYour answer is really I want to know when I read the docs about
async/await
, but none of them mentioned thedoesn't actually block the executing thread
things -
Jose Cabrera Zuniga over 2 yearsbut, could a single thread be "spawned" into two other threads? If the + operation could allow the "parallel" evaluation of the addition, couldn't we have parallelism? Is there a "parallel version of +" in Scala?
-
Vadym Chekan over 2 yearsNot sure what you mean. Threads are not "spawned" onto other threads but tasks are. So, you can make any function returning a Future and thus execute it in a spawned thread. @JoseCabreraZuniga