Scala 3 — Book

Concurrency

Language

When you want to write parallel and concurrent applications in Scala, you can use the native Java Thread—but the Scala Future offers a more high level and idiomatic approach, so it’s preferred, and covered in this chapter.

Introduction

Here’s a description of the Scala Future from its Scaladoc:

“A Future represents a value which may or may not currently be available, but will be available at some point, or an exception if that value could not be made available.”

To demonstrate what that means, let’s first look at single-threaded programming. In the single-threaded world you bind the result of a method call to a variable like this:

def aShortRunningTask(): Int = 42
val x = aShortRunningTask()

In this code, the value 42 is immediately bound to x.

When you’re working with a Future, the assignment process looks similar:

def aLongRunningTask(): Future[Int] = ???
val x = aLongRunningTask()

But the main difference in this case is that because aLongRunningTask takes an indeterminate amount of time to return, the value in x may or may not be currently available, but it will be available at some point—in the future.

Another way to look at this is in terms of blocking. In this single-threaded example, the println statement isn’t printed until aShortRunningTask completes:

def aShortRunningTask(): Int =
  Thread.sleep(500)
  42
val x = aShortRunningTask()
println("Here")

Conversely, if aShortRunningTask is created as a Future, the println statement is printed almost immediately because aShortRunningTask is spawned off on some other thread—it doesn’t block.

In this chapter you’ll see how to use futures, including how to run multiple futures in parallel and combine their results in a for expression. You’ll also see examples of methods that are used to handle the value in a future once it returns.

When you think about futures, it’s important to know that they’re intended as a one-shot, “Handle this relatively slow computation on some other thread, and call me back with a result when you’re done” construct. As a point of contrast, Akka actors are intended to run for a long time and respond to many requests during their lifetime. While an actor may live forever, a future eventually contains the result of a computation that ran only once.

An example in the REPL

A future is used to create a temporary pocket of concurrency. For instance, you use a future when you need to call an algorithm that runs an indeterminate amount of time—such as calling a remote microservice—so you want to run it off of the main thread.

To demonstrate how this works, let’s start with a Future example in the REPL. First, paste in these required import statements:

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{Failure, Success}

Now you’re ready to create a future. For this example, first define a long-running, single-threaded algorithm:

def longRunningAlgorithm() =
  Thread.sleep(10_000)
  42

That fancy algorithm returns the integer value 42 after a ten-second delay. Now call that algorithm by wrapping it into the Future constructor, and assigning the result to a variable:

scala> val eventualInt = Future(longRunningAlgorithm())
eventualInt: scala.concurrent.Future[Int] = Future(<not completed>)

Right away, your computation—the call to longRunningAlgorithm()—begins running. If you immediately check the value of the variable eventualInt, you see that the future hasn’t been completed yet:

scala> eventualInt
val res1: scala.concurrent.Future[Int] = Future(<not completed>)

But if you check again after ten seconds, you’ll see that it is completed successfully:

scala> eventualInt
val res2: scala.concurrent.Future[Int] = Future(Success(42))

While that’s a relatively simple example, it shows the basic approach: Just construct a new Future with your long-running algorithm.

One thing to notice is that the 42 you expected is wrapped in a Success, which is further wrapped in a Future. This is a key concept to understand: the value in a Future is always an instance of one of the scala.util.Try types: Success or Failure. Therefore, when you work with the result of a future, you use the usual Try-handling techniques.

Using map with futures

Future has a map method, which you use just like the map method on collections. This is what the result looks like when you call map right after creating the variable a:

scala> val a = Future(longRunningAlgorithm()).map(_ * 2)
a: scala.concurrent.Future[Int] = Future(<not completed>)

As shown, for the future that was created with the longRunningAlgorithm, the initial output shows Future(<not completed>). But when you check a’s value after ten seconds you’ll see that it contains the expected result of 84:

scala> a
res1: scala.concurrent.Future[Int] = Future(Success(84))

Once again, the successful result is wrapped inside a Success and a Future.

Using callback methods with futures

In addition to higher-order functions like map, you can also use callback methods with futures. One commonly used callback method is onComplete, which takes a partial function in which you handle the Success and Failure cases:

Future(longRunningAlgorithm()).onComplete {
  case Success(value) => println(s"Got the callback, value = $value")
  case Failure(e) => e.printStackTrace
}

When you paste that code in the REPL you’ll eventually see the result:

Got the callback, value = 42

Other Future methods

The Future class has other methods you can use. It has some methods that you find on Scala collections classes, including:

  • filter
  • flatMap
  • map

