std::execution: Asynchronous Algorithms

std::execution supports many asynchronous algorithms for various workflows.

Presenting proposal P2300R10 is not easy. First, it is very powerful, and second, it is very long. Therefore, I concentrate on specific aspects of the proposal.

What are the priorities of this proposal?

Priorities

  • Be composable and generic, allowing users to write code that can be used with many different types of execution resources.
  • Encapsulate common asynchronous patterns in customizable and reusable algorithms, so users don’t have to invent things themselves.
  • Make it easy to be correct by construction.
  • Support the diversity of execution resources and execution agents, because not all execution agents are created equal; some are less capable than others, but not less important.
  • Allow everything to be customized by an execution resource, including transfer to other execution resources, but don’t require that execution resources customize everything.
  • Care about all reasonable use cases, domains and platforms.
  • Errors must be propagated, but error handling must not present a burden.
  • Support cancellation, which is not an error.
  • Have clear and concise answers for where things execute.
  • Be able to manage and terminate the lifetimes of objects asynchronously.

The terms execution resource, execution agent, and scheduler are essential for the understanding of std::execution. Here are the first simplified definitions.

An execution resource is a program resource entity that manages a set of execution agents. Examples of execution resources include the active thread, a thread pool, or an extra hardware accelerator.

Each function invocation runs in an execution agent.

A scheduler is an abstraction of an execution resource with a uniform, generic interface for scheduling work onto that resource. It is a factory for senders.

Hello World

Here’s once more the “Hello World” of std::execution. You can execute the program on Compiler Explorer this time, and my analysis will go deeper.

// HelloWorldExecution.cpp

#include <exec/static_thread_pool.hpp>
#include <iostream>
#include <stdexec/execution.hpp>


int main() {
  exec::static_thread_pool pool(8);
  auto sch = pool.get_scheduler();

  auto begin = stdexec::schedule(sch);
  auto hi = stdexec::then(begin, [] {
    std::cout << "Hello world! Have an int.\n";
    return 13;
  });
  auto add_42 = stdexec::then(hi, [](int arg) { return arg + 42; });

  auto [i] = stdexec::sync_wait(add_42).value();

  std::cout << "i = " << i << '\n';
}

First, here’s the output of the program:

The program begins by including the necessary headers: <exec/static_thread_pool.hpp> for creating a thread pool, and <stdexec/execution.hpp> for execution-related utilities.

 

Rainer D 6 P2 500x500Modernes C++ Mentoring

  • "Fundamentals for C++ Professionals" (open)
  • "Design Patterns and Architectural Patterns with C++" (open)
  • "C++20: Get the Details" (open)
  • "Concurrency with Modern C++" (open)
  • "Generic Programming (Templates) with C++": October 2024
  • "Embedded Programming with Modern C++": October 2024
  • "Clean Code: Best Practices for Modern C++": March 2025
  • Do you want to stay informed: Subscribe.

     

    In the main function, a static_thread_pool pool is created with 8 threads.

    The get_scheduler member function of the thread pool is called to obtain a lightweight handle for the execution resource scheduler object named sch, which will be used to schedule senders on the thread pool. In this case, the execution resource is a thread pool, but it could also be the main thread, the GPU, or a task framework

    The program then creates a series of senders executed by executing agents.

    The first sender, begin, is created using the stdexec::schedule function, which schedules a sender on the specified scheduler sch. stdexec::schedule is a so-called sender factory. There are more sender factories. I use the namespace of the upcoming standard:

    execution::schedule
    execution::just
    execution::just_error
    execution::just_stopped
    execution::read_env
    

    The next sender, hi, uses the sender adaptor stdexec::then, which takes the begin sender and a lambda function. This lambda function prints “Hello world! Have an int.” to the console and returns the integer value 13. The third sender, add_42, is also created using the sender adaptor stdexec::then. The continuation takes the hi task and another lambda, which takes an integer argument arg and returns the result of adding 42 to it. Senders run asynchronously and are generally composable.

    std::execution offers more sender adapters:

    execution::continues_on
    execution::then
    execution::upon_*
    execution::let_*
    execution::starts_on
    execution::into_variant
    execution::stopped_as_optional
    execution::stopped_as_error
    execution::bulk
    execution::split
    execution::when_all
    

    In contrast to the senders, runs the sender consumer synchronously. The stdexec::sync_wait call waits for the completion of the add_42 sender. The value method is called on the result of sync_wait to obtain the value produced by the sender, which is unpacked into the variable i.

    this_thread::sync_wait is the only sender consumer in the execution framework.

    What’s Next?

    In my next post, I will analyze a more challenging algorithm.

    Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, Philipp Lenk, Charles-Jianye Chen, Keith Jeffery, Matt Godbolt, and Honey Sukesan.

    Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.

    My special thanks to Embarcadero
    My special thanks to PVS-Studio
    My special thanks to Tipi.build 
    My special thanks to Take Up Code
    My special thanks to SHAVEDYAKS

    Modernes C++ GmbH

    Modernes C++ Mentoring (English)

    Do you want to stay informed about my mentoring programs? Subscribe Here

    Rainer Grimm
    Yalovastraße 20
    72108 Rottenburg

    Mobil: +49 176 5506 5086
    Mail: schulung@ModernesCpp.de
    Mentoring: www.ModernesCpp.org

    Modernes C++ Mentoring,