Execution context is the place where a future is scheduled and executed. Execution context contains a threads pool from which threads are used to execute given tasks.

Below example shows creating an execution context using a single thread execution service.

val executor: ExecutorService = Executors.newFixedThreadPool(1)
 
val ec: ExecutionContext = ExecutionContext.fromExecutor(executor)

If we want to execute a future, we need to provide an implicit execution context to Future.apply so that it schedules the future in the execution service. For example,

  def heavyTask(sr: Int): Future[Unit] = Future {
    printWithThread(s"Task ${sr} has been started");
    Thread.sleep(5000)
  }

heavyTask calls Future to execute the task off main thread. Without an implicit execution context, it leads to compilation error.

Creating execution context

An execution context can be created using Executor and ExecutorService interfaces. These interfaces are only responsible for scheduling and executing the tasks.

Using Executor Interface

We can use Executors factory to create Executor implementations.

val executor = Executors.newSingleThreadExecutor()
 
given ec: ExecutionContext = ExecutionContext.fromExecutor(executor)

Now ec can be used to execute Futures.

Using ExecutorService

val executorService: ExecutorService = Executors.newFixedThreadPool(2)
given ec: ExecutionContext = ExecutionContext.fromExecutor(exectorService)

ec has executorService with a thread pool of two threads. This can schedule tasks concurrently on two threads.

Tips of using execution context

  1. Try to use different execution contexts for different part of the application. Avoid using same context everywhere. There might be different requirements of concurrency at different places.
  2. Using sensible names for thread pools

References

  1. Scala Execution Context - simply explained