Futures and Concurrency
Prerequisites: For-comprehensions
A Future[A] is a value that will be available later — the result of
work running on another thread.
import scala.concurrent.{Await, Future}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.*
def fetchPrice(item: String): Future[Double] = Future {
Thread.sleep(1000)
item.length * 1.5
}
@main def futures(): Unit =
val start = System.nanoTime()
val a = fetchPrice("keyboard")
val b = fetchPrice("mouse")
val total = for
x <- a
y <- b
yield x + y
println(Await.result(total, 2.seconds))
val elapsed = (System.nanoTime() - start) / 1_000_000
println(elapsed < 1800)
19.5
true
Future { ... }starts the work immediately on a thread pool — the execution context (agivenhere, imported fromImplicits.global).aandbwere started before the for-comprehension, so they ran in parallel: the total took about one second, not two. (Starting them inside theforwould run them one after the other.)map,flatMap, and for-comprehensions combine futures without blocking.Await.resultblocks — acceptable at the edge of a program (likemain) or in tests, but avoid it elsewhere.
Many futures at once
import scala.concurrent.{Await, Future}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.*
def square(n: Int): Future[Int] = Future { Thread.sleep(100); n * n }
@main def traverse(): Unit =
val all: Future[List[Int]] = Future.traverse(List(1, 2, 3, 4))(square)
println(Await.result(all, 2.seconds))
List(1, 4, 9, 16)
Future.traverse (and Future.sequence) turn many futures into one
future of a list, preserving order.
Failures
import scala.concurrent.{Await, Future}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.*
import scala.util.{Failure, Success}
@main def failures(): Unit =
val risky = Future { if true then throw IllegalStateException("service down") else 1 }
val recovered = risky.recover { case e: IllegalStateException => -1 }
println(Await.result(recovered, 1.second))
Await.ready(risky, 1.second).value.get match
case Success(v) => println(s"value $v")
case Failure(e) => println(s"failed: ${e.getMessage}")
-1
failed: service down
A failed Future holds the exception; recover and recoverWith turn
failures into values.
Limits of Future
Futures start running as soon as they're created, can't be cancelled, and mix describing work with running it. For complex concurrent systems, most Scala teams use an effect system — the next lesson.