Scala: the global ExecutionContext makes your life easier

TL;DR – when in doubt, stick with scala.concurrent.ExecutionContext.global

When you want to run some asynchronous code, choosing a thread pool isn’t any fun. Scala has your back, with its global ExecutionContext.

When I try to put some code in a Future without specifying where to run it, Scala tells me what to do:

scala> Future(println(“Do something slow”))
:14: error: Cannot find an implicit ExecutionContext, either require one yourself or import ExecutionContext.Implicits.global
      
There are some good reasons to use that recommended ExecutionContext. It tries to do things right in several ways. See below for how you can help it along.

The global ExecutionContext has an objective of keeping your CPU busy while limiting time spent context switching between threads. To do this, it starts up a ForkJoinPool[3] whose desired degree of parallelism is the number of CPUs.[1]

ForkJoinPool is particularly smart, able to run small computations with less overhead. It’s more work for its users, who must implement each computation inside a ForkJoinTask. Scala’s global ExecutionContext takes this burden from you: any task submitted to the global context from within the global context is quietly wrapped in a ForkJoinTask.

But wait, there’s more! We also get special handling for blocking tasks. Scala’s Futures resist supplying their values unless you pass them to Await.result(). That’s because Future knows that its result may not be available yet, so this is a blocking call. The Await object wraps the access in scala.concurrent.blocking { … }, which passes the code on to BlockingContext.

The BlockingContext object says, “Hey, current Thread, do you have anything special to do before I start this slow thing?” and the special thread created inside the global ExecutionContext says, “Why yes! I’m going to tell the ForkJoinPool about this.”

The thread’s block context defers to the managedBlock method in ForkJoinPool, which activates the ForkJoinPool’s powers of compensation. ForkJoinPool is trying to keep the CPU busy by keeping degree-of-parallelism threads computing all the time. When informed that one of those threads is about to block, it compensates by starting an additional thread. This way, while your thread is sitting around, a CPU doesn’t have to. As a bonus, this prevents pool-induced deadlock.

In this way, Scala’s Futures and its global ExecutionContext work together to keep your computer humming without going Thread-wild. You can invoke the same magic yourself by wrapping any Thread-hanging code in blocking { … }.[2]

All this makes scala.concurrent.ExecutionContext.global an excellent general-purpose ExecutionContext.

When should you not use it? When you’re writing an asynchronous library, or when you know you’re going to do a lot of blocking, declare your own thread pool. Leave the global one for everyone else.

Also, on mobile: Daniel Solano-Gómez reports: On startup, the global execution context “has to read its configuration.  In many apps, that’s probably fine, but in an Android app it becomes a problem.  I was trying to use Futures to avoid doing I/O on the main thread, so it was a little bit of a surprise that doing that caused I/O on the main thread…. In the end, I just created my own based on an executor service.”

———-
[1] You can alter this: set scala.concurrent.context.numThreads to a hard number or to a multiple, such as “x2” for double your CPUs. The documentation is the source code.
[2] Here’s some code that illustrates using blocking { … } to get the global ExecutionContext to start extra threads.
[3] Scala has its own ForkJoinPool implementation, because Java doesn’t get it until 7, and Scala runs on Java 6 or higher.

5 thoughts on “Scala: the global ExecutionContext makes your life easier

  1. How about using something like `ExecutionContext.fromExecutor(new ForkJoinPool)`? the constructor of `ForkJoinPool` seems to create a pool identical to the default one when called without parameters, but this way you get e.g. full lifecycle control over your pool.

  2. The ForkJoinPool created there won't have the extra magic of Scala's global one, which converts blocking{…} code and other submitted work into ForkJoin tasks. If you care about thread pool lifecycle, and are paying attention to using the ForkJoinPool, then your suggestion is the right way.

  3. If you're doing this, be very careful to wrap everything blocking in \”blocking\”, or using Futures with particular dedicated execution context. The default one is nice, but if you're using it for everything and then accidentally drop some unmarked blocking tasks in there, your whole application will grind to a halt – and the only way to avoid having this happen is CONSTANT VIGILANCE.Better approach: If you don't care what execution context a piece of code runs on, you probably shouldn't be passing a particular execution context, or having the overhead of creating a new task, at all. So it's better to use scalaz Task for this kind of thing; if you want a Task to run on a particular execution context, you can create one, but if you just want to map with a small computation, you do it, and since everything's trampolined and lazy that will just run on whichever thread the big chunk of work ends up happening on. Then you can create executors for the important things that need a thread pool (e.g. your database access service, your http client), but you can keep them local to those things and don't need to keep threading implicit ExecutionContexts through all your functions. It's the best of both worlds.

Comments are closed.

Discover more from Jessitron

Subscribe now to keep reading and get access to the full archive.

Continue reading