Its callback methods are:

  • onComplete
  • andThen
  • foreach

Other transformation methods include:

  • fallbackTo
  • recover
  • recoverWith

See the Futures and Promises page for a discussion of additional methods available to futures.

Running multiple futures and joining their results

To run multiple computations in parallel and join their results when all of the futures have been completed, use a for expression.

The correct approach is:

  1. Start the computations that return Future results
  2. Merge their results in a for expression
  3. Extract the merged result using onComplete or a similar technique

An example

The three steps of the correct approach are shown in the following example. A key is that you first start the computations that return futures, and then join them in the for expression:

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{Failure, Success}

val startTime = System.currentTimeMillis()
def delta() = System.currentTimeMillis() - startTime
def sleep(millis: Long) = Thread.sleep(millis)

@main def multipleFutures1 =

  println(s"creating the futures:   ${delta()}")

  // (1) start the computations that return futures
  val f1 = Future { sleep(800); 1 }   // eventually returns 1
  val f2 = Future { sleep(200); 2 }   // eventually returns 2
  val f3 = Future { sleep(400); 3 }   // eventually returns 3

  // (2) join the futures in a `for` expression
  val result =
    for
      r1 <- f1
      r2 <- f2
      r3 <- f3
    yield
      println(s"in the 'yield': ${delta()}")
      (r1 + r2 + r3)

  // (3) process the result
  result.onComplete {
    case Success(x) =>
      println(s"in the Success case: ${delta()}")
      println(s"result = $x")
    case Failure(e) =>
      e.printStackTrace
  }

  println(s"before the 'sleep(3000)': ${delta()}")

  // important for a little parallel demo: keep the jvm alive
  sleep(3000)

When you run that application, you see output that looks like this:

creating the futures:   1
before the 'sleep(3000)': 2
in the 'yield': 806
in the Success case: 806
result = 6

As that output shows, the futures are created very rapidly, and in just two milliseconds the print statement right before the sleep(3000) statement at the end of the method is reached. All of that code is run on the JVM’s main thread. Then, at 806 ms, the three futures complete and the code in the yield block is run. Then the code immediately goes to the Success case in the onComplete method.

The 806 ms output is a key to seeing that the three computations are run in parallel. If they were run sequentially, the total time would be about 1,400 ms—the sum of the sleep times of the three computations. But because they’re run in parallel, the total time is just slightly longer than the longest-running computation: f1, which is 800 ms.

Notice that if the computations were run within the for expression, they would be executed sequentially, not in parallel:

// Sequential execution (no parallelism!)
for
  r1 <- Future { sleep(800); 1 }
  r2 <- Future { sleep(200); 2 }
  r3 <- Future { sleep(400); 3 }
yield
  r1 + r2 + r3

So, if you want the computations to be possibly run in parallel, remember to run them outside the for expression.

A method that returns a future

So far you’ve seen how to pass a single-threaded algorithm into a Future constructor. You can use the same technique to create a method that returns a Future:

// simulate a slow-running method
def slowlyDouble(x: Int, delay: Long): Future[Int] = Future {
  sleep(delay)
  x * 2
}

As with the previous examples, just assign the result of the method call to a new variable. Then when you check the result right away you’ll see that it’s not completed, but after the delay time the future will have a result:

scala> val f = slowlyDouble(2, 5_000L)
val f: concurrent.Future[Int] = Future(<not completed>)

scala> f
val res0: concurrent.Future[Int] = Future(<not completed>)

scala> f
val res1: concurrent.Future[Int] = Future(Success(4))

Key points about futures

Hopefully those examples give you an idea of how Scala futures work. To summarize, a few key points about futures are:

  • You construct futures to run tasks off of the main thread
  • Futures are intended for one-shot, potentially long-running concurrent tasks that eventually return a value; they create a temporary pocket of concurrency
  • A future starts running as soon as you construct it
  • A benefit of futures over threads is that they work with for expressions, and come with a variety of callback methods that simplify the process of working with concurrent threads
  • When you work with futures you don’t have to concern yourself with the low-level details of thread management
  • You handle the result of a future with callback methods like onComplete and andThen, or transformation methods like filter, map, etc.
  • The value inside a Future is always an instance of one of the Try types: Success or Failure
  • If you’re using multiple futures to yield a single result, combine them in a for expression

Also, as you saw with the import statements in these examples, the Scala Future depends on an ExecutionContext.

For more details about futures, see Futures and Promises, an article that discusses futures, promises, and execution contexts. It also provides a discussion of how a for expression is translated into a flatMap operation.

Contributors to this page: