In my opinion many of the problems come from a misunderstanding or misconception on how Futures work. (e.g. the strict nature of Futures and the way they interact with Execution Contexts to name a few).

That’s enough of an excuse to dive into the Scala Future and ExecutionContext implementation.

This is a very basic code. The main program execute 2 tasks. Each tasks does nothing but wait a few seconds to simulate some execution (e.g. fetching something from a database or over the network, writing to a file, …).

Both tasks are executed sequentially on the same thread as the main program. The first tasks takes one second to complete and the second task takes 2. No doubt that we are wasting resources as the main thread should be able to continue execution while the tasks are executed. Also assuming no dependencies between the tasks we should be able to run them concurrently.

Asynchronous computation

In order to do it we need an asynchronous computation and in Scala this is typically done by using a Future. So let’s just wrap our tasks into a Future.

Basically a Future is just a placeholder for something that doesn’t exist yet. Here our 2 tasks return a Future[Unit], that is a placeholder that will contain Unit when the task completes. The placeholder (the Future) is returned straight away, before the task is actually executed.

As you can see we need an ExecutionContext in order to be able to use a Future. Don’t worry we’re going to see what is an ExcecutionContext in detail but for now let’s just use the global execution context as suggested by the compiler.

Already completed Futures

It’s a Future of a successful Promise. But what is a Promise? Well it’s something that gives a Future, obviously! A Future is a placeholder for a result that can be read when available. A Promise is like a “writable-once” container that can be used to complete a Future with the written value (“writable-once” is the reason why DefaultPromise extends AtomicReference).

In our case it’s an already kept promised (because we already know the value to use to complete the promise).

All futures constructed from an already known value (Futures constructed from a Try including Future.successful(), Future.failed() and Future.fromTry()) are implemented as a Kept promise (i.e. an already completed promise). It means that there is no computation executed in theses cases. The available value is just wrapped in the corresponding Future implementation using the current thread.

If you find yourself in such a situation remember to use one of these 3 constructors as it avoids unnecessary thread context switches.

So map is defined in terms of transform. And transform takes a function Try[T] => Try[S]. It can transform a successful future into a failed future and vice versa. It’s more general than map which can only transform a successful future into another successful future. (In fact map, recover and failed.map are all implemented in terms of transform).

However transform is not implemented directly on the Future trait. In our case we are calling transform on Future.unit which is actually a Kept Promise and transform is implemented in its superclass scala.concurrent.impl.Promise.

Here we create a new Promise. However this time it’s not a Kept Promise but a DefaultPromise, a promise that is not already fulfilled. Then onComplete is called with a callback that completes p, the DefaultPromise, by applying the transformation f to the result of the current future.

Remember that we are calling transform on Future.unit which is a Kept promise and this is where onComplete is defined.

In fact it’s no coincidence that it’s very similar to a Java Executor:

public interface Executor {
void execute(Runnable commande);
}

By now it should be clear how the callbacks are now executed onto a different thread (depending on the ExecutionContext passed in). That’s why using Future { } to construct a Future from a value already available is an anti-pattern.

But before we jump onto the execution context details let’s keep focused on the implementation of Futures and Promises as they are still a few important things to cover.

DefaultPromises as a state machine

DefaultPromise[T] implements both Future[T] and AtomicReference[AnyRef](Nil).

Extending AtomicReference is a clever trick because a Promise can be seen as a state-machine moving from the incomplete state to the complete (or fulfilled) state. Using an AtomicReference makes sure there is only one possible transition from incomplete to complete.

Initially the atomic reference is the empty list Nil and tryCompleteAndGetListeners can change the atomic reference from List[_] to Try[T] only once.

If the atomic reference is a Try we know the promise is fulfilled otherwise it is still incomplete. Moreover using an atomic reference allows to add more callbacks in a thread-safe and non-blocking way.

There is a third state worth noting. This is when the promise is link to another promise which is particularly useful in case of chained promises.

For instance we have seen how a call to transform creates a new Promise. By doing so we can end up with a chain of promises. This is not really a problem for transform because as soon as the root promise completes so does all of the promises down the chain.

However for flatMap (which is implemented using transformWith) things are more complicated. Like transform, transformWith creates a new Promise p. But this time the callback also returns a Future which might as well be a DefaultPromise. This new Promise can only complete when the promise p does. Therefore it’s safe to “link” this promise to p and add all of its callbacks to the callbacks of p. It’s like if the whole promise chain gets “collapsed” onto the root promise. This is a very useful thing to do as it may avoid some memory links (more details here)

Execution contexts

Similarly to the Java Executor the Scala ExecutionContext allows to separate the business logic (i.e. what the code does) from the execution logic (i.e. how the code is executed).

As a consequence one cannot just import the global execution context and get away with that. Instead we need to understand which execution context is needed and why.

The global execution context

As the global execution context is the default one and the most easy one to setup let’s have a look at what it actually is.

First thing to note is that the ExecutionContext.global and the ExecutionContext.Implicits.global are actually the exact same thing:

It started 8 tasks at once, then waited for these tasks to complete and started new tasks as the old ones finish. Overall it took 6 seconds to complete 20 tasks (each of them sleeping 2 seconds). That’s because we needed 3 rows to complete all the tasks (8 tasks completed in 2s, then another 8 tasks completed in 2s, then the remaining 4 tasks completed in 2s).

Looks OK, but why 8 threads are used and not 4 or 16, or anything else. Well the answer is in the code of the createDefaultExecutorService:

When the property scala.concurrent.context.maxThreads is not set it defaults to the number of available cores on the system. In my case it’s 8. It might be different on your machine. More importantly it means that it’s quite likely that the settings are going to be different between the developper environment and the production environment. Something worth to keep in mind in case of performance issues …

Now let’s see what happens if we change the parallelism settings. For instance let’s set this property to 1 in the build.sbt:

All we have done is wrap our computation into a scala.concurrent.blocking call. Now if we run our program again it starts all the tasks at once and completes in only 2s.

That’s because the global execution context is “blocking” aware and starts extra threads for blocking computations.
This setting is controlled by the property scala.concurrent.context.maxExtraThreads which defaults to 256.

However it’s only useful for IO-bound tasks where the threads are left idle waiting for a resource to be available. It’s useless for CPU-bound task where the threads is kept busy.

Note that not all ExecutionContext implements the BlockingContext trait. For such ExecutionContext wrapping computation into a blocking call has no effects.

Finally as the global ExecutionContext is backed by a ForkJoinPool it makes it more suited for CPU-bound tasks.

Other type of ExecutionContexts

Having a fromExecutor constructor for the ExecutionContext means that it’s quite easy to create an ExecutionContext from any of the Java executor service like FixedThreadPoolExecutor, SingleThreadPoolExecutor, CachedThreadPoolExecutor, …).

Execution contexts are not limited to the Java world, there are many more Execution context available: Monix schedulers and Akka dispatchers also implement the ExecutionContext.

Conclusion

This has been quite an involved journey (according to the length of this post) but by now it should be clear what are the implication of using Future and choosing an ExecutionContext.

As a summary here are the main pitfalls to avoid:

Using the global ExecutionContext for non-cpu bound tasks (e.g. Prefer a dedicated ThreadPool for IO-bound tasks)

Not using “blocking” construct (Always useful, even for “documenting” the code)