Executors

Get a detailed introduction to the executors, supposedly a part of C++23.

Executors have quite a history in C++. The discussion began in early 2010. For the details, Detlef Vollmanns gives in his presentation Finally Executors for C++ an excellent overview.

My introduction to executors is mainly based on the proposals for the design of executors P0761, and for their formal description P0443. I also refer to the relatively new Modest Executor Proposal P1055.

What are executors?

Executors are the basic building blocks for execution in C++ and fulfill a similar role for execution, such as allocators for the containers in C++. Many proposals for executors are published, and many design decisions are still open. They should be part of C++23, but can probably be used much earlier to extend the C++ standard.

An executor consists of rules about where, when, and how to run a callable.

  • Where: The callable may run on an internal or external processor, and that the result is read back from the internal or external processor.
  • When: The callable may run immediately or just be scheduled.
  • How: The callable may run on a CPU or GPU or even be executed in a vectorized way.

The concurrency and parallelism features of C++ heavily depend on executors as building blocks for execution. This dependency holds for existing concurrency features, such as the parallel algorithms of the Standard Template Library, but also for new concurrency features, such as latches and barriers, coroutines, the network library, extended futures, transactional memory, or task blocks.

First examples

The following code snippets should give you a first impression of executors.

Using an executor

  • The promise std::async
    // get an executor through some means
    my_executor_type my_executor = ...
    
    // launch an async using my executor
    auto future = std::async(my_executor, [] {
      std::cout << "Hello world, from a new execution agent!" < '\n';
    });
    
  • The STL algorithm std::for_each
    // get an executor through some means
    my_executor_type my_executor = ...
    
    // execute a parallel for_each "on" my executor
    std::for_each(std::execution::par.on(my_executor),
                data.begin(), data.end(), func);
    

Obtaining an executor

There are various ways to obtain an executor.

  • From the execution context static_thread_pool

    // create a thread pool with 4 threads
    static_thread_pool pool(4);
    
    // get an executor from the thread pool
    auto exec = pool.executor();
    
    // use the executor on some long-running task
    auto task1 = long_running_task(exec);
    
  • From the system executor

    The system executor is the default executor used if not specified otherwise.

  • From an executor adapter

    // get an executor from a thread pool
    auto exec = pool.executor();
    
    // wrap the thread pool's executor in a logging_executor
    logging_executor<decltype(exec)> logging_exec(exec);
    
    // use the logging executor in a parallel sort
    std::sort(std::execution::par.on(logging_exec), my_data.begin(), my_data.end());
    

Get hands-on with 1400+ tech skills courses.