Scala async/await and parallelization

15,062

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 Futures 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))

Share:
15,062
Sanete
Author by

Sanete

Updated on June 06, 2022

Comments

  • Sanete
    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
    Sanete over 10 years
    Very helpful explanation. Now I understand. Thanks! :-)
  • Freewind
    Freewind about 9 years
    Your answer is really I want to know when I read the docs about async/await, but none of them mentioned the doesn't actually block the executing thread things
  • Jose Cabrera Zuniga
    Jose Cabrera Zuniga over 2 years
    but, 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
    Vadym Chekan over 2 years
    Not 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