Published 2013-06-03.
Last modified 2014-11-13.
Time to read: 4 minutes.
Composable futures are a powerful concurrency mechanism – the one you should consider first if you need non-blocking, asynchronous computations. This lecture features a tiny implementation of a WriteOnce storage class, Promise and Future.
This lecture, and most of the other lectures in this section, is updated and expanded material which was originally published in my book, Composable Futures with Akka 2.0. That book is now out of date; it only pertained to Scala 2.10.
The sample code for this lecture can be found in
courseNotes/.
A Future
is a container which will assume a value when a computation completes or a result becomes available at a future time.
This lecture pertains to the Future class and companion object in the scala.concurrent package.
Scala Futures:
- Manage multi-threading asynchronously (unlike Scala parallel collections, which are synchronous).
- Are higher-level than j.u.c. primitives.
- Are immutable.
- Are inexpensive to create.
- Can be preset to a value or exception when created.
- Are non-blocking, however blocking operations are supported.
- Are composable.
Methods that return a Future instead of blocking for a result provide fast, asynchronous, non-blocking code.
These methods are called asynchronous.
A Future Can Start With a Promise
A Future[T] starts an asynchronous computation on another execution context and makes the result
available to the original execution context once it has completed.
Futures are writable once and readable many times.
They are usually created by passing in a closure (discussed in the
Closures lecture of the
Introduction to Scala course) or a Function
(discussed in the Functions are First Class and
More Fun With Functions lectures of the same course),
so they usually start off without a value.
The two states of a Future are pending or completed.
The value of a Future[T] might be of type T or it might be an Exception.
This is possible because the internally, a Future’s value is stored in an instance of Try[T].
Futures are executed as soon as an execution context is available to perform the work.
Normally when you hear “another execution context”, you think “another thread”,
but there are situations where there is only one thread ever available,
so for single-threaded programs like those produced by NodeJS, many execution contexts are interleaved on one thread.
This is good when you do not have hardware support for multi-threading, but is quite inefficient otherwise.
In many cases where multi-threading is enabled, a Future will start computing the value on another
thread immediately, but you cannot assume this to always be the case, because another thread may not be available right away.
A Promise is a signaling mechanism which exists in the same execution context as it was created on.
A Promise can be created complete with a value or exception,
or it can be completed with a value or exception at later time.
Unlike Futures, promises are fulfilled on demand.
The value of a Promise is held by an internal Future.
Internally, a Promise stores the value more or less as type Future[Try[T]].
We will explore the storage mechanism in more detail in a moment.
This means that Promises also have two states, just like Futures: pending or completed.
When complete, a Promise might have a Success[T] value, or it might contain a Throwable.
-
Invoking
Promise.complete(newValue)triggers a transition from statePendingtoCompletedwhile storingSuccess(newValue)as the value of thePromise. -
Invoking
Promise.failure(throwable)triggers a transition from statePendingtoCompletedwhile storingFailure(throwable)as the value of thePromise.
This might seem confusing, so let’s examine a simplified implementation of futures so we can understand how this really works.
I only show you this code so you can see how Promise relates to Future, and how value storage works.
Although TinyPromise and the write-once mechanism are reasonable facsimiles of how the real Scala implementation works,
the TinyFuture implementation is so minimal that it could teach bad habits.
Beware!
WriteOnce Immutable Type
One of the main concepts behind Scala futures is a write-once variable that can store one of two types of information: a parametric value or a Throwable.
I’ve defined the WriteOnce class that is faithful to the documented behavior of Futures and Promises.
The following code is provided in TinyFuture.scala and is defined in a TinyFuture object.
Once you get familiar with this code, understanding Scala’s Futures and Promises will be easy.
If you need some help remembering what the exists combinator does,
review the Combinators lecture which was presented earlier in this course.
class WriteOnce[T] {
private var value: Option[Try[T]] = None
def isCompleted = value.isDefined
def isFailure = value.exists(_.isFailure)
def isSuccess = value.exists(_.isSuccess)
/** Create an instance with a failure value (a Throwable instance) */
def fail(throwable: Throwable): Unit = {
if (value.isEmpty) value = Some(Failure(throwable)) else
throw new Exception(WriteOnce.alreadySetMSg)
()
}
/** Create an instance with a success value */
def set(newValue: T): T = {
if (value.isEmpty) value = Some(Success(newValue)) else
throw new Exception(WriteOnce.alreadySetMSg)
newValue
}
/** Get success value */
def success: T = value.map {
case v if v.isSuccess => v.get
case v => throw new Exception("WriteOnce is a failure, there is no value stored.")
}.getOrElse(throw new Exception(WriteOnce.stillPendingMsg))
/** Get failure value (a Throwable instance) */
def failed: Throwable = value.map {
case v if v.isFailure => v.failed.get
case v => throw new Exception("WriteOnce is a success, there is no Throwable stored.")
}.getOrElse(throw new Exception(WriteOnce.stillPendingMsg))
}
object WriteOnce {
val stillPendingMsg = "WriteOnce is still pending, there is no value yet."
val alreadySetMSg = "Value of WriteOnce is already set"
}
We can create a WriteOnce instance in various ways and use them, like this:
object WriteOnceDemo extends App {
import TinyFuture.WriteOnce
val writeOnce1 = new WriteOnce[Int]()
writeOnce1.set(42)
println(s"writeOnce1.isCompleted=${writeOnce1.isCompleted}")
println(s"writeOnce1.isFailure=${writeOnce1.isFailure}")
println(s"writeOnce1.isSuccess=${writeOnce1.isSuccess}")
println(s"writeOnce1.success=${writeOnce1.success}")
try { writeOnce1.failed } catch { case exception: Throwable => println(s"writeOnce1.failed: ${exception.getMessage}") }
println()
val writeOnce2 = new WriteOnce().set(420)
println(s"writeOnce2.isCompleted=${writeOnce2.isCompleted}")
println(s"writeOnce2.isFailure=${writeOnce2.isFailure}")
println(s"writeOnce2.isSuccess=${writeOnce2.isSuccess}")
println(s"writeOnce2.success=${writeOnce2.success}")
try { writeOnce2.failed } catch { case exception: Throwable => println(s"writeOnce2.failed: ${exception.getMessage}") }
println()
val writeOnce3 = new WriteOnce().fail(new Exception("You reached the end of the Internet"))
println(s"writeOnce3.isCompleted=${writeOnce3.isCompleted}")
println(s"writeOnce3.isFailure=${writeOnce3.isFailure}")
println(s"writeOnce3.isSuccess=${writeOnce3.isSuccess}")
try { writeOnce3.success } catch { case exception: Throwable => println(s"writeOnce3.success: ${exception.getMessage}") }
println(s"writeOnce3.failed=${writeOnce3.failed}")
}
You can run this by typing:
$ sbt "runMain multi.futures.WriteOnceDemo" writeOnce1.isCompleted=true writeOnce1.isFailure=false writeOnce1.isSuccess=true writeOnce1.success=42 writeOnce1.failed: WriteOnce is a success, there is no Throwable stored.
writeOnce2.isCompleted=true writeOnce2.isFailure=false writeOnce2.isSuccess=true writeOnce2.success=420 writeOnce2.failed: WriteOnce is a success, there is no Throwable stored.
writeOnce3.isCompleted=true writeOnce3.isFailure=true writeOnce3.isSuccess=false writeOnce3.success: WriteOnce is a failure, there is no value stored. writeOnce3.failed=java.lang.Exception: You reached the end of the Internet
TinyFuture Implementation
My tiny Future implementation is rather short and simple.
Because it does not support concurrency it just shows how the WriteOnce behavior supports the Future.value getter.
Getting the value of this tiny Future implementation is very different than how you retrieve values from the
real Scala Future implementation.
Although this code is simple and easy to understand,
it is a very naïve implementation and its only useful for showing how Futures work with WriteOnce values.
I will show you how to extract values from real Scala Futures later.
class Future[T] {
val value = new WriteOnce[T]
def isCompleted = value.isCompleted
}
object Future {
def apply[T](value: => T) = {
val future = new Future[T]()
try {
future.value.set(value)
} catch {
case e: Exception => future.value.fail(e)
}
future
}
def failed[T](throwable: Throwable) = {
val future = new Future[T]
future.value.fail(throwable)
future
}
def successful[T](value: T): Future[T] = Future(value)
}
The TinyFuture companion object has two methods that are just like two methods of the real Scala
Future’s companion object of the same name:
successful
and failed.
These methods create completed Futures from the values passed to them; no extra threads or context switch is required.
These methods are useful when you need to supply a Future containing a value that does not require computation as an argument to a method call,
or for use in a for-comprehension.
Tiny Promise Implementation
Next is the tiny version of Promise.
Even though it is quite simple, this code is reasonably faithful to the Scala implementation because Promise
does not require concurrency support; that is the job of Future.
class Promise[T] {
val future = new Future[T]
def complete(result: Try[T]): Promise[T] = {
if (result.isSuccess)
future.value.set(result.get)
else
future.value.fail(result.failed.get)
this
}
def failure(cause: Throwable): Promise[T] = {
future.value.fail(cause)
this
}
def success(value: T): Promise[T] = {
future.value.set(value)
this
}
final def tryCompleteWith(other: Future[T]): Promise[T] = {
if (!future.isCompleted) {
if (future.value.isSuccess)
future.value.set(other.value.success)
else
future.value.fail(other.value.failed)
}
this
}
def tryFailure(cause: Throwable): Boolean = {
if (future.isCompleted) {
false
} else {
future.value.fail(cause)
true
}
}
def trySuccess(value: T): Boolean = {
if (future.isCompleted) {
false
} else {
future.value.set(value)
true
}
}
}
object Promise {
def apply[T](): Promise[T] = new Promise[T]
def failed[T](throwable: Throwable): Promise[T] = {
val promise = new Promise[T]
promise.future.value.fail(throwable)
promise
}
def successful[T](value: T): Promise[T] = {
val promise = new Promise[T]
promise.future.value.set(value)
promise
}
}
TinyFutureDemo
Once again, this demo code does NOT show you how you would obtain values from real Scala Futures.
I only show you this code so you can see how WriteOnce and Promise relate to Future.
The fragments highlighted in yellow are what I want you to notice;
the fragments highighted in black are NOT how you should work with futures or promises,
they are just there for this quick little demo.
Don’t pick up these bad habits.
object TinyFutureDemo extends App {
import TinyFuture._
val promise1 = Promise.successful(1)
println(s"Value of promise1 = ${promise1.future.value.success}")
val promise2 = Promise.failed(new Exception("The knurblefritz has been badly gurbled"))
println(s"Throwable of promise2 = ${promise2.future.value.failed}")
val future1 = Future.successful(3+5)
println(s"value of future1 = ${future1.value.success}")
val future2 = Future.failed(new Exception("The omdedomdom has been hoomified"))
println(s"Throwable of future2 = ${future2.value.failed.getMessage}")
}
You can run this by typing:
$ sbt "runMain multi.futures.TinyFutureDemo" Value of promise1 = 1 Throwable of promise2 = java.lang.Exception: The knurblefritz has been badly gurbled value of future1 = 8 Throwable of future2 = The omdedomdom has been hoomified
© Copyright 1994-2024 Michael Slinn. All rights reserved.
If you would like to request to use this copyright-protected work in any manner,
please send an email.
This website was made using Jekyll and Mike Slinn’s Jekyll Plugins.