Executors
Get a detailed introduction to the executors, supposedly a part of C++23.
We'll cover the following
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.