Futures in Scala provide an easy way to write non blocking asynchronous code. But before going into the details of the Futures and how to carefully use them, let’s first of all develop a deeper understanding of the term asynchronous code.
Synchronous vs Asynchronous Execution
Let’s take a simple example of reading numbers from the database and adding them up.
Case 1
var globalSum = 0 for (i <- 1 to 100) { val num = makeDbCallSync() globalSum += num }
Seeing this code you may come up with following thoughts
- This above piece of code is a Synchronous Computation because the thread gets blocked every-time we make a DBCall
- This program is inefficient because it blocks the execution of the main thread every-time it has to get something from the DB. We can have multiple threads doing DB calls simultaneously
Case 2
val globalSum = new AtomicInteger(0) def createThreadExecution(from: Int, to: Int): Thread = { new Thread(new Runnable { override def run(): Unit = { for (i <- from to to) { val num = makeDbCallSync() globalSum.addAndGet(num) } } }) } val thread1 = createThreadExecution(0, 50) val thread2 = createThreadExecution(51, 100) thread1.join() thread2.join()
Seeing this code you may come up to the following thoughts
- Yay, we like it more because we have increased the number of threads , so now the CPU is more busy because when one of the threads get blocked due to DB Call, the other thread can be picked up by the CPU
- So do we call this execution as Synchronous or Asynchronous ??? Keep your answers to yourselves
Case 3
val executorService = new ThreadPoolExecutor() val futures = (1 to 100).map(_ => makeDbCallAsync()(ec)) val nums = Await.result(Future.sequence(futures)(ec)) val globalSum = nums.sum
Some time before, here the thoughts which came to my head on seeing this
- This is an asynchronous execution. In this execution strategy, we return the future ASAP and then accumulate the results from those futures later on.
Now lets try to understand whether executions are synchronous or asynchronous
- If we look closely enough, in each of the three cases we have blocking statements.
- In case 1, we have a makeDbCallSync which is a blocking execution
- In case 2, we have a thread.join() which is a blocking execution. Along with that we are also using makeDbCallSync which is also a blocking execution
- In case 3, however we are not using the makeDbCallSync instead we are using the makeDbCallAsync, which is a non blocking statement. But in case 3, we are using the Await.result which will block the execution of the statements.
Hence in all of these 3 cases, we have statements where we have synchronous computation in some form. So hence this makes the executions synchronous. So now lets make the definition of the Synchronous and Asynchronous executions clear
Synchronous Execution
Synchronous executions are executions where the thread executing the set of statements or instructions gets blocked by the kernel. This blocking might happen due to following
- Thread issued a read / write request and is now waiting for Disk IO to complete
- Thread issues a Network Read / Write request and is now waiting for the packets to be send or received
Asynchronous Execution
Asynchronous Executions are executions where the executing thread does not gets blocked due to any Disk or Network IO. In Asynchronous execution, the thread issues the IO request and then continue executing the main sequence of instructions. In Asynchronous execution, whenever the thread issues an IO Request it is handed over a future which it can use later on to get the final result. So essentially the execution just uses the future and does not not blocked on it is asynchronous execution.
val globalSum = new AtomicInteger(0) val executorService = new ThreadPoolExecutor() val futures = (1 to 100).map(_ => makeDbCallAsync()(ec)) // Till here we have all the calls as async , // hence no execution is getting blocked on IO val nums = Await.result(Future.sequence(futures)(ec)) // This Await.result is a blocking call which waits for the // DB calls to finish val globalSum = nums.sum
But after getting the future, if we wait on the result on the future that means that we can changed over asynchronous execution to a synchronous execution.
Generally when people refer to asynchronous or synchronous coding style, they generally refer whether they are tossing futures around between functions or are they passing concrete values between functions or classes. Take these two code samples for eg.
Synchronous Version: Whisky on the Rocks
trait BarItem case class Soda() extends BarItem case class Whisky() extends BarItem case class Ice() extends BarItem case class Drink(items: Seq[BarItem]) extends BarItem private def getMeSync(item: String): BarItem = { dbCall(item) } def prepareDrinkSync(): Drink = { val sodaFut = getMeSync("soda")) val whisky = getMeSync("whisky")) val ice = getMeSync("ice")) Drink(Seq(sodaFut, whisky, ice)) }
Asynchronous Version: Whisky on the Rocks
private def getMeAsync(item: String): Future[BarItem] = { Future { dbCall(item) } } def prepareDrinkAsync(): Future[Drink] = async { val sodaFut = await(getMeAsync("soda")) val whisky = await(getMeAsync("whisky")) val ice = await(getMeAsync("ice")) Drink(Seq(sodaFut, whisky, ice)) }
We can clearly see that in the first example, we are having a synchronous computation as the function is returning the concrete instance of Drink and in the second example, we are having a asynchronous computation because the function is returning a Future[Drink]
Conclusions
Now till now we understood that every function can be written as synchronous as well asynchronous depends on whether it returns a concrete instance or Future Instance. However do not consider this as the full truth because there might be some cases async computation in which the function gets blocked on some computation internally but still returns the Future.
So when to use sync or async. So if you were to ask me , one should try to follow asynchronous coding style which means that functions or classes which deal with any kind of IO , should return Futures,
- First it becomes the responsibility of the caller to Sync on it or not. But if the function sends the concrete value i.e. without Future, then automatically the caller function becomes synchronous in nature because then , the lower function does not expose a asynchronous API for the resource
- Second with Futures you need not worry about taking the full advantage of the multicore processors. In case of synchronous execution, like in case 1, we could easily have the case where we are not able to schedule the tasks on multiple cores. But in case of asynchronous execution, like in case before the await.result , we would easily take the advantage of the multi core processors because then all the DB calls are itself getting executed on different threads of an executorService, so essentially we are able to parallelize the calls in a much better way in case of asynchronous execution.