1. Introduction
This paper proposes a self-contained design for a Standard C++ framework for managing asynchronous execution on generic execution contexts. It is based on the ideas in [P0443R14] and its companion papers.
1.1. Motivation
Today, C++ software is increasingly asynchronous and parallel, a trend that is likely to only continue going forward. Asynchrony and parallelism appears everywhere, from processor hardware interfaces, to networking, to file I/O, to GUIs, to accelerators. Every C++ domain and every platform need to deal with asynchrony and parallelism, from scientific computing to video games to financial services, from the smallest mobile devices to your laptop to GPUs in the world’s fastest supercomputer.
While the C++ Standard Library has a rich set concurrency primitives (
,
,
, etc) and lower level building blocks (
, etc), we lack a Standard vocabulary and framework for asynchrony and parallelism that C++ programmers desperately need.
/
/
, C++11’s intended exposure for asynchrony, is inefficient, hard to use correctly, and severely lacking in genericity, making it unusable in many contexts.
We introduced parallel algorithms to the C++ Standard Library in C++17, and while they are an excellent start, they are all inherently synchronous and not composable.
This paper proposes a Standard C++ model for asynchrony, based around three key abstractions: schedulers, senders, and receivers, and a set of customizable asynchronous algorithms.
1.2. Priorities
-
Be composable and generic, allowing users to write code that can be used with many different types of execution contexts.
-
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 contexts 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 context, including transfer to other execution contexts, but don’t require that execution contexts 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.
1.3. Examples: End User
In this section we demonstrate the end-user experience of asynchronous programming directly with the sender algorithms presented in this paper. See § 4.20 User-facing sender factories, § 4.21 User-facing sender adaptors, and § 4.22 User-facing sender consumers for short explanations of the algorithms used in these code examples.
1.3.1. Hello world
using namespace std :: execution ; scheduler auto sch = thread_pool . scheduler (); // 1 sender auto begin = schedule ( sch ); // 2 sender auto hi = then ( begin , []{ // 3 std :: cout << "Hello world! Have an int." ; // 3 return 13 ; // 3 }); // 3 sender auto add_42 = then ( hi , []( int arg ) { return arg + 42 ; }); // 4 auto [ i ] = this_thread :: sync_wait ( add_42 ). value (); // 5
This example demonstrates the basics of schedulers, senders, and receivers:
-
First we need to get a scheduler from somewhere, such as a thread pool. A scheduler is a lightweight handle to an execution resource.
-
To start a chain of work on a scheduler, we call § 4.20.1 execution::schedule, which returns a sender that completes on the scheduler. sender describes asynchronous work and sends a signal (value, error, or done) to some recipient(s) when that work completes.
-
We use sender algorithms to produce senders and compose asynchronous work. § 4.21.2 execution::then is a sender adaptor that takes an input sender and a
, and calls thestd :: invocable
on the signal sent by the input sender. The sender returned bystd :: invocable
sends the result of that invocation. In this case, the input sender came fromthen
, so itsschedule
, meaning it won’t send us a value, so ourvoid
takes no parameters. But we return anstd :: invocable
, which will be sent to the next recipient.int -
Now, we add another operation to the chain, again using § 4.21.2 execution::then. This time, we get sent a value - the
from the previous step. We addint
to it, and then return the result.42 -
Finally, we’re ready to submit the entire asynchronous pipeline and wait for its completion. Everything up until this point has been completely asynchronous; the work may not have even started yet. To ensure the work has started and then block pending its completion, we use § 4.22.2 this_thread::sync_wait, which will either return a
with the value sent by the last sender, or an emptystd :: optional < std :: tuple < ... >>
if the last sender sent a done signal, or it throws an exception if the last sender sent an error.std :: optional
1.3.2. Asynchronous inclusive scan
using namespace std :: execution ; sender auto async_inclusive_scan ( scheduler auto sch , // 2 std :: span < const double > input , // 1 std :: span < double > output , // 1 double init , // 1 std :: size_t tile_count ) // 3 { std :: size_t const tile_size = ( input . size () + tile_count - 1 ) / tile_count ; std :: vector < double > partials ( tile_count + 1 ); // 4 partials [ 0 ] = init ; // 4 return transfer_just ( sch , std :: move ( partials )) // 5 | bulk ( tile_count , // 6 [ = ]( std :: size_t i , std :: vector < double >& partials ) { // 7 auto start = i * tile_size ; // 8 auto end = std :: min ( input . size (), ( i + 1 ) * tile_size ); // 8 partials [ i + 1 ] = *-- std :: inclusive_scan ( begin ( input ) + start , // 9 begin ( input ) + end , // 9 begin ( output ) + start ); // 9 }) // 10 | then ( // 11 []( std :: vector < double >& partials ) { std :: inclusive_scan ( begin ( partials ), end ( partials ), // 12 begin ( partials )); // 12 return std :: move ( partials ); // 13 }) | bulk ( tile_count , // 14 [ = ]( std :: size_t i , std :: vector < double >& partials ) { // 14 auto start = i * tile_size ; // 14 auto end = std :: min ( input . size (), ( i + 1 ) * tile_size ); // 14 std :: for_each ( begin ( output ) + start , begin ( output ) + end , // 14 [ & ] ( double & e ) { e = partials [ i ] + e ; } // 14 ); }) | then ( // 15 [ = ]( std :: vector < double >& partials ) { // 15 return output ; // 15 }); // 15 }
This example builds an asynchronous computation of an inclusive scan:
-
It scans a sequence of
s (represented as thedouble std :: span < const double >
) and stores the result in another sequence ofinput
s (represented asdouble std :: span < double >
).output -
It takes a scheduler, which specifies what execution context the scan should be launched on.
-
It also takes a
parameter that controls the number of execution agents that will be spawned.tile_count -
First we need to allocate temporary storage needed for the algorithm, which we’ll do with a
,std :: vector
. We need onepartials
of temporary storage for each execution agent we create.double -
Next we’ll create our initial sender with § 4.20.3 execution::transfer_just. This sender will send the temporary storage, which we’ve moved into the sender. The sender has a completion scheduler of
, which means the next item in the chain will usesch
.sch -
Senders and sender adaptors support composition via
, similar to C++ ranges. We’ll useoperator |
to attach the next piece of work, which will spawnoperator |
execution agents using § 4.21.9 execution::bulk (see § 4.13 Most sender adaptors are pipeable for details).tile_count -
Each agent will call a
, passing it two arguments. The first is the agent’s index (std :: invocable
) in the § 4.21.9 execution::bulk operation, in this case a unique integer ini
. The second argument is what the input sender sent - the temporary storage.[ 0 , tile_count ) -
We start by computing the start and end of the range of input and output elements that this agent is responsible for, based on our agent index.
-
Then we do a sequential
over our elements. We store the scan result for our last element, which is the sum of all of our elements, in our temporary storagestd :: inclusive_scan
.partials -
After all computation in that initial § 4.21.9 execution::bulk pass has completed, every one of the spawned execution agents will have written the sum of its elements into its slot in
.partials -
Now we need to scan all of the values in
. We’ll do that with a single execution agent which will execute after the § 4.21.9 execution::bulk completes. We create that execution agent with § 4.21.2 execution::then.partials -
§ 4.21.2 execution::then takes an input sender and an
and calls thestd :: invocable
with the value sent by the input sender. Inside ourstd :: invocable
, we callstd :: invocable
onstd :: inclusive_scan
, which the input senders will send to us.partials -
Then we return
, which the next phase will need.partials -
Finally we do another § 4.21.9 execution::bulk of the same shape as before. In this § 4.21.9 execution::bulk, we will use the scanned values in
to integrate the sums from other tiles into our elements, completing the inclusive scan.partials -
returns a sender that sends the outputasync_inclusive_scan
. A consumer of the algorithm can chain additional work that uses the scan result. At the point at whichstd :: span < double >
returns, the computation may not have completed. In fact, it may not have even started.async_inclusive_scan
1.3.3. Asynchronous dynamically-sized read
using namespace std :: execution ; sender_of < std :: size_t > auto async_read ( // 1 sender_of < std :: span < std :: byte >> auto buffer , // 1 auto handle ); // 1 struct dynamic_buffer { // 3 std :: unique_ptr < std :: byte [] > data ; // 3 std :: size_t size ; // 3 }; // 3 sender_of < dynamic_buffer > auto async_read_array ( auto handle ) { // 2 return just ( dynamic_buffer {}) // 4 | let_value ([] ( dynamic_buffer & buf ) { // 5 return just ( std :: as_writeable_bytes ( std :: span ( & buf . size , 1 )) // 6 | async_read ( handle ) // 7 | then ( // 8 [ & ] ( std :: size_t bytes_read ) { // 9 assert ( bytes_read == sizeof ( buf . size )); // 10 buf . data = std :: make_unique ( new std :: byte [ buf . size ]); // 11 return std :: span ( buf . data . get (), buf . size ); // 12 } | async_read ( handle ) // 13 | then ( [ & ] ( std :: size_t bytes_read ) { assert ( bytes_read == buf . size ); // 14 return std :: move ( buf ); // 15 }); }); }
This example demonstrates a common asynchronous I/O pattern - reading a payload of a dynamic size by first reading the size, then reading the number of bytes specified by the size:
-
is a pipeable sender adaptor. It’s a customization point object, but this is what it’s call signature looks like. It takes a sender parameter which must send an input buffer in the form of aasync_read
, and a handle to an I/O context. It will asynchronously read into the input buffer, up to the size of thestd :: span < std :: byte >
. It returns a sender which will send the number of bytes read once the read completes.std :: span -
takes an I/O handle and reads a size from it, and then a buffer of that many bytes. It returns a sender that sends aasync_read_array
object that owns the data that was sent.dynamic_buffer -
is an aggregate struct that contains adynamic_buffer
and a size.std :: unique_ptr < std :: byte [] > -
The first thing we do inside of
is create a sender that will send a new, emptyasync_read_array
object using § 4.20.2 execution::just. We can attach more work to the pipeline usingdynamic_array
composition (see § 4.13 Most sender adaptors are pipeable for details).operator | -
We need the lifetime of this
object to last for the entire pipeline. So, we usedynamic_array
, which takes an input sender and alet_value
that must return a sender itself (see § 4.21.4 execution::let_* for details).std :: invocable
sends the value from the input sender to thelet_value
. Critically, the lifetime of the sent object will last until the sender returned by thestd :: invocable
completes.std :: invocable -
Inside of the
let_value
, we have the rest of our logic. First, we want to initiate anstd :: invocable
of the buffer size. To do that, we need to send aasync_read
pointing tostd :: span
. We can do that with § 4.20.2 execution::just.buf . size -
We chain the
onto the § 4.20.2 execution::just sender withasync_read
.operator | -
Next, we pipe a
that will be invoked after thestd :: invocable
completes using § 4.21.2 execution::then.async_read -
That
gets sent the number of bytes read.std :: invocable -
We need to check that the number of bytes read is what we expected.
-
Now that we have read the size of the data, we can allocate storage for it.
-
We return a
to the storage for the data from thestd :: span < std :: byte >
. This will be sent to the next recipient in the pipeline.std :: invocable -
And that recipient will be another
, which will read the data.async_read -
Once the data has been read, in another § 4.21.2 execution::then, we confirm that we read the right number of bytes.
-
Finally, we move out of and return our
object. It will get sent by the sender returned bydynamic_buffer
. We can attach more things to that sender to use the data in the buffer.async_read_array
1.4. Asynchronous Windows socket recv
To get a better feel for how this interface might be used by low-level operations see this example implementation
of a cancellable
operation for a Windows Socket.
struct operation_base : WSAOVERALAPPED { using completion_fn = void ( operation_base * op , DWORD bytesTransferred , int errorCode ) noexcept ; // Assume IOCP event loop will call this when this OVERLAPPED structure is dequeued. completion_fn * completed ; }; template < typename Receiver > struct recv_op : operation_base { recv_op ( SOCKET s , void * data , size_t len , Receiver r ) : receiver ( std :: move ( r )) , sock ( s ) { this -> Internal = 0 ; this -> InternalHigh = 0 ; this -> Offset = 0 ; this -> OffsetHigh = 0 ; this -> hEvent = NULL; this -> completed = & recv_op :: on_complete ; buffer . len = len ; buffer . buf = static_cast < CHAR *> ( data ); } friend void tag_invoke ( std :: tag_t < std :: execution :: start > , recv_op & self ) noexcept { // Avoid even calling WSARecv() if operation already cancelled auto st = std :: execution :: get_stop_token ( self . receiver ); if ( st . stop_requested ()) { std :: execution :: set_done ( std :: move ( self . receiver )); return ; } // Store and cache result here in case it changes during execution const bool stopPossible = st . stop_possible (); if ( ! stopPossible ) { self . ready . store ( true, std :: memory_order_relaxed ); } // Launch the operation DWORD bytesTransferred = 0 ; DWORD flags = 0 ; int result = WSARecv ( self . sock , & self . buffer , 1 , & bytesTransferred , & flags , static_cast < WSAOVERLAPPED *> ( & self ), NULL); if ( result == SOCKET_ERROR ) { int errorCode = WSAGetLastError (); if ( errorCode != WSA_IO_PENDING )) { if ( errorCode == WSA_OPERATION_ABORTED ) { std :: execution :: set_done ( std :: move ( self . receiver )); } else { std :: execution :: set_error ( std :: move ( self . receiver ), std :: error_code ( errorCode , std :: system_category ())); } return ; } } else { // Completed synchronously (assuming FILE_SKIP_COMPLETION_PORT_ON_SUCCESS has been set) execution :: set_value ( std :: move ( self . receiver ), bytesTransferred ); return ; } // If we get here then operation has launched successfully and will complete asynchronously. // May be completing concurrently on another thread already. if ( stopPossible ) { // Register the stop callback self . stopCallback . emplace ( std :: move ( st ), cancel_cb { self }); // Mark as 'completed' if ( self . ready . load ( std :: memory_order_acquire ) || self . ready . exchange ( true, std :: memory_order_acq_rel )) { // Already completed on another thread self . stopCallback . reset (); BOOL ok = WSAGetOverlappedResult ( self . sock , ( WSAOVERLAPPED * ) & self , & bytesTransferred , FALSE , & flags ); if ( ok ) { std :: execution :: set_value ( std :: move ( self . receiver ), bytesTransferred ); } else { int errorCode = WSAGetLastError (); std :: execution :: set_error ( std :: move ( self . receiver ), std :: error_code ( errorCode , std :: system_category ())); } } } } struct cancel_cb { recv_op & op ; void operator ()() noexcept { CancelIoEx (( HANDLE ) op . sock , ( OVERLAPPED * )( WSAOVERLAPPED * ) & op ); } }; static void on_complete ( operation_base * op , DWORD bytesTransferred , int errorCode ) noexcept { recv_op & self = * static_cast < recv_op *> ( op ); if ( ready . load ( std :: memory_order_acquire ) || ready . exchange ( true, std :: memory_order_acq_rel )) { // Unsubscribe any stop-callback so we know that CancelIoEx() is not accessing 'op' any more stopCallback . reset (); if ( errorCode == 0 ) { std :: execution :: set_value ( std :: move ( receiver ), bytesTransferred ); } else { std :: execution :: set_error ( std :: move ( receiver ), std :: error_code ( errorCode , std :: system_category ())); } } } Receiver receiver ; SOCKET sock ; WSABUF buffer ; std :: optional < typename stop_callback_type_t < Receiver >:: template callback_type < cancel_cb >> stopCallback ; std :: atomic < bool > ready { false}; }; struct recv_sender { SOCKET sock ; void * data ; size_t len ; template < typename Receiver > friend recv_op < Receiver > tag_invoke ( std :: tag_t < std :: execution :: connect > const recv_sender & s , Receiver r ) { return recv_op < Receiver > { s . sock , s . data , s . len , std :: move ( r )}; } }; recv_sender async_recv ( SOCKET s , void * data , size_t len ) { return recv_sender { s , data , len }; }
1.4.1. More end-user examples
1.4.1.1. Sudoku solver
This example comes from Kirk Shoop, who ported an example from TBB’s documentation to sender/receiver in his fork of the libunifex repo. It is a Sudoku solver that uses a configurable number of threads to explore the search space for solutions.
The sender/receiver-based Sudoku solver can be found here. Some things that are worth noting about Kirk’s solution:
-
Although it schedules asychronous work onto a thread pool, and each unit of work will schedule more work, its use of structured concurrency patterns make reference counting unnecessary. The solution does not make use of
.shared_ptr -
In addition to eliminating the need for reference counting, the use of structured concurrency makes it easy to ensure that resources are cleaned up on all code paths. In contrast, the TBB example that inspired this one leaks memory.
For comparison, the TBB-based Sudoku solver can be found here.
1.4.1.2. File copy
This example also comes from Kirk Shoop which uses sender/receiver to recursively copy the files a directory tree. It demonstrates how sender/receiver can be used to do IO, using a scheduler that schedules work on Linux’s io_uring.
As with the Sudoku example, this example obviates the need for reference counting by employing structured concurrency. It uses iteration with an upper limit to avoid having too many open file handles.
You can find the example here.
1.4.1.3. Echo server
Dietmar Kuehl has a hobby project that implements networking APIs on top of sender/receiver. He recently implemented an echo server as a demo. His echo server code can be found here.
Below, I show the part of the echo server code. This code is executed for each client that connects to the echo server. In a loop, it reads input from a socket and echos the input back to the same socket. All of this, including the loop, is implemented with generic async algorithms.
outstanding . start ( EX :: repeat_effect_until ( EX :: let_value ( NN :: async_read_some ( ptr -> d_socket , context . scheduler (), NN :: buffer ( ptr -> d_buffer )) | EX :: then ([ ptr ]( :: std :: size_t n ){ :: std :: cout << "read='" << :: std :: string_view ( ptr -> d_buffer , n ) << "' \n " ; ptr -> d_done = n == 0 ; return n ; }), [ & context , ptr ]( :: std :: size_t n ){ return NN :: async_write_some ( ptr -> d_socket , context . scheduler (), NN :: buffer ( ptr -> d_buffer , n )); }) | EX :: then ([]( auto && ...){}) , [ owner = :: std :: move ( owner )]{ return owner -> d_done ; } ) );
In this code,
and
are asynchronous socket-based networking APIs that return senders.
,
, and
are fully generic sender adaptor algorithms that accept and return senders.
This is a good example of seamless composition of async IO functions with non-IO operations. And by composing the senders in this structured way, all the state for the composite operation -- the
expression and all its child operations -- is stored altogether in a single object.
1.5. Examples: Algorithms
In this section we show a few simple sender/receiver-based algorithm implementations.
1.5.1. then
template < receiver R , class F > class _then_receiver : std :: execution :: receiver_adaptor < _then_receiver < R , F > , R > { friend std :: execution :: receiver_adaptor < _then_receiver , R > ; F f_ ; // Customize set_value by invoking the callable and passing the result to the inner receiver template < class ... As > requires receiver_of < R , invoke_result_t < F , As ... >> void set_value ( As && ... as ) && { std :: execution :: set_value ( std :: move ( * this ). base (), invoke (( F && ) f_ , ( As && ) as ...)); } public : _then_receiver ( R r , F f ) : std :: execution :: receiver_adaptor < _then_receiver , R > { std :: move ( r )} , f_ ( std :: move ( f )) {} }; template < sender S , class F > struct _then_sender : std :: execution :: sender_base { S s_ ; F f_ ; template < receiver R > requires sender_to < S , _then_receiver < R , F >> friend auto tag_invoke ( std :: experimental :: connect_t , _then_sender && self , R r ) -> std :: execution :: connect_result_t < S , _then_receiver < R , F >> { return std :: execution :: connect (( S && ) s_ , _then_receiver < R , F > {( R && ) r , ( F && ) f_ }); } }; template < sender S , class F > sender auto then ( S s , F f ) { return _then_sender {{}, ( S && ) s , ( F && ) f }; }
This code builds a
algorithm that transforms the value(s) from the input sender
with a transformation function. The result of the transformation becomes the new value.
The other receiver functions (
and
), as well as all receiver queries,
are passed through unchanged.
In detail, it does the following:
-
Defines a receiver in terms of
that aggregates another receiver and an invocable that:execution :: receiver_adaptor -
Defines a constrained
overload for transforming the value channel.tag_invoke -
Defines another constrained overload of
that passes all other customizations through unchanged.tag_invoke
The
overloads are actually implemented bytag_invoke
; they dispatch either to named members, as shown above withexecution :: receiver_adaptor
, or to the adapted receiver._then_receiver :: set_value -
-
Defines a sender that aggregates another sender and the invocable, which defines a
customization fortag_invoke
that wraps the incoming receiver in the receiver from (1) and passes it and the incoming sender tostd :: execution :: connect
, returning the result.std :: execution :: connect
1.5.2. retry
template < class From , class To > using _decays_to = same_as < decay_t < From > , To > ; // _conv needed so we can emplace construct non-movable types into // a std::optional. template < invocable F > requires is_nothrow_move_constructible_v < F > struct _conv { F f_ ; explicit _conv ( F f ) noexcept : f_ (( F && ) f ) {} operator invoke_result_t < F > () && { return (( F && ) f_ )(); } }; // pass through all customizations except set_error, which retries the operation. template < class O , class R > struct _retry_receiver : std :: execution :: receiver_adaptor < _retry_receiver < O , R >> { O * o_ ; R && base () && noexcept { return ( R && ) o_ -> r_ ; } const R & base () const & noexcept { return o_ -> r_ ; } explicit _retry_receiver ( O * o ) : o_ ( o ) {} void set_error ( auto && ) && noexcept { o_ -> _retry (); // This causes the op to be retried } }; template < sender S > struct _retry_sender : std :: execution :: sender_base { S s_ ; explicit _retry_sender ( S s ) : s_ (( S && ) s ) {} // Hold the nested operation state in an optional so we can // re-construct and re-start it if the operation fails. template < receiver R > struct _op { S s_ ; R r_ ; std :: optional < std :: execution :: connect_result_t < S & , _retry_receiver < _op , R >>> o_ ; _op ( S s , R r ) : s_ (( S && ) s ), r_ (( R && ) r ), o_ { _connect ()} {} _op ( _op && ) = delete ; auto _connect () noexcept { return _conv {[ this ] { return std :: execution :: connect ( s_ , _retry_receiver < _op , R > { this }); }}; } void _retry () noexcept try { o_ . emplace ( _connect ()); // potentially throwing std :: execution :: start ( * o_ ); } catch (...) { std :: execution :: set_error (( R && ) r_ , std :: current_exception ()); } friend void tag_invoke ( std :: execution :: start_t , _op & o ) noexcept { std :: execution :: start ( * o . o_ ); } }; template < receiver R > requires sender_to < S & , R > friend _op < R > tag_invoke ( std :: execution :: connect_t , _retry_sender && self , R r ) { return {( S && ) self . s_ , ( R && ) r }; } }; namespace std :: execution { template < typed_sender S > struct sender_traits < _retry_sender < S >> : sender_traits < S > { }; } std :: execution :: sender auto retry ( std :: execution :: sender auto s ) { return _retry_sender { std :: move ( s )}; }
The
algorithm takes a multi-shot sender and causes it to repeat on error, passing
through values and done signals. Each time the input sender is restarted, a new receiver
is connected and the resulting operation state is stored in an
, which allows us
to reinitialize it multiple times.
This example does the following:
-
Defines a
utility that takes advantage of C++17’s guaranteed copy elision to emplace a non-movable type in a_conv
.std :: optional -
Defines a
that holds a pointer back to the operation state. It passes all customizations through unmodified to the inner receiver owned by the operation state except for_retry_receiver
, which causes aset_error
function to be called instead._retry () -
Defines an operation state that aggregates the input sender and receiver, and declares storage for the nested operation state in a
. Constructing the operation state constructs astd :: optional
with a pointer to the (under construction) operation state and uses it to connect to the aggregated sender._retry_receiver -
Starting the operation state dispatches to
on the inner operation state.start -
The
function reinitializes the inner operation state by connecting the sender to a new receiver, holding a pointer back to the outer operation state as before._retry () -
After reinitializing the inner operation state,
calls_retry ()
on it, causing the failed operation to be rescheduled.start -
Defines a
that implements the_retry_sender
customization point to return an operation state constructed from the passed-in sender and receiver.connect
1.6. Examples: Schedulers
In this section we look at some schedulers of varying complexity.
1.6.1. Inline scheduler
struct inline_scheduler { template < class R > struct _op { [[ no_unique_address ]] R rec_ ; friend void tag_invoke ( std :: execution :: start_t , _op & op ) noexcept try { std :: execution :: set_value (( R && ) op . rec_ ); } catch (...) { std :: execution :: set_error (( R && ) op . rec_ , std :: current_exception ()); } }; struct _sender { template < template < class ... > class Tuple , template < class ... > class Variant > using value_types = Variant < Tuple <>> ; template < template < class ... > class Variant > using error_types = Variant < std :: exception_ptr > ; static constexpr bool sends_done = false; template < std :: execution :: receiver_of R > friend auto tag_invoke ( std :: execution :: connect_t , _sender , R && rec ) noexcept ( std :: is_nothrow_constructible_v < std :: remove_cvref_t < R > , R > ) -> _op < std :: remove_cvref_t < R >> { return {( R && ) rec }; } }; friend _sender tag_invoke ( std :: execution :: schedule_t , const inline_scheduler & ) noexcept { return {}; } bool operator == ( const inline_scheduler & ) const noexcept = default ; };
The inline scheduler is a trivial scheduler that completes immediately and synchronously on
the thread that calls
on the operation state produced by its sender.
In other words,
is
just a fancy way of saying
, with the exception of the fact that
wants to be passed an lvalue.
Although not a particularly useful scheduler, it serves to illustrate the basics of
implementing one. The
:
-
Customizes
to return an instance of the sender typeexecution :: schedule
._sender -
The
type models the_sender
concept and provides the metadata needed to describe it as a sender of no values (seetyped_sender
) that can send anvalue_types
as an error (seeexception_ptr
), and that never callserror_types
(seeset_done
).sends_done -
The
type customizes_sender
to accept a receiver of no values. It returns an instance of typeexecution :: connect
that holds the receiver by value._op -
The operation state customizes
to callstd :: execution :: start
on the receiver, passing any exceptions tostd :: execution :: set_value
as anstd :: execution :: set_error
.exception_ptr
1.6.2. Single thread scheduler
This example shows how to create a scheduler for an execution context that consists of a single
thread. It is implemented in terms of a lower-level execution context called
.
class single_thread_context { std :: execution :: run_loop loop_ ; std :: thread thread_ ; public : single_thread_context () : loop_ () , thread_ ([ this ] { loop_ . run (); }) {} ~ single_thread_context () { loop_ . finish (); thread_ . join (); } auto get_scheduler () noexcept { return loop_ . get_scheduler (); } std :: thread :: id get_thread_id () const noexcept { return thread_ . get_id (); } };
The
owns an event loop and a thread to drive it. In the destructor, it tells the event
loop to finish up what it’s doing and then joins the thread, blocking for the event loop to drain.
The interesting bits are in the
context implementation. It
is slightly too long to include here, so we only provide a reference to
it,
but there is one noteworthy detail about its implementation. It uses space in
its operation states to build an intrusive linked list of work items. In
structured concurrency patterns, the operation states of nested operations
compose statically, and in an algorithm like
, the
composite operation state lives on the stack for the duration of the operation.
The end result is that work can be scheduled onto this thread with zero
allocations.
1.7. What this proposal is not
This paper is not a patch on top of [P0443R14]; we are not asking to update the existing paper, we are asking to retire it in favor of this paper, which is already self-contained; any example code within this paper can be written in Standard C++, without the need to standardize any further facilities.
This paper is not an alternative design to [P0443R14]; rather, we have taken the design in the current executors paper, and applied targeted fixes to allow it to fulfill the promises of the sender/receiver model, as well as provide all the facilities we consider essential when writing user code using standard execution concepts; we have also applied the guidance of removing one-way executors from the paper entirely, and instead provided an algorithm based around senders that serves the same purpose.
1.8. Design changes from P0443
-
The
concept has been removed and all of its proposed functionality is now based on schedulers and senders, as per SG1 direction.executor -
Properties are not included in this paper. We see them as a possible future extension, if the committee gets more comfortable with them.
-
Senders now advertise what scheduler, if any, their evaluation will complete on.
-
The places of execution of user code in P0443 weren’t precisely defined, whereas they are in this paper. See § 4.5 Senders can propagate completion schedulers.
-
P0443 did not propose a suite of sender algorithms necessary for writing sender code; this paper does. See § 4.20 User-facing sender factories, § 4.21 User-facing sender adaptors, and § 4.22 User-facing sender consumers.
-
P0443 did not specify the semantics of variously qualified
overloads; this paper does. See § 4.7 Senders can be either multi-shot or single-shot.connect -
Specific type erasure facilities are omitted, as per LEWG direction. Type erasure facilities can be built on top of this proposal, as discussed in § 5.9 Ranges-style CPOs vs tag_invoke.
-
A specific thread pool implementation is omitted, as per LEWG direction.
1.9. Prior art
This proposal builds upon and learns from years of prior art with asynchronous and parallel programming frameworks in C++. In this section, we discuss async abstractions that have previously been suggested as a possible basis for asynchronous algorithms and why they fall short.
1.9.1. Futures
A future is a handle to work that has already been scheduled for execution. It is one end of a communication channel; the other end is a promise, used to receive the result from the concurrent operation and to communicate it to the future.
Futures, as traditionally realized, require the dynamic allocation and management of a shared state, synchronization, and typically type-erasure of work and continuation. Many of these costs are inherent in the nature of "future" as a handle to work that is already scheduled for execution. These expenses rule out the future abstraction for many uses and makes it a poor choice for a basis of a generic mechanism.
1.9.2. Coroutines
C++20 coroutines are frequently suggested as a basis for asynchronous algorithms. It’s fair to ask why, if we added coroutines to C++, are we suggesting the addition of a library-based abstraction for asynchrony. Certainly, coroutines come with huge syntactic and semantic advantages over the alternatives.
Although coroutines are lighter weight than futures, coroutines suffer many of the same problems. Since they typically start suspended, they can avoid synchronizing the chaining of dependent work. However in many cases, coroutine frames require an unavoidable dynamic allocation and indirect function calls. This is done to hide the layout of the coroutine frame from the C++ type system, which in turn makes possible the separate compilation of coroutines and certain compiler optimizations, such as optimization of the coroutine frame size.
Those advantages come at a cost, though. Because of the dynamic allocation of coroutine frames, coroutines in embedded or heterogeneous environments, which often lack support for dynamic allocation, require great attention to detail. And the allocations and indirections tend to complicate the job of the inliner, often resulting in sub-optimal codegen.
The coroutine language feature mitigates these shortcomings somewhat with the HALO optimization [P0981R0], which leverages existing compiler optimizations such as allocation elision and devirtualization to inline the coroutine, completely eliminating the runtime overhead. However, HALO requires a sophisiticated compiler, and a fair number of stars need to align for the optimization to kick in. In our experience, more often than not in real-world code today’s compilers are not able to inline the coroutine, resulting in allocations and indirections in the generated code.
In a suite of generic async algorithms that are expected to be callable from hot code paths, the extra allocations and indirections are a deal-breaker. It is for these reasons that we consider coroutines a poor choise for a basis of all standard async.
1.9.3. Callbacks
Callbacks are the oldest, simplest, most powerful, and most efficient mechanism for creating chains of work, but suffer problems of their own. Callbacks must propagate either errors or values. This simple requirement yields many different interface possibilities. The lack of a standard callback shape obstructs generic design.
Additionally, few of these possibilities accommodate cancellation signals when the user requests upstream work to stop and clean up.
1.10. Field experience
1.10.1. libunifex
This proposal draws heavily from our field experience with libunifex. Libunifex implements all of the concepts and customization points defined in this paper, many of this paper’s algorithms (some under different names), and much more besides.
Libunifex has several concrete schedulers in addition to the
suggested here (where it is called
). It has schedulers that dispatch efficiently to epoll and io_uring on Linux and the Windows Thread Pool on Windows.
In addition to the proposed interfaces and the additional schedulers, it has several important extensions to the facilities described in this paper, which demonstrate directions in which these abstractions may be evolved over time, including:
-
Timed schedulers, which permit scheduling work on an execution context at a particular time or after a particular duration has elapsed. In addition, it provides time-based algorithms.
-
File I/O schedulers, which permit filesystem I/O to be scheduled.
-
Two complementary abstractions for streams (asynchronous ranges), and a set of stream-based algorithms.
Libunifex has seen heavy production use at Facebook. As of October 2021, it is currently used in production within the following applications and platforms:
-
Facebook Messenger on iOS, Android, Windows, and macOS
-
Instagram on iOS and Android
-
Facebook on iOS and Android
-
Portal
-
An internal Facebook product that runs on Linux
All of these applications are making direct use of the sender/receiver abstraction as presented in this paper. One product (Instagram on iOS) is making use of the sender/coroutine integration as presented. The monthly active users of these products number in the billions.
1.10.2. Other implementations
The authors are aware of a number of other implementations of sender/receiver from this paper. These are presented here in perceived order of maturity and field experience.
-
HPX is a general purpose C++ runtime system for parallel and distributed applications that has been under active development since 2007. HPX exposes a uniform, standards-oriented API, and keeps abreast of the latest standards and proposals. It is used in a wide variety of high-performance applications.
The sender/receiver implementation in HPX has been under active development since May 2020. It is used to erase the overhead of futures and to make it possible to write efficient generic asynchronous algorithms that are agnostic to their execution context. In HPX, algorithms can migrate execution between execution contexts, even to GPUs and back, using a uniform standard interface with sender/receiver.
Far and away, the HPX team has the greatest usage experience outside Facebook. Mikael Simburg summarizes the experience as follows:
Summarizing, for us the major benefits of sender/receiver compared to the old model are:
-
Proper hooks for transitioning between execution contexts.
-
The adaptors. Things like
are really nice additions.let_value -
Separation of the error channel from the value channel (also cancellation, but we don’t have much use for it at the moment). Even from a teaching perspective having to explain that the future
in the continuation will always be ready heref2
is enough of a reason to separate the channels. All the other obvious reasons apply as well of course.f1 . then ([]( future < T > f2 ) {...}) -
For futures we have a thing called
which is an optimized version ofhpx :: dataflow
which avoids intermediate allocations. With the sender/receiverwhen_all (...). then (...)
we get that "for free".when_all (...) | then (...)
-
-
kuhllib by Dietmar Kuehl
This is a prototype Standard Template Library with an implementation of sender/receiver that has been under development since May, 2021. It is significant mostly for its support for sender/receiver-based networking interfaces.
Here, Dietmar Kuehl speaks about the perceived complexity of sender/receiver:
... and, also similar to STL: as I had tried to do things in that space before I recognize sender/receivers as being maybe complicated in one way but a huge simplification in another one: like with STL I think those who use it will benefit - if not from the algorithm from the clarity of abstraction: the separation of concerns of STL (the algorithm being detached from the details of the sequence representation) is a major leap. Here it is rather similar: the separation of the asynchronous algorithm from the details of execution. Sure, there is some glue to tie things back together but each of them is simpler than the combined result.
Elsewhere, he said:
... to me it feels like sender/receivers are like iterators when STL emerged: they are different from what everybody did in that space. However, everything people are already doing in that space isn’t right.
Kuehl also has experience teaching sender/receiver at Bloomberg. About that experience he says:
When I asked [my students] specifically about how complex they consider the sender/receiver stuff the feedback was quite unanimous that the sender/receiver parts aren’t trivial but not what contributes to the complexity.
-
This is a partial implementation written from the specification in this paper. Its primary purpose is to help find specification bugs and to harden the wording of the proposal. When finished, it will be a minimal and complete implementation of this proposal, fit for broad use and for contribution to libc++. It will be finished before this proposal is approved.
It currently lacks some of the proposed sender adaptors and
, but otherwise implements the concepts, customization points, traits, queries, coroutine integration, sender factories, pipe support,execution :: start_detached
, andexecution :: receiver_adaptor
.execution :: run_loop -
Reference implementation for the Microsoft STL by Michael Schellenberger Costa
This is another reference implementation of this proposal, this time in a fork of the Mircosoft STL implementation. Michael Schellenberger Costa is not affiliated with Microsoft. He intends to contribute this implementation upstream when it is complete.
1.10.3. Inspirations
This proposal also draws heavily from our experience with Thrust and Agency. It is also inspired by the needs of countless other C++ frameworks for asynchrony, parallelism, and concurrency, including:
2. Revision history
2.1. R3
The changes since R2 are as follows:
Fixes:
-
Fix specification of the
algorithm to clarify lifetimes of intermediate operation states and properly scope theon
query.get_scheduler -
Fix a memory safety bug in the implementation of
.connect - awaitable -
Fix recursive definition of the
concept.scheduler
Enhancements:
-
Add
execution context.run_loop -
Add
utility to simplify writing receivers.receiver_adaptor -
Require a scheduler’s sender to model
and provide a completion scheduler.sender_of -
Specify the cancellation scope of the
algorithm.when_all -
Make
a customization point.as_awaitable -
Change
's handling of awaitables to consider those types that are awaitable owing to customization ofconnect
.as_awaitable -
Add
andvalue_types_of_t
alias templates; renameerror_types_of_t
tostop_token_type_t
.stop_token_of_t -
Add a design rationale for the removal of the possibly eager algorithms.
-
Expand the section on field experience.
2.2. R2
The changes since R1 are as follows:
-
Remove the eagerly executing sender algorithms.
-
Extend the
customization point and theexecution :: connect
template to recognize awaitables assender_traits <>
s.typed_sender -
Add utilities
andas_awaitable ()
so a coroutine type can trivially make senders awaitable with a coroutine.with_awaitable_senders <> -
Add a section describing the design of the sender/awaitable interactions.
-
Add a section describing the design of the cancellation support in sender/receiver.
-
Add a section showing examples of simple sender adaptor algorithms.
-
Add a section showing examples of simple schedulers.
-
Add a few more examples: a sudoku solver, a parallel recursive file copy, and an echo server.
-
Refined the forward progress guarantees on the
algorithm.bulk -
Add a section describing how to use a range of senders to represent async sequences.
-
Add a section showing how to use senders to represent partial success.
-
Add sender factories
andexecution :: just_error
.execution :: just_done -
Add sender adaptors
andexecution :: done_as_optional
.execution :: done_as_error -
Document more production uses of sender/receiver at scale.
-
Various fixes of typos and bugs.
2.3. R1
The changes since R0 are as follows:
-
Added a new concept,
.sender_of -
Added a new scheduler query,
.this_thread :: execute_may_block_caller -
Added a new scheduler query,
.get_forward_progress_guarantee -
Removed the
adaptor.unschedule -
Various fixes of typos and bugs.
2.4. R0
Initial revision.
3. Design - introduction
The following four sections describe the entirety of the proposed design.
-
§ 3 Design - introduction describes the conventions used through the rest of the design sections, as well as an example illustrating how we envision code will be written using this proposal.
-
§ 4 Design - user side describes all the functionality from the perspective we intend for users: it describes the various concepts they will interact with, and what their programming model is.
-
§ 5 Design - implementer side describes the machinery that allows for that programming model to function, and the information contained there is necessary for people implementing senders and sender algorithms (including the standard library ones) - but is not necessary to use senders productively.
3.1. Conventions
The following conventions are used throughout the design section:
-
The namespace proposed in this paper is the same as in [P0443R14]:
; however, for brevity, thestd :: execution
part of this name is omitted. When you seestd ::
, treat that asexecution :: foo
.std :: execution :: foo -
Universal references and explicit calls to
/std :: move
are omitted in code samples and signatures for simplicity; assume universal references and perfect forwarding unless stated otherwise.std :: forward -
None of the names proposed here are names that we are particularly attached to; consider the names to be reasonable placeholders that can freely be changed, should the committee want to do so.
3.2. Queries and algorithms
A query is a
that takes some set of objects (usually one) as parameters and returns facts about those objects without modifying them. Queries are usually customization point objects, but in some cases may be functions.
An algorithm is a
that takes some set of objects as parameters and causes those objects to do something. Algorithms are usually customization point objects, but in some cases may be functions.
4. Design - user side
4.1. Execution contexts describe the place of execution
An execution context is a resource that represents the place where execution will happen. This could be a concrete resource - like a specific thread pool object, or a GPU - or a more abstract one, like the current thread of execution. Execution contexts don’t need to have a representation in code; they are simply a term describing certain properties of execution of a function.
4.2. Schedulers represent execution contexts
A scheduler is a lightweight handle that represents a strategy for scheduling work onto an execution context. Since execution contexts don’t necessarily manifest in C++ code, it’s not possible to program
directly against their API. A scheduler is a solution to that problem: the scheduler concept is defined by a single sender algorithm,
, which returns a sender that will complete on an execution context determined
by the scheduler. Logic that you want to run on that context can be placed in the receiver’s completion-signalling method.
execution :: scheduler auto sch = thread_pool . scheduler (); execution :: sender auto snd = execution :: schedule ( sch ); // snd is a sender (see below) describing the creation of a new execution resource // on the execution context associated with sch
Note that a particular scheduler type may provide other kinds of scheduling operations
which are supported by its associated execution context. It is not limited to scheduling
purely using the
API.
Future papers will propose additional scheduler concepts that extend
to add other capabilities. For example:
-
A
concept that extendstime_scheduler
to support time-based scheduling. Such a concept might provide access toscheduler
,schedule_after ( sched , duration )
andschedule_at ( sched , time_point )
APIs.now ( sched ) -
Concepts that extend
to support opening, reading and writing files asynchronously.scheduler -
Concepts that extend
to support connecting, sending data and receiving data over the network asynchronously.scheduler
4.3. Senders describe work
A sender is an object that describes work. Senders are similar to futures in existing asynchrony designs, but unlike futures, the work that is being done to arrive at the values they will send is also directly described by the sender object itself. A sender is said to send some values if a receiver connected (see § 5.3 execution::connect) to that sender will eventually receive said values.
The primary defining sender algorithm is § 5.3 execution::connect; this function, however, is not a user-facing API; it is used to facilitate communication between senders and various sender algorithms, but end user code is not expected to invoke it directly.
The way user code is expected to interact with senders is by using sender algorithms. This paper proposes an initial set of such sender algorithms, which are described in § 4.4 Senders are composable through sender algorithms, § 4.20 User-facing sender factories, § 4.21 User-facing sender adaptors, and § 4.22 User-facing sender consumers. For example, here is how a user can create a new sender on a scheduler, attach a continuation to it, and then wait for execution of the continuation to complete:
execution :: scheduler auto sch = thread_pool . scheduler (); execution :: sender auto snd = execution :: schedule ( sch ); execution :: sender auto cont = execution :: then ( snd , []{ std :: fstream file { "result.txt" }; file << compute_result ; }); this_thread :: sync_wait ( cont ); // at this point, cont has completed execution
4.4. Senders are composable through sender algorithms
Asynchronous programming often departs from traditional code structure and control flow that we are familiar with. A successful asynchronous framework must provide an intuitive story for composition of asynchronous work: expressing dependencies, passing objects, managing object lifetimes, etc.
The true power and utility of senders is in their composability. With senders, users can describe generic execution pipelines and graphs, and then run them on and across a variety of different schedulers. Senders are composed using sender algorithms:
-
sender factories, algorithms that take no senders and return a sender.
-
sender adaptors, algorithms that take (and potentially
) senders and return a sender.execution :: connect -
sender consumers, algorithms that take (and potentially
) senders and do not return a sender.execution :: connect
4.5. Senders can propagate completion schedulers
One of the goals of executors is to support a diverse set of execution contexts, including traditional thread pools, task and fiber frameworks (like HPX and Legion), and GPUs and other accelerators (managed by runtimes such as CUDA or SYCL). On many of these systems, not all execution agents are created equal and not all functions can be run on all execution agents. Having precise control over the execution context used for any given function call being submitted is important on such systems, and the users of standard execution facilities will expect to be able to express such requirements.
[P0443R14] was not always clear about the place of execution of any given piece of code. Precise control was present in the two-way execution API present in earlier executor designs, but it has so far been missing from the senders design. There has been a proposal ([P1897R3]) to provide a number of sender algorithms that would enforce certain rules on the places of execution of the work described by a sender, but we have found those sender algorithms to be insufficient for achieving the best performance on all platforms that are of interest to us. The implementation strategies that we are aware of result in one of the following situations:
-
trying to submit work to one execution context (such as a CPU thread pool) from another execution context (such as a GPU or a task framework), which assumes that all execution agents are as capable as a
(which they aren’t).std :: thread -
forcibly interleaving two adjacent execution graph nodes that are both executing on one execution context (such as a GPU) with glue code that runs on another execution context (such as a CPU), which is prohibitively expensive for some execution contexts (such as CUDA or SYCL).
-
having to customise most or all sender algorithms to support an execution context, so that you can avoid problems described in 1. and 2, which we believe is impractical and brittle based on months of field experience attempting this in Agency.
None of these implementation strategies are acceptable for many classes of parallel runtimes, such as task frameworks (like HPX) or accelerator runtimes (like CUDA or SYCL).
Therefore, in addition to the
sender algorithm from [P1897R3], we are proposing a way for senders to advertise what scheduler (and by extension what execution context) they will complete on.
Any given sender may have completion schedulers for some or all of the signals (value, error, or done) it completes with (for more detail on the completion signals, see § 5.1 Receivers serve as glue between senders).
When further work is attached to that sender by invoking sender algorithms, that work will also complete on an appropriate completion scheduler.
4.5.1. execution :: get_completion_scheduler
is a query that retrieves the completion scheduler for a specific completion signal from a sender.
Calling
on a sender that does not have a completion scheduler for a given signal is ill-formed.
If a sender advertises a completion scheduler for a signal in this way, that sender must ensure that it sends that signal on an execution agent belonging to an execution context represented by a scheduler returned from this function.
See § 4.5 Senders can propagate completion schedulers for more details.
execution :: scheduler auto cpu_sched = new_thread_scheduler {}; execution :: scheduler auto gpu_sched = cuda :: scheduler (); execution :: sender auto snd0 = execution :: schedule ( cpu_sched ); execution :: scheduler auto completion_sch0 = execution :: get_completion_scheduler < execution :: set_value_t > ( snd0 ); // completion_sch0 is equivalent to cpu_sched execution :: sender auto snd1 = execution :: then ( snd0 , []{ std :: cout << "I am running on cpu_sched! \n " ; }); execution :: scheduler auto completion_sch1 = execution :: get_completion_scheduler < execution :: set_value_t > ( snd1 ); // completion_sch1 is equivalent to cpu_sched execution :: sender auto snd2 = execution :: transfer ( snd1 , gpu_sched ); execution :: sender auto snd3 = execution :: then ( snd2 , []{ std :: cout << "I am running on gpu_sched! \n " ; }); execution :: scheduler auto completion_sch3 = execution :: get_completion_scheduler < execution :: set_value_t > ( snd3 ); // completion_sch3 is equivalent to gpu_sched
4.6. Execution context transitions are explicit
[P0443R14] does not contain any mechanisms for performing an execution context transition. The only sender algorithm that can create a sender that will move execution to a specific execution context is
, which does not take an input sender.
That means that there’s no way to construct sender chains that traverse different execution contexts. This is necessary to fulfill the promise of senders being able to replace two-way executors, which had this capability.
We propose that, for senders advertising their completion scheduler, all execution context transitions must be explicit; running user code anywhere but where they defined it to run must be considered a bug.
The
sender adaptor performs a transition from one execution context to another:
execution :: scheduler auto sch1 = ...; execution :: scheduler auto sch2 = ...; execution :: sender auto snd1 = execution :: schedule ( sch1 ); execution :: sender auto then1 = execution :: then ( snd1 , []{ std :: cout << "I am running on sch1! \n " ; }); execution :: sender auto snd2 = execution :: transfer ( then1 , sch2 ); execution :: sender auto then2 = execution :: then ( snd2 , []{ std :: cout << "I am running on sch2! \n " ; }); this_thread :: sync_wait ( then2 );
4.7. Senders can be either multi-shot or single-shot
Some senders may only support launching their operation a single time, while others may be repeatable and support being launched multiple times. Executing the operation may consume resources owned by the sender.
For example, a sender may contain a
that it will be transferring ownership of to the
operation-state returned by a call to
so that the operation has access to
this resource. In such a sender, calling
consumes the sender such that after
the call the input sender is no longer valid. Such a sender will also typically be move-only so that
it can maintain unique ownership of that resource.
A single-shot sender can only be connected to a receiver at most once. Its implementation of
only has overloads for an rvalue-qualified sender. Callers must pass the sender
as an rvalue to the call to
, indicating that the call consumes the sender.
A multi-shot sender can be connected to multiple receivers and can be launched multiple
times. Multi-shot senders customise
to accept an lvalue reference to the
sender. Callers can indicate that they want the sender to remain valid after the call to
by passing an lvalue reference to the sender to call these overloads. Multi-shot senders should also define
overloads of
that accept rvalue-qualified senders to allow the sender to be also used in places
where only a single-shot sender is required.
If the user of a sender does not require the sender to remain valid after connecting it to a
receiver then it can pass an rvalue-reference to the sender to the call to
.
Such usages should be able to accept either single-shot or multi-shot senders.
If the caller does wish for the sender to remain valid after the call then it can pass an lvalue-qualified sender
to the call to
. Such usages will only accept multi-shot senders.
Algorithms that accept senders will typically either decay-copy an input sender and store it somewhere
for later usage (for example as a data-member of the returned sender) or will immediately call
on the input sender, such as in
or
.
Some multi-use sender algorithms may require that an input sender be copy-constructible but will only call
on an rvalue of each copy, which still results in effectively executing the operation multiple times.
Other multi-use sender algorithms may require that the sender is move-constructible but will invoke
on an lvalue reference to the sender.
For a sender to be usable in both multi-use scenarios, it will generally be required to be both copy-constructible and lvalue-connectable.
4.8. Senders are forkable
Any non-trivial program will eventually want to fork a chain of senders into independent streams of work, regardless of whether they are single-shot or multi-shot. For instance, an incoming event to a middleware system may be required to trigger events on more than one downstream system. This requires that we provide well defined mechanisms for making sure that connecting a sender multiple times is possible and correct.
The
sender adaptor facilitates connecting to a sender multiple times, regardless of whether it is single-shot or multi-shot:
auto some_algorithm ( execution :: sender auto && input ) { execution :: sender auto multi_shot = split ( input ); // "multi_shot" is guaranteed to be multi-shot, // regardless of whether "input" was multi-shot or not return when_all ( then ( multi_shot , [] { std :: cout << "First continuation \n " ; }), then ( multi_shot , [] { std :: cout << "Second continuation \n " ; }) ); }
4.9. Senders are joinable
Similarly to how it’s hard to write a complex program that will eventually want to fork sender chains into independent streams, it’s also hard to write a program that does not want to eventually create join nodes, where multiple independent streams of execution are merged into a single one in an asynchronous fashion.
is a sender adaptor that returns a sender that completes when the last of the input senders completes. It sends a pack of values, where the elements of said pack are the values sent by the input senders, in order.
returns a sender that also does not have an associated scheduler.
accepts an additional scheduler argument. It returns a sender whose value completion scheduler is the scheduler provided as an argument, but otherwise behaves the same as
. You can think of it as a composition of
, but one that allows for better efficiency through customization.
4.10. Senders support cancellation
Senders are often used in scenarios where the application may be concurrently executing multiple strategies for achieving some program goal. When one of these strategies succeeds (or fails) it may not make sense to continue pursuing the other strategies as their results are no longer useful.
For example, we may want to try to simultaneously connect to multiple network servers and use whichever server responds first. Once the first server responds we no longer need to continue trying to connect to the other servers.
Ideally, in these scenarios, we would somehow be able to request that those other strategies stop executing promptly so that their resources (e.g. cpu, memory, I/O bandwidth) can be released and used for other work.
While the design of senders has support for cancelling an operation before it starts
by simply destroying the sender or the operation-state returned from
before calling
, there also needs to be a standard, generic mechanism
to ask for an already-started operation to complete early.
The ability to be able to cancel in-flight operations is fundamental to supporting some kinds of generic concurrency algorithms.
For example:
-
a
algorithm should cancel other operations as soon as one operation failswhen_all ( ops ...) -
a
algorithm should cancel the other operations as soon as one operation completes successfulyfirst_successful ( ops ...) -
a generic
algorithm needs to be able to cancel thetimeout ( src , duration )
operation after the timeout duration has elapsed.src -
a
algorithm should cancelstop_when ( src , trigger )
ifsrc
completes first and canceltrigger
iftrigger
completes firstsrc
The mechanism used for communcating cancellation-requests, or stop-requests, needs to have a uniform interface so that generic algorithms that compose sender-based operations, such as the ones listed above, are able to communicate these cancellation requests to senders that they don’t know anything about.
The design is intended to be composable so that cancellation of higher-level operations can propagate those cancellation requests through intermediate layers to lower-level operations that need to actually respond to the cancellation requests.
For example, we can compose the algorithms mentioned above so that child operations are cancelled when any one of the multiple cancellation conditions occurs:
sender auto composed_cancellation_example ( auto query ) { return stop_when ( timeout ( when_all ( first_successful ( query_server_a ( query ), query_server_b ( query )), load_file ( "some_file.jpg" )), 5 s ), cancelButton . on_click ()); }
In this example, if we take the operation returned by
, this operation will
receive a stop-request when any of the following happens:
-
algorithm will send a stop-request iffirst_successful
completes successfullyquery_server_a ( query ) -
algorithm will send a stop-request if thewhen_all
operation completes with an error or done result.load_file ( "some_file.jpg" ) -
algorithm will send a stop-request if the operation does not complete within 5 seconds.timeout -
algorithm will send a stop-request if the user clicks on the "Cancel" button in the user-interface.stop_when -
The parent operation consuming the
sends a stop-requestcomposed_cancellation_example ()
Note that within this code there is no explicit mention of cancellation, stop-tokens, callbacks, etc. yet the example fully supports and responds to the various cancellation sources.
The intent of the design is that the common usage of cancellation in sender/receiver-based code is primarily through use of concurrency algorithms that manage the detailed plumbing of cancellation for you. Much like algorithms that compose senders relieve the user from having to write their own receiver types, algorithms that introduce concurrency and provide higher-level cancellation semantics relieve the user from having to deal with low-level details of cancellation.
4.10.1. Cancellation design summary
The design of cancellation described in this paper is built on top of and extends the
-based
cancellation facilities added in C++20, first proposed in [P2175R0].
At a high-level, the facilities proposed by this paper for supporting cancellation include:
-
Add
andstd :: stoppable_token
concepts that generalise the interface ofstd :: stoppable_token_for
type to allow other types with different implementation strategies.std :: stop_token -
Add
concept for detecting whether astd :: unstoppable_token
can never receive a stop-request.stoppable_token -
Add
,std :: in_place_stop_token
andstd :: in_place_stop_source
types that provide a more efficient implementation of a stop-token for use in structured concurrency situations.std :: in_place_stop_callback < CB > -
Add
for use in places where you never want to issue a stop-requeststd :: never_stop_token -
Add
CPO for querying the stop-token to use for an operation from its receiver.std :: execution :: get_stop_token () -
Add
for querying the type of a stop-token returned fromstd :: execution :: stop_token_of_t < T > get_stop_token ()
In addition, there are requirements added to some of the algorithms to specify what their cancellation behaviour is and what the requirements of customisations of those algorithms are with respect to cancellation.
The key component that enables generic cancellation within sender-based operations is the
CPO.
This CPO takes a single parameter, which is the receiver passed to
, and returns a
that the operation should use to check for stop-requests for that operation.
As the caller of
typically has control over the receiver type it passes, it is able to customise
the
CPO for that receiver type to return a stop-token that it has control over and that
it can use to communicate a stop-request to the operation once it has started.
4.10.2. Support for cancellation is optional
Support for cancellation is optional, both on part of the author of the receiver and on part of the author of the sender.
If the receiver does not customise the
CPO then invoking the CPO on that receiver will
invoke the default implementation which returns
. This is a special
type that
is statically known to always return false
from the
method.
Sender code that tries to use this stop-token will in general result in code that handles stop-requests being compiled out and having little to no run-time overhead.
If the sender doesn’t call
, for example because the operation does not support
cancellation, then it will simply not respond to stop-requests from the caller.
Note that stop-requests are generally racy in nature as there is often a race betwen an operation completing naturally and the stop-request being made. If the operation has already completed or past the point at which it can be cancelled when the stop-request is sent then the stop-request may just be ignored. An application will typically need to be able to cope with senders that might ignore a stop-request anyway.
4.10.3. Cancellation is inherently racy
Usually, an operation will attach a stop-callback at some point inside the call to
so that
a subsequent stop-request will interrupt the logic.
A stop-request can be issued concurrently from another thread. This means the implementation of
needs to be careful to ensure that, once a stop-callback has been registered, that there are no data-races between
a potentially concurrently-executing stop-callback and the rest of the
implementation.
An implementation of
that supports cancellation will generally need to perform (at least)
two separate steps: launch the operation, subscribe a stop-callback to the receiver’s stop-token. Care needs
to be taken depending on the order in which these two steps are performed.
If the stop-callback is subscribed first and then the operation is launched, care needs to be taken to ensure that a stop-request that invokes the stop-callback on another thread after the stop-callback is registered but before the operation finishes launching does not either result in a missed cancellation request or a data-race. e.g. by performing an atomic write after the launch has finished executing
If the operation is launched first and then the stop-callback is subscribed, care needs to be taken to ensure
that if the launched operation completes concurrently on another thread that it does not destroy the operation-state
until after the stop-callback has been registered. e.g. by having the
implementation write to
an atomic variable once it has finished registering the stop-callback and having the concurrent completion handler
check that variable and either call the completion-signalling operation or store the result and defer calling the
receiver’s completion-signalling operation to the
call (which is still executing).
For an example of an implementation strategy for solving these data-races see § 1.4 Asynchronous Windows socket recv.
4.10.4. Cancellation design status
This paper currently includes the design for cancellation as proposed in [P2175R0] - "Composable cancellation for sender-based async operations". P2175R0 contains more details on the background motivation and prior-art and design rationale of this design.
It is important to note, however, that initial review of this design in the SG1 concurrency subgroup raised some concerns related to runtime overhead of the design in single-threaded scenarios and these concerns are still being investigated.
The design of P2175R0 has been included in this paper for now, despite its potential to change, as we believe that support for cancellation is a fundamental requirement for an async model and is required in some form to be able to talk about the semantics of some of the algorithms proposed in this paper.
This paper will be updated in the future with any changes that arise from the investigations into P2175R0.
4.11. Sender factories and adaptors are lazy
In an earlier revision of this paper, some of the proposed algorithms supported executing their logic eagerly; i.e., before the returned sender has been connected to a receiver and started. These algorithms were removed because eager execution has a number of negative semantic and performance implications.
We have originally included this functionality in the paper because of a long-standing belief that eager execution is a mandatory feature to be included in the standard Executors facility for that facility to be acceptable for accelerator vendors. A particular concern was that we must be able to write generic algorithms that can run either eagerly or lazily, depending on the kind of an input sender or scheduler that have been passed into them as arguments. We considered this a requirement, because the _latency_ of launching work on an accelerator can sometimes be considerable.
However, in the process of working on this paper and implementations of the features
proposed within, our set of requirements has shifted, as we understood the different
implementation strategies that are available for the feature set of this paper better,
and, after weighting the earlier concerns against the points presented below, we
have arrived at the conclusion that a purely lazy model is enough for most algorithms,
and users who intend to launch work earlier may use an algorithm such as
to achieve that goal. We have also come to deeply appreciate the fact that a purely
lazy model allows both the implementation and the compiler to have a much better
understanding of what the complete graph of tasks looks like, allowing them to better
optimize the code - also when targetting accelerators.
4.11.1. Eager execution leads to detached work or worse
One of the questions that arises with APIs that can potentially return
eagerly-executing senders is "What happens when those senders are destructed
without a call to
?" or similarly, "What happens if a call
to
is made, but the returned operation state is destroyed
before
is called on that operation state"?
In these cases, the operation represented by the sender is potentially executing concurrently in another thread at the time that the destructor of the sender and/or operation-state is running. In the case that the operation has not completed executing by the time that the destructor is run we need to decide what the semantics of the destructor is.
There are three main strategies that can be adopted here, none of which is particularly satisfactory:
-
Make this undefined-behaviour - the caller must ensure that any eagerly-executing sender is always joined by connecting and starting that sender. This approach is generally pretty hostile to programmers, particularly in the presence of exceptions, since it complicates the ability to compose these operations.
Eager operations typically need to acquire resources when they are first called in order to start the operation early. This makes eager algorithms prone to failure. Consider, then, what might happen in an expression such as
. Imaginewhen_all ( eager_op_1 (), eager_op_2 ())
starts an asynchronous operation successfully, but theneager_op_1 ()
throws. For lazy senders, that failure happens in the context of theeager_op_2 ()
algorithm, which handles the failure and ensures that async work joins on all code paths. In this case though -- the eager case -- the child operation has failed even beforewhen_all
has been called.when_all It then becomes the responsibility, not of the algorithm, but of the end user to handle the exception and ensure that
is joined before allowing the exception to propagate. If they fail to do that, they incur undefined behavior.eager_op_1 () -
Detach from the computation - let the operation continue in the background - like an implicit call to
. While this approach can work in some circumstances for some kinds of applications, in general it is also pretty user-hostile; it makes it difficult to reason about the safe destruction of resources used by these eager operations. In general, detached work necessitates some kind of garbage collection; e.g.,std :: thread :: detach ()
, to ensure resources are kept alive until the operations complete, and can make clean shutdown nigh impossible.std :: shared_ptr -
Block in the destructor until the operation completes. This approach is probably the safest to use as it preserves the structured nature of the concurrent operations, but also introduces the potential for deadlocking the application if the completion of the operation depends on the current thread making forward progress.
The risk of deadlock might occur, for example, if a thread-pool with a small number of threads is executing code that creates a sender representing an eagerly-executing operation and then calls the destructor of that sender without joining it (e.g. because an exception was thrown). If the current thread blocks waiting for that eager operation to complete and that eager operation cannot complete until some entry enqueued to the thread-pool’s queue of work is run then the thread may wait for an indefinite amount of time. If all thread of the thread-pool are simultaneously performing such blocking operations then deadlock can result.
There are also minor variations on each of these choices. For example:
-
A variation of (1): Call
if an eager sender is destructed without joining it. This is the approach thatstd :: terminate
destructor takes.std :: thread -
A variation of (2): Request cancellation of the operation before detaching. This reduces the chances of operations continuing to run indefinitely in the background once they have been detached but does not solve the lifetime- or shutdown-related challenges.
-
A variation of (3): Request cancellation of the operation before blocking on its completion. This is the strategy that
uses for its destructor. It reduces the risk of deadlock but does not eliminate it.std :: jthread
4.11.2. Eager senders complicate algorithm implementations
Algorithms that can assume they are operating on senders with strictly lazy
semantics are able to make certain optimizations that are not available if
senders can be potentially eager. With lazy senders, an algorithm can safely
assume that a call to
on an operation state strictly happens
before the execution of that async operation. This frees the algorithm from
needing to resolve potential race conditions. For example, consider an algorithm
that puts async operations in sequence by starting an operation only
after the preceding one has completed. In an expression like
, one my reasonably assume that
,
and
are sequenced and therefore do not need synchronisation. Eager algorithms
break that assumption.
When an algorithm needs to deal with potentially eager senders, the potential race conditions can be resolved one of two ways, neither of which is desirable:
-
Assume the worst and implement the algorithm defensively, assuming all senders are eager. This obviously has overheads both at runtime and in algorithm complexity. Resolving race conditions is hard.
-
Require senders to declare whether they are eager or not with a query. Algorithms can then implement two different implementation strategies, one for strictly lazy senders and one for potentially eager senders. This addresses the performance problem of (1) while compounding the complexity problem.
4.11.3. Eager senders incur cancellation-related overhead
Another implication of the use of eager operations is with regards to cancellation. The eagerly executing operation will not have access to the caller’s stop token until the sender is connected to a receiver. If we still want to be able to cancel the eager operation then it will need to create a new stop source and pass its associated stop token down to child operations. Then when the returned sender is eventually connected it will register a stop callback with the receiver’s stop token that will request stop on the eager sender’s stop source.
As the eager operation does not know at the time that it is launched what the
type of the receiver is going to be, and thus whether or not the stop token
returned from
is an
or not,
the eager operation is going to need to assume it might be later connected to a
receiver with a stop token that might actually issue a stop request. Thus it
needs to declare space in the operation state for a type-erased stop callback
and incur the runtime overhead of supporting cancellation, even if cancellation
will never be requested by the caller.
The eager operation will also need to do this to support sending a stop request to the eager operation in the case that the sender representing the eager work is destroyed before it has been joined (assuming strategy (5) or (6) listed above is chosen).
4.11.4. Eager senders cannot access execution context from the receiver
In sender/receiver, contextual information is passed from parent operations to their children by way of receivers. Information like stop tokens, allocators, current scheduler, priority, and deadline are propagated to child operations with custom receivers at the time the operation is connected. That way, each operation has the contextual information it needs before it is started.
But if the operation is started before it is connected to a receiver, then there isn’t a way for a parent operation to communicate contextual information to its child operations, which may complete before a receiver is ever attached.
4.12. Schedulers advertise their forward progress guarantees
To decide whether a scheduler (and its associated execution context) is sufficient for a specific task, it may be necessary to know what kind of forward progress guarantees it provides for the execution agents it creates. The C++ Standard defines the following forward progress guarantees:
-
concurrent, which requires that a thread makes progress eventually;
-
parallel, which requires that a thread makes progress once it executes a step; and
-
weakly parallel, which does not require that the thread makes progress.
This paper introduces a scheduler query function,
, which returns one of the enumerators of a new
type,
. Each enumerator of
corresponds to one of the aforementioned
guarantees.
4.13. Most sender adaptors are pipeable
To facilitate an intuitive syntax for composition, most sender adaptors are pipeable; they can be composed (piped) together with
.
This mechanism is similar to the
composition that C++ range adaptors support and draws inspiration from piping in *nix shells.
Pipeable sender adaptors take a sender as their first parameter and have no other sender parameters.
will pass the sender
as the first argument to the pipeable sender adaptor
. Pipeable sender adaptors support partial application of the parameters after the first. For example, all of the following are equivalent:
execution :: bulk ( snd , N , [] ( std :: size_t i , auto d ) {}); execution :: bulk ( N , [] ( std :: size_t i , auto d ) {})( snd ); snd | execution :: bulk ( N , [] ( std :: size_t i , auto d ) {});
Piping enables you to compose together senders with a linear syntax. Without it, you’d have to use either nested function call syntax, which would cause a syntactic inversion of the direction of control flow, or you’d have to introduce a temporary variable for each stage of the pipeline. Consider the following example where we want to execute first on a CPU thread pool, then on a CUDA GPU, then back on the CPU thread pool:
Syntax Style | Example |
---|---|
Function call (nested) |
|
Function call (named temporaries) |
|
Pipe |
|
Certain sender adaptors are not be pipeable, because using the pipeline syntax can result in confusion of the semantics of the adaptors involved. Specifically, the following sender adaptors are not pipeable.
-
andexecution :: when_all
: Since this sender adaptor takes a variadic pack of senders, a partially applied form would be ambiguous with a non partially applied form with an arity of one less.execution :: when_all_with_variant -
: This sender adaptor changes how the sender passed to it is executed, not what happens to its result, but allowing it in a pipeline makes it read as if it performed a function more similar toexecution :: on
.transfer
Sender consumers could be made pipeable, but we have chosen to not do so. However, since these are terminal nodes in a pipeline and nothing can be piped after them, we believe a pipe syntax may be confusing as well as unnecessary, as consumers cannot be chained. We believe sender consumers read better with function call syntax.
4.14. A range of senders represents an async sequence of data
Senders represent a single unit of asynchronous work. In many cases though, what is being modelled is a sequence of data arriving asynchronously, and you want computation to happen on demand, when each element arrives. This requires nothing more than what is in this paper and the range support in C++20. A range of senders would allow you to model such input as keystrikes, mouse movements, sensor readings, or network requests.
Given some expression
that is a range of senders, consider the following in a coroutine that returns an async generator type:
for ( auto snd : R ) { if ( auto opt = co_await execution :: done_as_optional ( std :: move ( snd ))) co_yield fn ( * std :: move ( opt )); else break ; }
This transforms each element of the asynchronous sequence
with the function
on demand, as the data arrives. The result is a new asynchronous sequence of the transformed values.
Now imagine that
is the simple expression
. This creates a lazy range of senders, each of which completes immediately with monotonically increasing integers. The above code churns through the range, generating a new infine asynchronous range of values [
,
,
, ...].
Far more interesting would be if
were a range of senders representing, say, user actions in a UI. The above code gives a simple way to respond to user actions on demand.
4.15. Senders can represent partial success
Receivers have three ways they can complete: with success, failure, or cancellation. This begs the question of how they can be used to represent async operations that partially succeed. For example, consider an API that reads from a socket. The connection could drop after the API has filled in some of the buffer. In cases like that, it makes sense to want to report both that the connection dropped and that some data has been successfully read.
Often in the case of partial success, the error condition is not fatal nor does it mean the API has failed to satisfy its post-conditions. It is merely an extra piece of information about the nature of the completion. In those cases, "partial success" is another way of saying "success". As a result, it is sensible to pass both the error code and the result (if any) through the value channel, as shown below:
// Capture a buffer for read_socket_async to fill in execution :: just ( array < byte , 1024 > {}) | execution :: let_value ([ socket ]( array < byte , 1024 >& buff ) { // read_socket_async completes with two values: an error_code and // a count of bytes: return read_socket_async ( socket , span { buff }) // For success (partial and full), specify the next action: | execution :: let_value ([]( error_code err , size_t bytes_read ) { if ( err != 0 ) { // OK, partial success. Decide how to deal with the partial results } else { // OK, full success here. } }); })
In other cases, the partial success is more of a partial failure. That happens when the error condition indicates that in some way the function failed to satisfy its post-conditions. In those cases, sending the error through the value channel loses valuable contextual information. It’s possible that bundling the error and the incomplete results into an object and passing it through the error channel makes more sense. In that way, generic algorithms will not miss the fact that a post-condition has not been met and react inappropriately.
Another possibility is for an async API to return a range of senders: if the API completes with full success, full error, or cancellation, the returned range contains just one sender with the result. Otherwise, if the API partially fails (doesn’t satisfy its post-conditions, but some incomplete result is available), the returned range would have two senders: the first containing the partial result, and the second containing the error. Such an API might be used in a coroutine as follows:
// Declare a buffer for read_socket_async to fill in array < byte , 1024 > buff ; for ( auto snd : read_socket_async ( socket , span { buff })) { try { if ( optional < size_t > bytes_read = co_await execution :: done_as_optional ( std :: move ( snd ))) // OK, we read some bytes into buff. Process them here.... } else { // The socket read was cancelled and returned no data. React // appropriately. } } catch (...) { // read_socket_async failed to meet its post-conditions. // Do some cleanup and propagate the error... } }
Finally, it’s possible to combine these two approaches when the API can both partially succeed (meeting its post-conditions) and partially fail (not meeting its post-conditions).
4.16. All awaitables are senders
Since C++20 added coroutines to the standard, we expect that coroutines and awaitables will be how a great many will choose to express their asynchronous code. However, in this paper, we are proposing to add a suite of asynchronous algorithms that accept senders, not awaitables. One might wonder whether and how these algorithms will be accessible to those who choose coroutines instead of senders.
In truth there will be no problem because all generally awaitable types automatically model the
concept. The adaptation is transparent and happens in the sender customization points, which are aware of awaitables. (By "generally awaitable" we mean types that don’t require custom
trickery from a promise type to make them awaitable.)
For an example, imagine a coroutine type called
that knows nothing about senders. It doesn’t implement any of the sender customization points. Despite that fact, and despite the fact that the
algorithm is constrained with the
concept, the following would compile and do what the user wants:
task < int > doSomeAsyncWork (); int main () { // OK, awaitable types satisfy the requirements for typed senders: auto o = this_thread :: sync_wait ( doSomeAsyncWork ()); }
Since awaitables are senders, writing a sender-based asynchronous algorithm is trivial if you have a coroutine task type: implement the algorithm as a coroutine. If you are not bothered by the possibility of allocations and indirections as a result of using coroutines, then there is no need to ever write a sender, a receiver, or an operation state.
4.17. Many senders can be trivially made awaitable
If you choose to implement your sender-based algorithms as coroutines, you’ll run into the issue of how to retrieve results from a passed-in sender. This is not a problem. If the coroutine type opts in to sender support -- trivial with the
utility -- then a large class of senders are transparently awaitable from within the coroutine.
For example, consider the following trivial implementation of the sender-based
algorithm:
template < class S > requires single - typed - sender < S &> // See [execution.coro_utils.as_awaitable] task < single - sender - value - type < S >> retry ( S s ) { for (;;) { try { co_return co_await s ; } catch (...) { } } }
Only some senders can be made awaitable directly because of the fact that callbacks are more expressive than coroutines. An awaitable expression has a single type: the result value of the async operation. In contrast, a callback can accept multiple arguments as the result of an operation. What’s more, the callback can have overloaded function call signatures that take different sets of arguments. There is no way to automatically map such senders into awaitables. The
utility recognizes as awaitables those senders that send a single value of a single type. To await another kind of sender, a user would have to first map its value channel into a single value of a single type -- say, with the
sender algorithm -- before
-ing that sender.
4.18. Cancellation of a sender can unwind a stack of coroutines
When looking at the sender-based
algorithm in the previous section, we can see that the value and error cases are correctly handled. But what about cancellation? What happens to a coroutine that is suspended awaiting a sender that completes by calling
?
When your task type’s promise inherits from
, what happens is this: the coroutine behaves as if an uncatchable exception had been thrown from the
expression. (It is not really an exception, but it’s helpful to think of it that way.) Provided that the promise types of the calling coroutines also inherit from
, or more generally implement a member function called
, the exception unwinds the chain of coroutines as if an exception were thrown except that it bypasses
clauses.
In order to "catch" this uncatchable done exception, one of the calling coroutines in the stack would have to await a sender that maps the done channel into either a value or an error. That is achievable with the
,
,
, or
sender adaptors. For instance, we can use
to "catch" the done signal and map it into an empty optional as shown below:
if ( auto opt = co_await execution :: done_as_optional ( some_sender )) { // OK, some_sender completed successfully, and opt contains the result. } else { // some_sender completed with a cancellation signal. }
As described in the section "All awaitables are senders", the sender customization points recognize awaitables and adapt them transparently to model the sender concept. When
-ing an awaitable and a receiver, the adaptation layer awaits the awaitable within a coroutine that implements
in its promise type. The effect of this is that an "uncatchable" done exception propagates seamlessly out of awaitables, causing
to be called on the receiver.
Obviously,
is a library extension of the coroutine promise interface. Many promise types will not implement
. When an uncatchable done exception tries to propagate through such a coroutine, it is treated as an unhandled exception and
is called. The solution, as described above, is to use a sender adaptor to handle the done exception before awaiting it. It goes without saying that any future Standard Library coroutine types ought to implement
. The author of [P1056R1], which proposes a standard coroutine task type, is in agreement.
4.19. Composition with parallel algorithms
The C++ Standard Library provides a large number of algorithms that offer the potential for non-sequential execution via the use of execution policies. The set of algorithms with execution policy overloads are often referred to as "parallel algorithms", although additional policies are available.
Existing policies, such as
, give the implementation permission to execute the algorithm in parallel. However, the choice of execution resources used to perform the work is left to the implementation.
We will propose a customization point for combining schedulers with policies in order to provide control over where work will execute.
template < class ExecutionPolicy > implementation - defined executing_on ( execution :: scheduler auto scheduler , ExecutionPolicy && policy );
This function would return an object of an implementation-defined type which can be used in place of an execution policy as the first argument to one of the parallel algorithms. The overload selected by that object should execute its computation as requested by
while using
to create any work to be run. The expression may be ill-formed if
is not able to support the given policy.
The existing parallel algorithms are synchronous; all of the effects performed by the computation are complete before the algorithm returns to its caller. This remains unchanged with the
customization point.
In the future, we expect additional papers will propose asynchronous forms of the parallel algorithms which (1) return senders rather than values or
and (2) where a customization point pairing a sender with an execution policy would similarly be used to
obtain an object of implementation-defined type to be provided as the first argument to the algorithm.
4.20. User-facing sender factories
A sender factory is an algorithm that takes no senders as parameters and returns a sender.
4.20.1. execution :: schedule
execution :: sender auto schedule ( execution :: scheduler auto scheduler );
Returns a sender describing the start of a task graph on the provided scheduler. See § 4.2 Schedulers represent execution contexts.
execution :: scheduler auto sch1 = get_system_thread_pool (). scheduler (); execution :: sender auto snd1 = execution :: schedule ( sch1 ); // snd1 describes the creation of a new task on the system thread pool
4.20.2. execution :: just
execution :: sender auto just ( auto ... && values );
Returns a sender with no completion schedulers, which sends the provided values. The input values are decay-copied into the returned sender. When the returned sender is connected to a receiver, the values are moved into the operation state if the sender is an rvalue; otherwise, they are copied. Then xvalues referencing the values in the operation state are passed to the receiver’s
.
execution :: sender auto snd1 = execution :: just ( 3.14 ); execution :: sender auto then1 = execution :: then ( snd1 , [] ( double d ) { std :: cout << d << " \n " ; }); execution :: sender auto snd2 = execution :: just ( 3.14 , 42 ); execution :: sender auto then2 = execution :: then ( snd1 , [] ( double d , int i ) { std :: cout << d << ", " << i << " \n " ; }); std :: vector v3 { 1 , 2 , 3 , 4 , 5 }; execution :: sender auto snd3 = execution :: just ( v3 ); execution :: sender auto then3 = execution :: then ( snd3 , [] ( std :: vector < int >&& v3copy ) { for ( auto && e : v3copy ) { e *= 2 ; } return std :: move ( v3copy ); } auto && [ v3copy ] = this_thread :: sync_wait ( then3 ). value (); // v3 contains {1, 2, 3, 4, 5}; v3copy will contain {2, 4, 6, 8, 10}. execution :: sender auto snd4 = execution :: just ( std :: vector { 1 , 2 , 3 , 4 , 5 }); execution :: sender auto then4 = execution :: then ( std :: move ( snd4 ), [] ( std :: vector < int >&& v4 ) { for ( auto && e : v4 ) { e *= 2 ; } return std :: move ( v4 ); }); auto && [ v4 ] = this_thread :: sync_wait ( std :: move ( then4 )). value (); // v4 contains {2, 4, 6, 8, 10}. No vectors were copied in this example.
4.20.3. execution :: transfer_just
execution :: sender auto transfer_just ( execution :: scheduler auto scheduler , auto ... && values );
Returns a sender whose value completion scheduler is the provided scheduler, which sends the provided values in the same manner as
.
execution :: sender auto vals = execution :: transfer_just ( get_system_thread_pool (). scheduler (), 1 , 2 , 3 ); execution :: sender auto snd = execution :: then ( vals , []( auto ... args ) { std :: ( args ...); }); // when snd is executed, it will print "123"
This adaptor is included as it greatly simplifies lifting values into senders.
4.20.4. execution :: just_error
execution :: sender auto just_error ( auto && error );
Returns a sender with no completion schedulers, which completes with the specified error. If the provided error is an lvalue reference, a copy is made inside the returned sender and a non-const lvalue reference to the copy is sent to the receiver’s
. If the provided value is an rvalue reference, it is moved into the returned sender and an rvalue reference to it is sent to the receiver’s
.
4.20.5. execution :: just_done
execution :: sender auto just_done ();
Returns a sender with no completion schedulers, which completes immediately by calling the receiver’s
.
4.21. User-facing sender adaptors
A sender adaptor is an algorithm that takes one or more senders, which it may
, as parameters, and returns a sender, whose completion is related to the sender arguments it has received.
Sender adaptors are lazy, that is, they are never allowed to submit any work for execution prior to the returned sender being started later on, and are also guaranteed to not start any input senders passed into them. Sender consumers such as § 4.21.13 execution::ensure_started, § 4.22.1 execution::start_detached, and § 4.22.2 this_thread::sync_wait start senders.
For more implementer-centric description of starting senders, see § 5.5 Sender adaptors are lazy.
4.21.1. execution :: transfer
execution :: sender auto transfer ( execution :: sender auto input , execution :: scheduler auto scheduler );
Returns a sender describing the transition from the execution agent of the input sender to the execution agent of the target scheduler. See § 4.6 Execution context transitions are explicit.
execution :: scheduler auto cpu_sched = get_system_thread_pool (). scheduler (); execution :: scheduler auto gpu_sched = cuda :: scheduler (); execution :: sender auto cpu_task = execution :: schedule ( cpu_sched ); // cpu_task describes the creation of a new task on the system thread pool execution :: sender auto gpu_task = execution :: transfer ( cpu_task , gpu_sched ); // gpu_task describes the transition of the task graph described by cpu_task to the gpu
4.21.2. execution :: then
execution :: sender auto then ( execution :: sender auto input , std :: invocable < values - sent - by ( input ) ... > function );
returns a sender describing the task graph described by the input sender, with an added node of invoking the provided function with the values sent by the input sender as arguments.
is guaranteed to not begin executing
until the returned sender is started.
execution :: sender auto input = get_input (); execution :: sender auto snd = execution :: then ( input , []( auto ... args ) { std :: ( args ...); }); // snd describes the work described by pred // followed by printing all of the values sent by pred
This adaptor is included as it is necessary for writing any sender code that actually performs a useful function.
4.21.3. execution :: upon_ *
execution :: sender auto upon_error ( execution :: sender auto input , std :: invocable < errors - sent - by ( input ) ... > function ); execution :: sender auto upon_done ( execution :: sender auto input , std :: invocable auto function );
and
are similar to
, but where
works with values sent by the input sender,
works with errors, and
is invoked when the "done" signal is sent.
4.21.4. execution :: let_ *
execution :: sender auto let_value ( execution :: sender auto input , std :: invocable < values - sent - by ( input ) ... > function ); execution :: sender auto let_error ( execution :: sender auto input , std :: invocable < errors - sent - by ( input ) ... > function ); execution :: sender auto let_done ( execution :: sender auto input , std :: invocable auto function );
is very similar to
: when it is started, it invokes the provided function with the values sent by the input sender as arguments. However, where the sender returned from
sends exactly what that function ends up returning -
requires that the function return a sender, and the sender returned by
sends the values sent by the sender returned from the callback. This is similar to the notion of "future unwrapping" in future/promise-based frameworks.
is guaranteed to not begin executing
until the returned sender is started.
and
are similar to
, but where
works with values sent by the input sender,
works with errors, and
is invoked when the "done" signal is sent.
4.21.5. execution :: on
execution :: sender auto on ( execution :: scheduler auto sched , execution :: sender auto snd );
Returns a sender which, when started, will start the provided sender on an execution agent belonging to the execution context associated with the provided scheduler. This returned sender has no completion schedulers.
4.21.6. execution :: into_variant
execution :: sender auto into_variant ( execution :: sender auto snd );
Returns a sender which sends a variant of tuples of all the possible sets of types sent by the input sender. Senders can send multiple sets of values depending on runtime conditions; this is a helper function that turns them into a single variant value.
4.21.7. execution :: done_as_optional
execution :: sender auto done_as_optional ( single - typed - sender auto snd );
Returns a sender that maps the value channel from a
to an
, and maps the done channel to a value of an empty
.
4.21.8. execution :: done_as_error
template < move_constructible Error > execution :: sender auto done_as_error ( execution :: sender auto snd , Error err = Error {} );
Returns a sender that maps the done channel to an error of
.
4.21.9. execution :: bulk
execution :: sender auto bulk ( execution :: sender auto input , std :: integral auto size , invocable < decltype ( size ), values - sent - by ( input ) ... > function );
Returns a sender describing the task of invoking the provided function with every index in the provided shape along with the values sent by the input sender. The returned sender completes once all invocations have completed, or an error has occurred. If it completes by sending values, they are equivalent to those sent by the input sender.
No instance of
will begin executing until the returned sender is started. Each invocation of
runs in an execution agent whose forward progress guarantees are determined by the scheduler on which they are run. All agents created by a single use
of
execute with the same guarantee. This allows, for instance, a scheduler to execute all invocations of the function in parallel.
The
operation is intended to be used at the point where the number of agents to be created is known and provided to
via its
parameter. For some parallel computations, the number of agents to be created may be a function of the input data or
dynamic conditions of the execution environment. In such cases,
can be combined with additional operations such as
to deliver dynamic shape information to the
operation.
In this proposal, only integral types are used to specify the shape of the bulk section. We expect that future papers may wish to explore extensions of the interface to explore additional kinds of shapes, such as multi-dimensional grids, that are commonly used for parallel computing tasks.
4.21.10. execution :: split
execution :: sender auto split ( execution :: sender auto sender );
If the provided sender is a multi-shot sender, returns that sender. Otherwise, returns a multi-shot sender which sends values equivalent to the values sent by the provided sender. See § 4.7 Senders can be either multi-shot or single-shot.
4.21.11. execution :: when_all
execution :: sender auto when_all ( execution :: sender auto ... inputs ); execution :: sender auto when_all_with_variant ( execution :: sender auto ... inputs );
returns a sender that completes once all of the input senders have completed. It is constrained to only accept senders that can complete with a single set of values (_i.e._, it only calls one overload of
on its receiver). The values sent by this sender are the values sent by each of the input senders, in order of the arguments passed to
. It completes inline on the execution context on which the last input sender completes, unless stop is requested before
is started, in which case it completes inline within the call to
.
does the same, but it adapts all the input senders using
, and so it does not constrain the input arguments as
does.
The returned sender has no completion schedulers.
See § 4.9 Senders are joinable.
execution :: scheduler auto sched = thread_pool . scheduler (); execution :: sender auto sends_1 = ...; execution :: sender auto sends_abc = ...; execution :: sender auto both = execution :: when_all ( sched , sends_1 , sends_abc ); execution :: sender auto final = execution :: then ( both , []( auto ... args ){ std :: cout << std :: format ( "the two args: {}, {}" , args ...); }); // when final executes, it will print "the two args: 1, abc"
4.21.12. execution :: transfer_when_all
execution :: sender auto transfer_when_all ( execution :: scheduler auto sched , execution :: sender auto ... inputs ); execution :: sender auto transfer_when_all_with_variant ( execution :: scheduler auto sched , execution :: sender auto ... inputs );
Similar to § 4.21.11 execution::when_all, but returns a sender whose value completion scheduler is the provided scheduler.
See § 4.9 Senders are joinable.
4.21.13. execution :: ensure_started
execution :: sender auto ensure_started ( execution :: sender auto sender );
Once
returns, it is known that the provided sender has been connected and
has been called on the resulting operation state (see § 5.2 Operation states represent work); in other words, the work described by the provided sender has been submitted
for execution on the appropriate execution contexts. Returns a sender which completes when the provided sender completes and sends values equivalent to those of the provided sender.
If the returned sender is destroyed before
is called, or if
is called but the
returned operation-state is destroyed before
is called, then a stop-request is sent to the eagerly launched
operation and the operation is detached and will run to completion in the background. Its result will be discarded when it
eventually completes.
Note that the application will need to make sure that resources are kept alive in the case that the operation detaches.
e.g. by holding a
to those resources or otherwise having some out-of-band way to signal completion of
the operation so that resource release can be sequenced after the completion.
4.22. User-facing sender consumers
A sender consumer is an algorithm that takes one or more senders, which it may
, as parameters, and does not return a sender.
4.22.1. execution :: start_detached
void start_detached ( execution :: sender auto sender );
Like
, but does not return a value; if the provided sender sends an error instead of a value,
is called.
4.22.2. this_thread :: sync_wait
auto sync_wait ( execution :: sender auto sender ) requires ( always - sends - same - values ( sender )) -> std :: optional < std :: tuple < values - sent - by ( sender ) >> ;
is a sender consumer that submits the work described by the provided sender for execution, similarly to
, except that it blocks the current
or thread of
until the work is completed, and returns
an optional tuple of values that were sent by the provided sender on its completion of work. Where § 4.20.1 execution::schedule and § 4.20.3 execution::transfer_just are meant to enter the domain of senders,
is meant to exit the domain of
senders, retrieving the result of the task graph.
If the provided sender sends an error instead of values,
throws that error as an exception, or rethrows the original exception if the error is of type
.
If the provided sender sends the "done" signal instead of values,
returns an empty optional.
For an explanation of the
clause, see § 5.8 Most senders are typed. That clause also explains another sender consumer, built on top of
:
.
Note: This function is specified inside
, and not inside
. This is because
has to block the current execution agent, but determining what the current execution agent is is not reliable. Since the standard
does not specify any functions on the current execution agent other than those in
, this is the flavor of this function that is being proposed. If C++ ever obtains fibers, for instance, we expect that a variant of this function called
would be provided. We also expect that runtimes with execution agents that use different synchronization mechanisms than
's will provide their own flavors of
as well (assuming their execution agents have the means
to block in a non-deadlock manner).
4.23. execution :: execute
In addition to the three categories of functions presented above, we also propose to include a convenience function for fire-and-forget eager one-way submission of an invocable to a scheduler, to fulfil the role of one-way executors from P0443.
void execution :: execute ( execution :: schedule auto sched , std :: invocable auto fn );
Submits the provided function for execution on the provided scheduler, as-if by:
auto snd = execution :: schedule ( sched ); auto work = execution :: then ( snd , fn ); execution :: start_detached ( work );
5. Design - implementer side
5.1. Receivers serve as glue between senders
A receiver is a callback that supports more than one channel. In fact, it supports three of them:
-
, which is the moral equivalent of anset_value
or a function call, which signals successful completion of the operation its execution depends on;operator () -
, which signals that an error has happened during scheduling of the current work, executing the current work, or at some earlier point in the sender chain; andset_error -
, which signals that the operation completed without succeeding (set_done
) and without failing (set_value
). This result is often used to indicate that the operation stopped early, typically because it was asked to do so because the result is no longer needed.set_error
Exactly one of these channels must be successfully (i.e. without an exception being thrown) invoked on a receiver before it is destroyed; if a call to
failed with an exception, either
or
must be invoked on the same receiver. These
requirements are know as the receiver contract.
While the receiver interface may look novel, it is in fact very similar to the interface of
, which provides the first two signals as
and
, and it’s possible to emulate the third channel with lifetime management of the promise.
Receivers are not a part of the end-user-facing API of this proposal; they are necessary to allow unrelated senders communicate with each other, but the only users who will interact with receivers directly are authors of senders.
Receivers are what is passed as the second argument to § 5.3 execution::connect.
5.2. Operation states represent work
An operation state is an object that represents work. Unlike senders, it is not a chaining mechanism; instead, it is a concrete object that packages the work described by a full sender chain, ready to be executed. An operation state is neither movable nor
copyable, and its interface consists of a single algorithm:
, which serves as the submission point of the work represented by a given operation state.
Operation states are not a part of the user-facing API of this proposal; they are necessary for implementing sender consumers like
and
, and the knowledge of them is necessary to implement senders, so the only users who will
interact with operation states directly are authors of senders and authors of sender algorithms.
The return value of § 5.3 execution::connect must satisfy the operation state concept.
5.3. execution :: connect
is a customization point which connects senders with receivers, resulting in an operation state that will ensure that the receiver contract of the receiver passed to
will be fulfilled.
execution :: sender auto snd = some input sender ; execution :: receiver auto rcv = some receiver ; execution :: operation_state auto state = execution :: connect ( snd , rcv ); execution :: start ( state ); // at this point, it is guaranteed that the work represented by state has been submitted // to an execution context, and that execution context will eventually fulfill the // receiver contract of rcv // operation states are not movable, and therefore this operation state object must be // kept alive until the operation finishes
5.4. Sender algorithms are customizable
Senders being able to advertise what their completion schedulers are fulfills one of the promises of senders: that of being able to customize an implementation of a sender algorithm based on what scheduler any work it depends on will complete on.
The simple way to provide customizations for functions like
, that is for sender adaptors and sender consumers, is to follow the customization scheme that has been adopted for C++20 ranges library; to do that, we would define
the expression
to be equivalent to:
-
, if that expression is well formed; otherwisesender . then ( invocable ) -
, performed in a context where this call always performs ADL, if that expression is well formed; otherwisethen ( sender , invocable ) -
a default implementation of
, which returns a sender adaptor, and then define the exact semantics of said adaptor.then
However, this definition is problematic. Imagine another sender adaptor,
, which is a structured abstraction for a loop over an index space. Its default implementation is just a for loop. However, for accelerator runtimes like CUDA, we would like sender algorithms
like
to have specialized behavior, which invokes a kernel of more than one thread (with its size defined by the call to
); therefore, we would like to customize
for CUDA senders to achieve this. However, there’s no reason for CUDA kernels to
necessarily customize the
sender adaptor, as the generic implementation is perfectly sufficient. This creates a problem, though; consider the following snippet:
execution :: scheduler auto cuda_sch = cuda_scheduler {}; execution :: sender auto initial = execution :: schedule ( cuda_sch ); // the type of initial is a type defined by the cuda_scheduler // let’s call it cuda::schedule_sender<> execution :: sender auto next = execution :: then ( cuda_sch , []{ return 1 ; }); // the type of next is a standard-library implementation-defined sender adaptor // that wraps the cuda sender // let’s call it execution::then_sender_adaptor<cuda::schedule_sender<>> execution :: sender auto kernel_sender = execution :: bulk ( next , shape , []( int i ){ ... });
How can we specialize the
sender adaptor for our wrapped
? Well, here’s one possible approach, taking advantage of ADL (and the fact that the definition of "associated namespace" also recursively enumerates the associated namespaces of all template
parameters of a type):
namespace cuda :: for_adl_purposes { template < typename ... SentValues > class schedule_sender { execution :: operation_state auto connect ( execution :: receiver auto rcv ); execution :: scheduler auto get_completion_scheduler () const ; }; execution :: sender auto bulk ( execution :: sender auto && input , execution :: shape auto && shape , invocable < sender - values ( input ) > auto && fn ) { // return a cuda sender representing a bulk kernel launch } } // namespace cuda::for_adl_purposes
However, if the input sender is not just a
like in the example above, but another sender that overrides
by itself, as a member function, because its author believes they know an optimization for bulk - the specialization above will no
longer be selected, because a member function of the first argument is a better match than the ADL-found overload.
This means that well-meant specialization of sender algorithms that are entirely scheduler-agnostic can have negative consequences. The scheduler-specific specialization - which is essential for good performance on platforms providing specialized ways to launch certain sender algorithms - would not be selected in such cases. But it’s really the scheduler that should control the behavior of sender algorithms when a non-default implementation exists, not the sender. Senders merely describe work; schedulers, however, are the handle to the runtime that will eventually execute said work, and should thus have the final say in how the work is going to be executed.
Therefore, we are proposing the following customization scheme (also modified to take § 5.9 Ranges-style CPOs vs tag_invoke into account): the expression
, for any given sender algorithm that accepts a sender as its first argument, should be
equivalent to:
-
, if that expression is well-formed; otherwisetag_invoke ( < sender - algorithm > , get_completion_scheduler < Signal > ( sender ), sender , args ...) -
, if that expression is well-formed; otherwisetag_invoke ( < sender - algorithm > , sender , args ...) -
a default implementation, if there exists a default implementation of the given sender algorithm.
where
is one of
,
, or
; for most sender algorithms, the completion scheduler for
would be used, but for some (like
or
), one of the others would be used.
For sender algorithms which accept concepts other than
as their first argument, we propose that the customization scheme remains as it has been in [P0443R14] so far, except it should also use
.
5.5. Sender adaptors are lazy
Contrary to early revisions of this paper, we propose to make all sender adaptors perform strictly lazy submission, unless specified otherwise (the one notable exception in this paper is § 4.21.13 execution::ensure_started, whose sole purpose is to start an input sender).
Strictly lazy submission means that there is a guarantee that no work is submitted to an execution context before a receiver is connected to a sender, and
is called on the resulting operation state.
5.6. Lazy senders provide optimization opportunities
Because lazy senders fundamentally describe work, instead of describing or representing the submission of said work to an execution context, and thanks to the flexibility of the customization of most sender algorithms, they provide an opportunity for fusing multiple algorithms in a sender chain together, into a single function that can later be submitted for execution by an execution context. There are two ways this can happen.
The first (and most common) way for such optimizations to happen is thanks to the structure of the implementation: because all the work is done within callbacks invoked on the completion of an earlier sender, recursively up to the original source of computation, the compiler is able to see a chain of work described using senders as a tree of tail calls, allowing for inlining and removal of most of the sender machinery. In fact, when work is not submitted to execution contexts outside of the current thread of execution, compilers are capable of removing the senders abstraction entirely, while still allowing for composition of functions across different parts of a program.
The second way for this to occur is when a sender algorithm is specialized for a specific set of arguments. For instance, we expect that, for senders which are known to have been started already, § 4.21.13 execution::ensure_started will be an identity transformation, because the sender algorithm will be specialized for such senders. Similarly, an implementation could recognize two subsequent § 4.21.9 execution::bulks of compatible shapes, and merge them together into a single submission of a GPU kernel.
5.7. Execution context transitions are two-step
Because
takes a sender as its first argument, it is not actually directly customizable by the target scheduler. This is by design: the target scheduler may not know how to transition from a scheduler such as a CUDA scheduler;
transitioning away from a GPU in an efficient manner requires making runtime calls that are specific to the GPU in question, and the same is usually true for other kinds of accelerators too (or for scheduler running on remote systems). To avoid this problem,
specialized schedulers like the ones mentioned here can still hook into the transition mechanism, and inject a sender which will perform a transition to the regular CPU execution context, so that any sender can be attached to it.
This, however, is a problem: because customization of sender algorithms must be controlled by the scheduler they will run on (see § 5.4 Sender algorithms are customizable), the type of the sender returned from
must be controllable by the target scheduler. Besides, the target
scheduler may itself represent a specialized execution context, which requires additional work to be performed to transition to it. GPUs and remote node schedulers are once again good examples of such schedulers: executing code on their execution contexts
requires making runtime API calls for work submission, and quite possibly for the data movement of the values being sent by the input sender passed into
.
To allow for such customization from both ends, we propose the inclusion of a secondary transitioning sender adaptor, called
. This adaptor is a form of
, but takes an additional, second argument: the input sender. This adaptor is not
meant to be invoked manually by the end users; they are always supposed to invoke
, to ensure that both schedulers have a say in how the transitions are made. Any scheduler that specializes
shall ensure that the
return value of their customization is equivalent to
, where
is a successor of
that sends values equivalent to those sent by
.
The default implementation of
is
.
5.8. Most senders are typed
All senders should advertise the types they will send when they complete. This is necessary for a number of features, and writing code in a way that’s agnostic of whether an input sender is typed or not in common sender adaptors such as
is
hard.
The mechanism for this advertisement is the same as in [P0443R14]; the way to query the types is through
.
is a template that takes two arguments: one is a tuple-like template, the other is a variant-like template. The tuple-like argument is required to represent senders sending more than one value (such as
). The variant-like
argument is required to represent senders that choose which specific values to send at runtime.
There’s a choice made in the specification of § 4.22.2 this_thread::sync_wait: it returns a tuple of values sent by the sender passed to it, wrapped in
to handle the
signal. However, this assumes that those values can be represented as a
tuple, like here:
execution :: sender auto sends_1 = ...; execution :: sender auto sends_2 = ...; execution :: sender auto sends_3 = ...; auto [ a , b , c ] = this_thread :: sync_wait ( execution :: transfer_when_all ( execution :: get_completion_scheduler < execution :: set_value_t > ( sends_1 ), sends_1 , sends_2 , sends_3 )). value (); // a == 1 // b == 2 // c == 3
This works well for senders that always send the same set of arguments. If we ignore the possibility of having a sender that sends different sets of arguments into a receiver, we can specify the "canonical" (i.e. required to be followed by all senders) form of
of a sender which sends
to be as follows:
template < template < typename ... > typename TupleLike > using value_types = TupleLike ;
If senders could only ever send one specific set of values, this would probably need to be the required form of
for all senders; defining it otherwise would cause very weird results and should be considered a bug.
This matter is somewhat complicated by the fact that (1)
for receivers can be overloaded and accept different sets of arguments, and (2) senders are allowed to send multiple different sets of values, depending on runtime conditions, the data they
consumed, and so on. To accomodate this, [P0443R14] also includes a second template parameter to
, one that represents a variant-like type. If we permit such senders, we would almost certainly need to require that the canonical form of
for all senders (to ensure consistency in how they are handled, and to avoid accidentally interpreting a user-provided variant as a sender-provided one) sending the different sets of arguments
,
, ...,
to be as follows:
template < template < typename ... > typename TupleLike , template < typename ... > typename VariantLike > using value_types = VariantLike < TupleLike < Types1 ... > , TupleLike < Types2 ... > , ..., TupleLike < Types3 ... > > ;
This, however, introduces a couple of complications:
-
A
sender would also need to follow this structure, so the correct type for storing the value sent by it would bejust ( 1 )
or some such. This introduces a lot of compile time overhead for the simplest senders, and this overhead effectively exists in all places in the code wherestd :: variant < std :: tuple < int >>
is queried, regardless of the tuple-like and variant-like templates passed to it. Such overhead does exist if only the tuple-like parameter exists, but is made much worse by adding this second wrapping layer.value_types -
As a consequence of (1): because
needs to store the above type, it can no longer return just async_wait
forstd :: tuple < int >
; it has to returnjust ( 1 )
. C++ currently does not have an easy way to destructure this; it may get less awkward with pattern matching, but even then it seems extremely heavyweight to involve variants in this API, and for the purpose of generic code, the kind of the return type ofstd :: variant < std :: tuple < int >>
must be the same across all sender types.sync_wait
One possible solution to (2) above is to place a requirement on
that it can only accept senders which send only a single set of values, therefore removing the need for
to appear in its API; because of this, we propose to expose both
, which is a simple, user-friendly version of the sender consumer, but requires that
have only one possible variant, and
, which accepts any sender, but returns an optional whose value type is the variant of all the
possible tuples sent by the input sender:
auto sync_wait_with_variant ( execution :: sender auto sender ) -> std :: optional < std :: variant < std :: tuple < values 0 - sent - by ( sender ) > , std :: tuple < values 1 - sent - by ( sender ) > , ..., std :: tuple < values n - sent - by ( sender ) > >> ; auto sync_wait ( execution :: sender auto sender ) requires ( always - sends - same - values ( sender )) -> std :: optional < std :: tuple < values - sent - by ( sender ) >> ;
5.9. Ranges-style CPOs vs tag_invoke
The contemporary technique for customization in the Standard Library is customization point objects. A customization point object, will it look for member functions and then for nonmember functions with the same name as the customization point, and calls those if they match. This is the technique used by the C++20 ranges library, and previous executors proposals ([P0443R14] and [P1897R3]) intended to use it as well. However, it has several unfortunate consequences:
-
It does not allow for easy propagation of customization points unknown to the adaptor to a wrapped object, which makes writing universal adapter types much harder - and this proposal uses quite a lot of those.
-
It effectively reserves names globally. Because neither member names nor ADL-found functions can be qualified with a namespace, every customization point object that uses the ranges scheme reserves the name for all types in all namespaces. This is unfortunate due to the sheer number of customization points already in the paper, but also ones that we are envisioning in the future. It’s also a big problem for one of the operations being proposed already:
. We imagine that if, in the future, C++ was to gain fibers support, we would want to also havesync_wait
, in addition tostd :: this_fiber :: sync_wait
. However, because we would want the names to be the same in both cases, we would need to make the names of the customizations not match the names of the customization points. This is undesirable.std :: this_thread :: sync_wait
This paper proposes to instead use the mechanism described in [P1895R0]:
; the wording for
has been incorporated into the proposed specification in this paper.
In short, instead of using globally reserved names,
uses the type of the customization point object itself as the mechanism to find customizations. It globally reserves only a single name -
- which itself is used the same way that
ranges-style customization points are used. All other customization points are defined in terms of
. For example, the customization for
will call
, instead of attempting
to invoke
, and then
if the member call is not valid.
Using
has the following benefits:
-
It reserves only a single global name, instead of reserving a global name for every customization point object we define.
-
It is possible to propagate customizations to a subobject, because the information of which customization point is being resolved is in the type of an argument, and not in the name of the function:
// forward most customizations to a subobject template < typename Tag , typename ... Args > friend auto tag_invoke ( Tag && tag , wrapper & self , Args && ... args ) { return std :: forward < Tag > ( tag )( self . subobject , std :: forward < Args > ( args )...); } // but override one of them with a specific value friend auto tag_invoke ( specific_customization_point_t , wrapper & self ) { return self . some_value ; } -
It is possible to pass those as template arguments to types, because the information of which customization point is being resolved is in the type. Similarly to how [P0443R14] defines a polymorphic executor wrapper which accepts a list of properties it supports, we can imagine scheduler and sender wrappers that accept a list of queries and operations they support. That list can contain the types of the customization point objects, and the polymorphic wrappers can then specialize those customization points on themselves using
, dispatching to manually constructed vtables containing pointers to specialized implementations for the wrapped objects. For an example of such a polymorphic wrapper, seetag_invoke
(example).unifex :: any_unique
6. Specification
Much of this wording follows the wording of [P0443R14].
§ 7 General utilities library [utilities] is meant to be a diff relative to the wording of the [utilities] clause of [N4885]. This diff applies changes from [P1895R0].
§ 8 Thread support library [thread] is meant to be a diff relative to the wording of the [thread] clause of [N4885]. This diff applies changes from [P2175R0].
§ 9 Execution control library [execution] is meant to be added as a new library clause to the working draft of C++.
7. General utilities library [utilities]
7.1. Function objects [function.objects]
7.1.1. Header < functional >
synopsis [functional.syn]
At the end of this subclause, insert the following declarations into the synopsis within
:
// [func.tag_invoke], tag_invoke inline namespace unspecified { inline constexpr unspecified tag_invoke = unspecified ; } template < auto & Tag > using tag_t = decay_t < decltype ( Tag ) > ; template < class Tag , class ... Args > concept tag_invocable = invocable < decltype ( tag_invoke ), Tag , Args ... > ; template < class Tag , class ... Args > concept nothrow_tag_invocable = tag_invocable < Tag , Args ... > && is_nothrow_invocable_v < decltype ( tag_invoke ), Tag , Args ... > ; template < class Tag , class ... Args > using tag_invoke_result = invoke_result < decltype ( tag_invoke ), Tag , Args ... > ; template < class Tag , class ... Args > using tag_invoke_result_t = invoke_result_t < decltype ( tag_invoke ), Tag , Args ... > ;
7.1.2. execution :: tag_invoke
[func.tag_invoke]
Insert this section as a new subclause, between Searchers [func.search] and Class template
[unord.hash].
The name
denotes a customization point object. For some subexpressions
std :: tag_invoke and
tag ,
args ... is expression-equivalent to an unqualified call to
tag_invoke ( tag , args ...) with overload resolution performed in a context that includes the declaration:
tag_invoke ( decay - copy ( tag ), args ...) void tag_invoke (); and that does not include the the
name.
std :: tag_invoke
8. Thread support library [thread]
Note: The specification in this section is incomplete; it does not provide an API specification for the new types added into
. For a less formal specification of the missing pieces, see the "Proposed Changes" section of [P2175R0]. A future revision
of this paper will contain a full specification for the new types.
8.1. Stop tokens [thread.stoptoken]
8.1.1. Header < stop_token >
synopsis [thread.stoptoken.syn]
At the beginning of this subclause, insert the following declarations into the synopsis within
:
template < template < typename > class > struct check - type - alias - exists ; // exposition-only template < typename T > concept stoppable_token = see - below ; template < typename T , typename CB , typename Initializer = CB > concept stoppable_token_for = see - below ; template < typename T > concept unstoppable_token = see - below ;
At the end of this subclause, insert the following declarations into the synopsis of within
:
// [stoptoken.never], class never_stop_token class never_stop_token ; // [stoptoken.inplace], class in_place_stop_token class in_place_stop_token ; // [stopsource.inplace], class in_place_stop_source class in_place_stop_source ; // [stopcallback.inplace], class template in_place_stop_callback template < typename Callback > class in_place_stop_callback ;
8.1.2. Stop token concepts [thread.stoptoken.concepts]
Insert this section as a new subclause between Header
synopsis [thread.stoptoken.syn] and Class
[stoptoken].
The
concept checks for the basic interface of a “stop token” which is copyable and allows polling to see if stop has been requested and also whether a stop request is possible. It also requires an associated nested template-type-alias,
stoppable_token , that identifies the stop-callback type to use to register a callback to be executed if a stop-request is ever made on a stoppable_token of type,
T :: callback_type < CB > . The
T concept checks for a stop token type compatible with a given callback type. The
stoppable_token_for concept checks for a stop token type that does not allow stopping.
unstoppable_token template < typename T > concept stoppable_token = copy_constructible < T > && move_constructible < T > && is_nothrow_copy_constructible_v < T > && is_nothrow_move_constructible_v < T > && equality_comparable < T > && requires ( const T & token ) { { token . stop_requested () } noexcept -> boolean - testable ; { token . stop_possible () } noexcept -> boolean - testable ; typename check - type - alias - exists < T :: template callback_type > ; }; template < typename T , typename CB , typename Initializer = CB > concept stoppable_token_for = stoppable_token < T > && invocable < CB > && requires { typename T :: template callback_type < CB > ; } && constructible_from < CB , Initializer > && constructible_from < typename T :: template callback_type < CB > , T , Initializer > && constructible_from < typename T :: template callback_type < CB > , T & , Initializer > && constructible_from < typename T :: template callback_type < CB > , const T , Initializer > && constructible_from < typename T :: template callback_type < CB > , const T & , Initializer > ; template < typename T > concept unstoppable_token = stoppable_token < T > && requires { { T :: stop_possible () } -> boolean - testable ; } && ( ! T :: stop_possible ());
Let
and
t be distinct object of type
u . The type
T models
T only if:
stoppable_token
All copies of a
reference the same logical shared stop state and shall report values consistent with each other.
stoppable_token If
evaluates to
t . stop_possible () false
then, if, references the same logical shared stop state,
u shall also subsequently evaluate to
u . stop_possible () false
andshall also subsequently evaluate to
u . stop_requested () false
.If
evaluates to
t . stop_requested () true
then, if, references the same logical shared stop state,
u shall also subsequently evaluate to
u . stop_requested () true
andshall also subsequently evaluate to
u . stop_possible () true
.Given a callback-type, CB, and a callback-initializer argument,
, of type
init then constructing an instance,
Initializer , of type
cb , passing
T :: callback_type < CB > as the first argument and
t as the second argument to the constructor, shall, if
init is
t . stop_possible () true
, construct an instance,, of type
callback , direct-initialized with
CB , and register callback with
init ’s shared stop state such that callback will be invoked with an empty argument list if a stop request is made on the shared stop state.
t
If
is
t . stop_requested () true
at the time callback is registered then callback may be invoked immediately inline inside the call to’s constructor.
cb If callback is invoked then, if
references the same shared stop state as
u , an evaluation of
t will be
u . stop_requested () true
if the beginning of the invocation of callback strongly-happens-before the evaluation of.
u . stop_requested () If
evaluates to
t . stop_possible () false
then the construction ofis not required to construct and initialize
cb .
callback Construction of a
instance shall only throw exceptions thrown by the initialization of the
T :: callback_type < CB > instance from the value of type
CB .
Initializer Destruction of the
object,
T :: callback_type < CB > , removes
cb from the shared stop state such that
callback will not be invoked after the destructor returns.
callback
If
is currently being invoked on another thread then the destructor of
callback will block until the invocation of
cb returns such that the return from the invocation of
callback strongly-happens-before the destruction of
callback .
callback Destruction of a callback
shall not block on the completion of the invocation of some other callback registered with the same shared stop state.
cb
9. Execution control library [execution]
-
This Clause describes components supporting execution of function objects [function.objects].
-
The following subclauses describe the requirements, concepts, and components for execution control primitives as summarized in Table 1.
Subclause | Header | |
[execution.execute] | One-way execution |
9.1. Header < execution >
synopsis [execution.syn]
namespace std :: execution { // [execution.helpers], helper concepts template < class T > concept movable - value = see - below ; // exposition only template < class From , class To > concept decays - to = same_as < decay_t < From > , To > ; // exposition only template < class T > concept class - type = decays - to < T , T > && is_class_v < T > ; // exposition only // [execution.schedulers], schedulers template < class S > concept scheduler = see - below ; // [execution.schedulers.queries], scheduler queries enum class forward_progress_guarantee ; inline namespace unspecified { struct get_forward_progress_guarantee_t ; inline constexpr get_forward_progress_guarantee_t get_forward_progress_guarantee {}; } } namespace std :: this_thread { inline namespace unspecified { struct execute_may_block_caller_t ; inline constexpr execute_may_block_caller_t execute_may_block_caller {}; } } namespace std :: execution { // [execution.receivers], receivers template < class T , class E = exception_ptr > concept receiver = see - below ; template < class T , class ... An > concept receiver_of = see - below ; inline namespace unspecified { struct set_value_t ; inline constexpr set_value_t set_value {}; struct set_error_t ; inline constexpr set_error_t set_error {}; struct set_done_t ; inline constexpr set_done_t set_done {}; } // [execution.receivers.queries], receiver queries inline namespace unspecified { struct get_scheduler_t ; inline constexpr get_scheduler_t get_scheduler {}; struct get_allocator_t ; inline constexpr get_allocator_t get_allocator {}; struct get_stop_token_t ; inline constexpr get_stop_token_t get_stop_token {}; } template using stop_token_of_t = remove_cvref_t ())) > ; // [execution.op_state], operation states template < class O > concept operation_state = see - below ; inline namespace unspecified { struct start_t ; inline constexpr start_t start {}; } // [execution.senders], senders template < class S > concept sender = see - below ; template < class S , class R > concept sender_to = see - below ; template < class S > concept has - sender - types = see - below ; // exposition only template < class S > concept typed_sender = see - below ; template < class ... Ts > struct type - list ; template < class S , class ... Ts > concept sender_of = see - below ; template < class S > using single - sender - value - type = see below ; // exposition only template < class S > concept single - typed - sender = see below ; // exposition only // [execution.senders.traits], sender traits inline namespace unspecified { struct sender_base {}; } template < class S > struct sender_traits ; template < class ... Ts > using variant - or - empty = see below ; // exposition only template < typed_sender S , template < class ... > class Tuple = tuple , template < class ... > class Variant = variant - or - empty > using value_types_of_t = typename sender_traits < remove_cvref_t < S >>:: template value_types < Tuple , Variant > ; template < typed_sender S , template < class ... > class Variant = variant - or - empty > using error_types_of_t = typename sender_traits < remove_cvref_t < S >>:: template error_types < Variant > ; inline namespace unspecified { // [execution.senders.connect], the connect sender algorithm struct connect_t ; inline constexpr connect_t connect {}; template < class S , class R > using connect_result_t = decltype ( connect ( declval < S > (), declval < R > ())); // [execution.senders.queries], sender queries template < class CPO > struct get_completion_scheduler_t ; template < class CPO > inline constexpr get_completion_scheduler_t < CPO > get_completion_scheduler {}; // [execution.senders.factories], sender factories struct schedule_t ; inline constexpr schedule_t schedule {}; template < movable - value ... Ts > unspecified just ( Ts && ...) noexcept ( see below ); template < movable - value Error > unspecified just_error ( Error && ) noexcept ( see below ); unspecified just_done () noexcept ; template < movable - value ... Ts > unspecified just ( Ts && ...); struct transfer_just_t ; inline constexpr transfer_just_t transfer_just {}; // [execution.senders.adaptors], sender adaptors template < class - type D > struct sender_adaptor_closure { }; struct on_t ; inline constexpr on_t on {}; struct transfer_t ; inline constexpr transfer_t transfer {}; struct schedule_from_t ; inline constexpr schedule_from_t schedule_from {}; struct then_t ; inline constexpr then_t then {}; struct upon_error_t ; inline constexpr upon_error_t upon_error {}; struct upon_done_t ; inline constexpr upon_done_t upon_done {}; struct let_value_t ; inline constexpr let_value_t let_value {}; struct let_error_t ; inline constexpr let_error_t let_error {}; struct let_done_t ; inline constexpr let_done_t let_done {}; struct bulk_t ; inline constexpr bulk_t bulk {}; struct split_t ; inline constexpr split_t split {}; struct when_all_t ; inline constexpr when_all_t when_all {}; struct when_all_with_variant_t ; inline constexpr when_all_with_variant_t when_all_with_variant {}; struct transfer_when_all_t ; inline constexpr transfer_when_all_t transfer_when_all {}; struct transfer_when_all_with_variant_t ; inline constexpr transfer_when_all_with_variant_t transfer_when_all_with_variant {}; template < typed_sender S > using into - variant - type = see - below ; // exposition-only template < typed_sender S > see - below into_variant ( S && ); struct done_as_optional_t ; inline constexpr done_as_optional_t done_as_optional ; template < move_constructible Error , sender S > see - below done_as_error ( S && , Error err = Error {}); // [execution.senders.consumers], sender consumers struct ensure_started_t ; inline constexpr ensure_started_t ensure_started {}; struct start_detached_t ; inline constexpr start_detached_t start_detached {}; } // [execution.snd_rec_utils], sender and receiver utilities template < class - type Derived , receiver Base = unspecified > using receiver_adaptor = unspecified ; // [execution.contexts], execution contexts class run_loop ; } namespace std :: this_thread { inline namespace unspecified { template < typed_sender S > using sync - wait - type = see - below ; // exposition-only template < typed_sender S > using sync - wait - with - variant - type = see - below ; // exposition-only struct sync_wait_t ; inline constexpr sync_wait_t sync_wait {}; struct sync_wait_with_variant_t ; inline constexpr sync_wait_with_variant_t sync_wait_with_variant {}; } } namespace std :: execution { inline namespace unspecified { // [execution.execute], one-way execution struct execute_t ; inline constexpr execute_t execute {}; } // [execution.coro_utils.as_awaitable] inline namespace unspecified { struct as_awaitable_t ; inline constexpr as_awaitable_t as_awaitable ; } // [execution.coro_utils.with_awaitable_senders] template < class - type Promise > struct with_awaitable_senders ; }
9.2. Helper concepts [execution.helpers]
template < class T > concept movable - value = // exposition only move_constructible < decay_t < T >> && constructible_from < decay_t < T > , T > ;
9.3. Schedulers [execution.schedulers]
-
The
concept defines the requirements of a type that allows for scheduling of work on its associated execution context.scheduler template < class S > concept scheduler = copy_constructible < remove_cvref_t < S >> && equality_comparable < remove_cvref_t < S >> && requires ( S && s , const get_completion_scheduler_t < set_value_t > tag ) { { execution :: schedule (( S && ) s ) } -> sender_of ; { tag_invoke ( tag , execution :: schedule (( S && ) s )) } -> same_as < remove_cvref_t < S >> ; }; -
None of a scheduler’s copy constructor, destructor, equality comparison, or
member functions shall exit via an exception.swap -
None of these member functions, nor a scheduler type’s
function, shall introduce data races as a result of concurrent invocations of those functions from different threads.schedule -
For any two (possibly const) values
ands1
of some scheduler types2
,S
shall returns1 == s2 true
only if both
ands1
are handles to the same associated execution context.s2 -
For a given scheduler expression
, the expressions
shall compare equal toexecution :: get_completion_scheduler < set_value_t > ( execution :: schedule ( s ))
.s -
A scheduler type’s destructor shall not block pending completion of any receivers connected to the sender objects returned from
. [Note: The ability to wait for completion of submitted function objects may be provided by the associated execution context of the scheduler. —end note]schedule
9.3.1. Scheduler queries [execution.schedulers.queries]
9.3.1.1. execution :: get_forward_progress_guarantee
[execution.schedulers.queries.get_forward_progress_guarantee]
enum class forward_progress_guarantee { concurrent , parallel , weakly_parallel };
-
is used to ask a scheduler about the forward progress guarantees of execution agents created by that scheduler.execution :: get_forward_progress_guarantee -
The name
denotes a customization point object. For some subexpressionexecution :: get_forward_progress_guarantee
, lets
beS
. Ifdecltype (( s ))
does not satisfyS
,execution :: scheduler
is ill-formed. Otherwise,execution :: get_forward_progress_guarantee
is expression equivalent to:execution :: get_forward_progress_guarantee ( s ) -
, if this expression is well formed and its type istag_invoke ( execution :: get_forward_progress_guarantee , as_const ( s ))
, and isexecution :: forward_progress_guarantee
.noexcept -
Otherwise,
.execution :: forward_progress_guarantee :: weakly_parallel
-
-
If
for some schedulerexecution :: get_forward_progress_guarantee ( s )
returnss
, all execution agents created by that scheduler shall provide the concurrent forward progress guarantee. If it returnsexecution :: forward_progress_guarantee :: concurrent
, all execution agents created by that scheduler shall provide at least the parallel forward progress guarantee.execution :: forward_progress_guarantee :: parallel
9.3.1.2. this_thread :: execute_may_block_caller
[execution.schedulers.queries.execute_may_block_caller
-
is used to ask a schedulerthis_thread :: execute_may_block_caller
whether a calls
with any invocableexecution :: execute ( s , f )
may block the thread where such a call occurs.f -
The name
denotes a customization point object. For some subexpressionthis_thread :: execute_may_block_caller
, lets
beS
. Ifdecltype (( s ))
does not satisfyS
,execution :: scheduler
is ill-formed. Otherwise,this_thread :: execute_may_block_caller
is expression equivalent to:this_thread :: execute_may_block_caller ( s ) -
, if this expression is well formed and its type istag_invoke ( this_thread :: execute_may_block_caller , as_const ( s ))
, and isbool
.noexcept -
Otherwise,
true
.
-
-
If
for some schedulerthis_thread :: execute_may_block_caller ( s )
returnss false
, no
call with some invocableexecution :: execute ( s , f )
shall block the calling thread.f
9.4. Receivers [execution.receivers]
-
A receiver represents the continuation of an asynchronous operation. An asynchronous operation may complete with a (possibly empty) set of values, an error, or it may be cancelled. A receiver has three principal operations corresponding to the three ways an asynchronous operation may complete:
,set_value
, andset_error
. These are collectively known as a receiver’s completion-signal operations.set_done -
The
concept defines the requirements for a receiver type with an unknown set of value types. Thereceiver
concept defines the requirements for a receiver type with a known set of value types, whose error type isreceiver_of
.std :: exception_ptr template < class T , class E = exception_ptr > concept receiver = move_constructible < remove_cvref_t < T >> && constructible_from < remove_cvref_t < T > , T > && requires ( remove_cvref_t < T >&& t , E && e ) { { execution :: set_done ( std :: move ( t )) } noexcept ; { execution :: set_error ( std :: move ( t ), ( E && ) e ) } noexcept ; }; template < class T , class ... An > concept receiver_of = receiver < T > && requires ( remove_cvref_t < T >&& t , An && ... an ) { execution :: set_value ( std :: move ( t ), ( An && ) an ...); }; -
The receiver’s completion-signal operations have semantic requirements that are collectively known as the receiver contract, described below:
-
None of a receiver’s completion-signal operations shall be invoked before
has been called on the operation state object that was returned byexecution :: start
to connect that receiver to a sender.execution :: connect -
Once
has been called on the operation state object, exactly one of the receiver’s completion-signal operations shall complete non-exceptionally before the receiver is destroyed.execution :: start -
If
exits with an exception, it is still valid to callexecution :: set_value
orexecution :: set_error
on the receiver, but it is no longer valid to callexecution :: set_done
on the receiver.execution :: set_value
-
-
Once one of a receiver’s completion-signal operations has completed non-exceptionally, the receiver contract has been satisfied.
9.4.1. execution :: set_value
[execution.receivers.set_value]
-
is used to send a value completion signal to a receiver.execution :: set_value -
The name
denotes a customization point object. The expressionexecution :: set_value
for some subexpressionsexecution :: set_value ( R , Vs ...)
andR
is expression-equivalent to:Vs ... -
, if that expression is valid. If the function selected bytag_invoke ( execution :: set_value , R , Vs ...)
does not send the value(s)tag_invoke
to the receiverVs ...
’s value channel, the program is ill-formed with no diagnostic required.R -
Otherwise,
is ill-formed.execution :: set_value ( R , Vs ...)
-
9.4.2. execution :: set_error
[execution.receivers.set_error]
-
is used to send a error signal to a receiver.execution :: set_error -
The name
denotes a customization point object. The expressionexecution :: set_error
for some subexpressionsexecution :: set_error ( R , E )
andR
is expression-equivalent to:E -
, if that expression is valid. If the function selected bytag_invoke ( execution :: set_error , R , E )
does not send the errortag_invoke
to the receiverE
’s error channel, the program is ill-formed with no diagnostic required.R -
Otherwise,
is ill-formed.execution :: set_error ( R , E )
-
9.4.3. execution :: set_done
[execution.receivers.set_done]
-
is used to send a done signal to a receiver.execution :: set_done -
The name
denotes a customization point object. The expressionexecution :: set_done
for some subexpressionexecution :: set_done ( R )
is expression-equivalent to:R -
, if that expression is valid. If the function selected bytag_invoke ( execution :: set_done , R )
does not signal the receivertag_invoke
’s done channel, the program is ill-formed with no diagnostic required.R -
Otherwise,
is ill-formed.execution :: set_done ( R )
-
9.4.4. Receiver queries [execution.receivers.queries]
9.4.4.1. execution :: get_scheduler
[execution.receivers.queries.get_scheduler]
-
is used to ask a receiver object for a suggested scheduler to be used by a sender it is connected to when it needs to launch additional work. [Note: the presence of this query on a receiver does not bind a sender to use its result. --end note]execution :: get_scheduler -
The name
denotes a customization point object. For some subexpressionexecution :: get_scheduler
, letr
beR
. Ifdecltype (( r ))
does not satisfyR
,execution :: receiver
is ill-formed. Otherwise,execution :: get_scheduler
is expression equivalent to:execution :: get_scheduler ( r ) -
, if this expression is well formed and satisfiestag_invoke ( execution :: get_scheduler , as_const ( r ))
, and isexecution :: scheduler
.noexcept -
Otherwise,
is ill-formed.execution :: get_scheduler ( r )
-
9.4.4.2. execution :: get_allocator
[execution.receivers.queries.get_allocator]
-
is used to ask a receiver object for a suggested allocator to be used by a sender it is connected to when it needs to allocate memory. [Note: the presence of this query on a receiver does not bind a sender to use its result. --end note]execution :: get_allocator -
The name
denotes a customization point object. For some subexpressionexecution :: get_allocator
, letr
beR
. Ifdecltype (( r ))
does not satisfyR
,execution :: receiver
is ill-formed. Otherwise,execution :: get_allocator
is expression equivalent to:execution :: get_allocator ( r ) -
, if this expression is well formed and models Allocator, and istag_invoke ( execution :: get_allocator , as_const ( r ))
.noexcept -
Otherwise,
is ill-formed.execution :: get_allocator ( r )
-
9.4.4.3. execution :: get_stop_token
[execution.receivers.queries.get_stop_token]
-
is used to ask a receiver object for an associated stop token of that receiver. A sender connected with that receiver can use this stop token to check whether a stop request has been made. [Note: such a stop token being signalled does not bind the sender to actually cancel any work. --end note]execution :: get_stop_token -
The name
denotes a customization point object. For some subexpressionexecution :: get_stop_token
, letr
beR
. Ifdecltype (( r ))
does not satisfyR
,execution :: receiver
is ill-formed. Otherwise,execution :: get_stop_token
is expression equivalent to:execution :: get_stop_token ( r ) -
, if this expression is well formed and satisfiestag_invoke ( execution :: get_stop_token , as_const ( r ))
, and isstoppable_token
.noexcept -
Otherwise,
.never_stop_token {}
-
-
Let
be a receiver,r
be a sender, ands
be an operation state resulting from anop_state
call. Letexecution :: connect ( s , r )
be a stop token resulting from antoken
call.execution :: get_stop_token ( r )
must remain valid at least until a call to a receiver completion-signal function oftoken
returns successfully. [Note: this means that, unless it knows about further guarantees provided by the receiverr
, the implementation ofr
should not useop_state
after it makes a call to a receiver completion-signal function oftoken
. This also implies that stop callbacks registered onr
by the implementation oftoken
orop_state
must be destroyed before such a call to a receiver completion-signal function ofs
. --end note]r
9.5. Operation states [execution.op_state]
-
The
concept defines the requirements for an operation state type, which allows for starting the execution of work.operation_state template < class O > concept operation_state = destructible < O > && is_object_v < O > && requires ( O & o ) { { execution :: start ( o ) } noexcept ; };
9.5.1. execution :: start
[execution.op_state.start]
-
is used to start work represented by an operation state object.execution :: start -
The name
denotes a customization point object. The expressionexecution :: start
for some lvalue subexpressionexecution :: start ( O )
is expression-equivalent to:O -
, if that expression is valid. If the function selected bytag_invoke ( execution :: start , O )
does not start the work represented by the operation statetag_invoke
, the program is ill-formed with no diagnostic required.O -
Otherwise,
is ill-formed.execution :: start ( O )
-
-
The caller of
must guarantee that the lifetime of the operation state objectexecution :: start ( O )
extends at least until one of the receiver completion-signal functions of a receiverO
passed into theR
call that producedexecution :: connect
is ready to successfully return. [Note: this allows for the receiver to manage the lifetime of the operation state object, if destroying it is the last operation it performs in its completion-signal functions. --end note]O
9.6. Senders [execution.senders]
-
A sender describes a potentially asynchronous operation. A sender’s responsibility is to fulfill the receiver contract of a connected receiver by delivering one of the receiver completion-signals.
-
The
concept defines the requirements for a sender type. Thesender
concept defines the requirements for a sender type capable of being connected with a specific receiver type.sender_to template < class S > concept sender = move_constructible < remove_cvref_t < S >> && ! requires { typename sender_traits < remove_cvref_t < S >>:: __unspecialized ; // exposition only }; template < class S , class R > concept sender_to = sender < S > && receiver < R > && requires ( S && s , R && r ) { execution :: connect (( S && ) s , ( R && ) r ); }; -
A sender is typed if it declares what types it sends through a connected receiver’s channels.
-
The
concept defines the requirements for a typed sender type.typed_sender template < class S > concept has - sender - types = // exposition only requires { typename has - value - types < S :: template value_types > ; typename has - error - types < S :: template error_types > ; typename bool_constant < S :: sends_done > ; }; template < class S > concept typed_sender = sender < S > && has - sender - types < sender_traits < remove_cvref_t < S >>> ; -
The
concept defines the requirements for a typed sender type that on successful completion sends the specified set of value types.sender_of template < class S , class ... Ts > concept sender_of = typed_sender < S > && same_as < type - list < Ts ... > , typename sender_traits < S >:: value_types < type - list , type_identity_t > > ;
9.6.1. Sender traits [execution.senders.traits]
-
The class
is used as a base class to tag sender types which do not expose member templatessender_base
,value_types
, and a static member constant expressionerror_types
.sends_done -
The class template
is used to query a sender type for facts associated with the signal it sends.sender_traits -
The primary class template
also recognizes awaitables as typed senders. For this clause ([execution]):sender_traits < S > -
An awaitable is an expression that would be well-formed as the operand of a
expression within a given context.co_await -
For any type
,T
isis - awaitable < T > true
if and only if an expression of that type is an awaitable as described above within the context of a coroutine whose promise type does not define a member
. For a coroutine promise typeawait_transform
,P
isis - awaitable < T , P > true
if and only if an expression of that type is an awaitable as described above within the context of a coroutine whose promise type is
.P -
For an awaitable
such thata
is typedecltype (( a ))
,A
is an alias forawait - result - type < A >
, wheredecltype ( e )
ise
's await-resume expression ([expr.await]) within the context of a coroutine whose promise type does not define a membera
. For a coroutine promise typeawait_transform
,P
is an alias forawait - result - type < A , P >
, wheredecltype ( e )
ise
's await-resume expression ([expr.await]) within the context of a coroutine whose promise type isa
.P
-
-
The primary class template
is defined as if inheriting from an implementation-defined class templatesender_traits < S >
defined as follows:sender - traits - base < S > -
If
ishas - sender - types < S > true
, then
is equivalent to:sender - traits - base < S > template < class S > struct sender - traits - base { template < template < class ... > class Tuple , template < class ... > class Variant > using value_types = typename S :: template value_types < Tuple , Variant > ; template < template < class ... > class Variant > using error_types = typename S :: template error_types < Variant > ; static constexpr bool sends_done = S :: sends_done ; }; -
Otherwise, if
isderived_from < S , sender_base > true
, then
is equivalent tosender - traits - base < S > template < class S > struct sender - traits - base {}; -
Otherwise, if
isis - awaitable < S > true
, then-
If
isawait - result - type < S >
thencv void
is equivalent tosender - traits - base < S > template < class S > struct sender - traits - base { template < template < class ... > class Tuple , template < class ... > class Variant > using value_types = Variant < Tuple <>> ; template < template < class ... > class Variant > using error_types = Variant < exception_ptr > ; static constexpr bool sends_done = false; }; -
Otherwise,
is equivalent tosender - traits - base < S > template < class S > struct sender - traits - base { template < template < class ... > class Tuple , template < class ... > class Variant > using value_types = Variant < Tuple < await - result - type < S >> ; template < template < class ... > class Variant > using error_types = Variant < exception_ptr > ; static constexpr bool sends_done = false; };
-
-
Otherwise,
is equivalent tosender - traits - base < S > template < class S > struct sender - traits - base { using __unspecialized = void ; // exposition only };
-
-
The exposition-only type
names the typevariant - or - empty < Ts ... >
ifvariant < Ts ... >
is greater than zero; otherwise, it names an implementation defined class type equivalent to the following:sizeof ...( Ts ) struct empty - variant { empty - variant () = delete ; }; -
If
for some sender typevalue_types_of_t < S , Tuple , Variant >
is well formed, it shall be a typeS
, where the type packsVariant < Tuple < Args 0 ... > , Tuple < Args 1 ... > , ..., Tuple < Args N ... >>>
throughArgs 0
are the packs of types the senderArgs N
passes as arguments toS
after a receiver object. If such senderexecution :: set_value
odr-uses ([basic.def.odr])S
for some receiverexecution :: set_value ( r , args ...)
, wherer
is not one of the type packsdecltype ( args )...
throughArgs 0 ...
, the program is ill-formed with no diagnostic required.Args N ... -
If
for some sender typeerror_types_of_t < S , Variant >
is well formed, it shall be a typeS
, where the typesVariant < E 0 , E 1 , ..., E N >
throughE 0
are the types the senderE N
passes as arguments toS
after a receiver object. If such senderexecution :: set_error
odr-usesS
for some receiverexecution :: set_error ( r , e )
, wherer
is not one of the typesdecltype ( e )
throughE 0
, the program is ill-formed with no diagnostic required.E N -
If
is well formed andsender_traits < S >:: sends_done false
, and such sender
odr-usesS
for some receiverexecution :: set_done ( r )
, the program is ill-formed with no diagnostic required.r -
Users may specialize
on program-defined types.sender_traits
9.6.2. execution :: connect
[execution.senders.connect]
-
is used to connect a sender with a receiver, producing an operation state object that represents the work that needs to be performed to satisfy the receiver contract of the receiver with values that are the result of the operations described by the sender.execution :: connect -
The name
denotes a customization point object. For some subexpressionsexecution :: connect
ands
, letr
beS
anddecltype (( s ))
beR
, and letdecltype (( r ))
andS '
be the decayed types ofR '
andS
, respectively. IfR
does not satisfyR
,execution :: receiver
is ill-formed. Otherwise, the expressionexecution :: connect ( s , r )
is expression-equivalent to:execution :: connect ( s , r ) -
, if that expression is valid, its type satisfiestag_invoke ( execution :: connect , s , r )
, andexecution :: operation_state
satisfiesS
. If the function selected byexecution :: sender
does not return an operation state for whichtag_invoke
starts work described byexecution :: start
, the program is ill-formed with no diagnostic required.s -
Otherwise,
ifconnect - awaitable ( s , r )
isis - awaitable < S , connect - awaitable - promise > true
and that expression is valid, where
is a coroutine equivalent to the following:connect - awaitable operation - state - task connect - awaitable ( S 's , R 'r ) requires see - below { exception_ptr ep ; try { set - value - expr } catch (...) { ep = current_exception (); } set - error - expr } where
is the promise typeconnect - awaitable - promise
, and whereconnect - awaitable
suspends at the initial suspends point ([dcl.fct.def.coroutine]), and:connect - awaitable -
set-value-expr first evaluates
, then suspends the coroutine and evaluatesco_await ( S && ) s
ifexecution :: set_value (( R && ) r )
isawait - result - type < S , connect - awaitable - promise >
; otherwise, it evaluatescv void
, then suspends the coroutine and evaluatesauto && res = co_await ( S && ) s
.execution :: set_value (( R && ) r , ( decltype ( res )) res ) If the call to
exits with an exception, the coroutine is resumed and the exception is immediately propagated in the context of the coroutine.execution :: set_value [Note: If the call to
exits normally, then theexecution :: set_value
coroutine is never resumed. --end note]connect - awaitable -
set-error-expr first suspends the coroutine and then executes
.execution :: set_error (( R && ) r , std :: move ( ep )) [Note: The
coroutine is never resumed after the call toconnect - awaitable
. --end note]execution :: set_error -
is a type that modelsoperation - state - task
. Itsoperation_state
resumes theexecution :: start
coroutine, advancing it past the initial suspend point.connect - awaitable -
The type
satisfiesconnect - awaitable - promise
. [Note: It need not modelreceiver
. -- end note].receiver -
Let
be an lvalue reference to the promise of thep
coroutine, letconnect - awaitable
be ab
lvalue reference to the receiverconst
, and letr
be any customization point object excluding those of typec
,set_value_t
andset_error_t
. Thenset_done_t
is expression-equivalent tostd :: tag_invoke ( c , p , as ...)
for any set of argumentsc ( b , as ...)
.as ... -
The expression
is expression-equivalent top . unhandled_done ()
.( execution :: set_done (( R && ) r ), noop_coroutine ()) -
For some expression
, the expressione
is expression-equivalent top . await_transform ( e )
if that expression is well-formed; otherwise, it is expression-equivalent totag_invoke ( as_awaitable , e , p )
.e
The operand of the requires-clause of
is equivalent toconnect - awaitable
ifreceiver_of < R >
isawait - result - type < S , connect - awaitable - promise >
; otherwise, it iscv void
.receiver_of < R , await - result - type < S , connect - awaitable - promise >> -
-
Otherwise,
is ill-formed.execution :: connect ( s , r )
-
-
Standard sender types shall always expose an rvalue-qualified overload of a customization of
. Standard sender types shall only expose an lvalue-qualified overload of a customization ofexecution :: connect
if they are copyable.execution :: connect
9.6.3. Sender queries [execution.senders.queries]
9.6.3.1. execution :: get_completion_scheduler
[execution.senders.queries.get_completion_scheduler]
-
is used to ask a sender object for the completion scheduler for one of its signals.execution :: get_completion_scheduler -
The name
denotes a customization point object template. For some subexpressionexecution :: get_completion_scheduler
, lets
beS
. Ifdecltype (( s ))
does not satisfyS
,execution :: sender
is ill-formed for all template argumentsexecution :: get_completion_scheduler < CPO > ( s )
. If the template argumentCPO
inCPO
is not one ofexecution :: get_completion_scheduler < CPO >
,execution :: set_value_t
, orexecution :: set_error_t
,execution :: set_done_t
is ill-formed. Otherwise,execution :: get_completion_scheduler < CPO >
is expression-equivalent to:execution :: get_completion_scheduler < CPO > ( s ) -
, if this expression is well formed and satisfiestag_invoke ( execution :: get_completion_scheduler < CPO > , as_const ( s ))
, and isexecution :: scheduler
.noexcept -
Otherwise,
is ill-formed.execution :: get_completion_scheduler < CPO > ( s )
-
-
If, for some sender
and customization point objects
,CPO
is well-formed and results in a schedulerexecution :: get_completion_scheduler < decltype ( CPO ) > ( s )
, and the sendersch
invokess
, for some receiverCPO ( r , args ...)
which has been connected tor
, with additional argumentss
, on an execution agent which does not belong to the associated execution context ofargs ...
, the behavior is undefined.sch
9.6.4. Sender factories [execution.senders.factories]
9.6.4.1. General [execution.senders.factories.general]
-
Subclause [execution.senders.factories] defines sender factories, which are utilities that return senders without accepting senders as arguments.
9.6.4.2. execution :: schedule
[execution.senders.schedule]
-
is used to obtain a sender associated with a scheduler, which can be used to describe work to be started on that scheduler’s associated execution context.execution :: schedule -
The name
denotes a customization point object. For some subexpressionexecution :: schedule
, the expressions
is expression-equivalent to:execution :: schedule ( s ) -
, if that expression is valid and its type satisfiestag_invoke ( execution :: schedule , s )
. If the function selected byexecution :: sender
does not return a sender whosetag_invoke
completion scheduler is equivalent toset_value
, the program is ill-formed with no diagnostic required.s -
Otherwise,
is ill-formed.execution :: schedule ( s )
-
9.6.4.3. execution :: just
[execution.senders.just]
-
is used to create a sender that propagates a set of values to a connected receiver.execution :: just template < class ... Ts > struct just - sender // exposition only { std :: tuple < Ts ... > vs_ ; template < template < class ... > class Tuple , template < class ... > class Variant > using value_types = Variant < Tuple < Ts ... >> ; template < template < class ... > class Variant > using error_types = Variant < exception_ptr > ; static const constexpr auto sends_done = false; template < class R > struct operation_state { std :: tuple < Ts ... > vs_ ; R r_ ; friend void tag_invoke ( execution :: start_t , operation_state & s ) noexcept { try { apply ([ & s ]( Ts & ... values_ ) { execution :: set_value ( std :: move ( s . r_ ), std :: move ( values_ )...); }, s . vs_ ); } catch (...) { execution :: set_error ( std :: move ( s . r_ ), current_exception ()); } } }; template < receiver R > requires receiver_of < R , Ts ... > && ( copy_constructible < Ts > && ...) friend auto tag_invoke ( execution :: connect_t , const just - sender & j , R && r ) { return operation_state < R > { j . vs_ , std :: forward < R > ( r ) }; } template < receiver R > requires receiver_of < R , Ts ... > friend auto tag_invoke ( execution :: connect_t , just - sender && j , R && r ) { return operation_state < R > { std :: move ( j . vs_ ), std :: forward < R > ( r ) }; } }; template < movable - value ... Ts > just - sender < decay_t < Ts > ... > just ( Ts && ... ts ) noexcept ( see - below ); -
Effects: Initializes
withvs_
.make_tuple ( forward < Ts > ( ts )...) -
Remarks: The expression in the
is equivalent tonoexcept - specifier ( is_nothrow_constructible_v < decay_t < Ts > , Ts > && ...)
9.6.4.4. execution :: transfer_just
[execution.senders.transfer_just]
-
is used to create a sender that propagates a set of values to a connected receiver on an execution agent belonging to the associated execution context of a specified scheduler.execution :: transfer_just -
The name
denotes a customization point object. For some subexpressionsexecution :: transfer_just
ands
, letvs ...
beS
anddecltype (( s ))
beVs ...
. Ifdecltype (( vs ))
does not satisfyS
, or any typeexecution :: scheduler
inV
does not satisfyVs
,movable - value
is ill-formed. Otherwise,execution :: transfer_just ( s , vs ...)
is expression-equivalent to:execution :: transfer_just ( s , vs ...) -
, if that expression is valid and its type satisfiestag_invoke ( execution :: transfer_just , s , vs ...)
. If the function selected byexecution :: typed_sender
does not return a sender whosetag_invoke
completion scheduler is equivalent toset_value
and sends values equivalent tos
to a receiver connected to it, the program is ill-formed with no diagnostic required.vs ... -
Otherwise,
.execution :: transfer ( execution :: just ( vs ...), s )
-
9.6.4.5. execution :: just_error
[execution.senders.just_error]
-
is used to create a sender that propagates an error to a connected receiver.execution :: just_error template < class T > struct just - error - sender // exposition only { T err_ ; template < template < class ... > class Tuple , template < class ... > class Variant > using value_types = Variant <> ; template < template < class ... > class Variant > using error_types = Variant < T > ; static const constexpr auto sends_done = false; template < class R > struct operation_state { T err_ ; R r_ ; friend void tag_invoke ( execution :: start_t , operation_state & s ) noexcept { execution :: set_error ( std :: move ( s . r_ ), std :: move ( err_ )); } }; template < receiver R > requires receiver < R , T > && copy_constructible < T > friend auto tag_invoke ( execution :: connect_t , const just - error - sender & j , R && r ) { return operation_state < remove_cvref_t < R >> { j . err_ , std :: forward < R > ( r ) }; } template < receiver R > requires receiver < R , T > friend auto tag_invoke ( execution :: connect_t , just - error - sender && j , R && r ) { return operation_state < remove_cvref_t < R >> { std :: move ( j . err_ ), std :: forward < R > ( r ) }; } }; template < movable - value T > just - error - sender < decay_t < T >> just_error ( T && t ) noexcept ( see - below ); -
Effects: Returns a
withjust - error - sender < decay_t < T >>
direct initialized witherr_
.static_cast < T &&> ( t ) -
Remarks: The expression in the
is equivalent tonoexcept - specifier is_nothrow_constructible_v < decay_t < T > , T >
9.6.4.6. execution :: just_done
[execution.senders.just_done]
-
is used to create a sender that propagates a done signal to a connected receiver.execution :: just_done struct just - done - sender // exposition only { template < template < class ... > class Tuple , template < class ... > class Variant > using value_types = Variant <> ; template < template < class ... > class Variant > using error_types = Variant <> ; static const constexpr auto sends_done = true; template < class R > struct operation_state { R r_ ; friend void tag_invoke ( execution :: start_t , operation_state & s ) noexcept { execution :: set_done ( std :: move ( s . r_ )); } }; template < receiver R > friend auto tag_invoke ( execution :: connect_t , const just - done - sender & j , R && r ) { return operation_state < R > { std :: forward < R > ( r ) }; } }; just - done - sender just_done () noexcept ; -
Effects: Equivalent to
.just - done - sender {}
9.6.5. Sender adaptors [execution.senders.adaptors]
9.6.5.1. General [execution.senders.adaptors.general]
-
Subclause [execution.senders.adaptors] defines sender adaptors, which are utilities that transform one or more senders into a sender with custom behaviors. When they accept a single sender argument, they can be chained to create sender chains.
-
The bitwise OR operator is overloaded for the purpose of creating sender chains. The adaptors also support function call syntax with equivalent semantics.
-
Unless otherwise specified, a sender adaptor is required to not begin executing any functions which would observe or modify any of the arguments of the adaptor before the returned sender is connected with a receiver using
, andexecution :: connect
is called on the resulting operation state. This requirement applies to any function that is selected by the implementation of the sender adaptor.execution :: start -
Unless otherwise specified, all sender adaptors which accept a single
argument return sender objects that propagate sender queries to that single sender argument. This requirement applies to any function that is selected by the implementation of the sender adaptor.sender -
Unless otherwise specified, whenever a sender adaptor constructs a receiver it passes to another sender’s connect, that receiver shall propagate receiver queries to a receiver accepted as an argument of
. This requirements applies to any sender returned from a function that is selected by the implementation of such sender adaptor.execution :: connect
9.6.5.2. Sender adaptor closure objects [execution.senders.adaptor.objects]
-
A pipeable sender adaptor closure object is a function object that accepts one or more
arguments and returns asender
. For a sender adaptor closure objectsender
and an expressionC
such thatS
modelsdecltype (( S ))
, the following expressions are equivalent and yield asender
:sender C ( S ) S | C Given an additional pipeable sender adaptor closure object
, the expressionD
produces another pipeable sender adaptor closure objectC | D
:E
is a perfect forwarding call wrapper ([func.require]) with the following properties:E -
Its target object is an object
of typed
direct-non-list-initialized withdecay_t < decltype (( D )) >
.D -
It has one bound argument entity, an object
of typec
direct-non-list-initialized withdecay_t < decltype (( C )) >
.C -
Its call pattern is
, whered ( c ( arg ))
is the argument used in a function call expression ofarg
.E
The expression
is well-formed if and only if the initializations of the state entities ofC | D
are all well-formed.E -
-
An object
of typet
is a pipeable sender adaptor closure object ifT
modelsT
,derived_from < sender_adaptor_closure < T >>
has no other base classes of typeT
for any other typesender_adaptor_closure < U >
, andU
does not modelT
.sender -
The template parameter
forD
may be an incomplete type. Before any expression of typesender_adaptor_closure
appears as an operand to thecv D
operator,|
shall be complete and modelD
. The behavior of an expression involving an object of typederived_from < sender_adaptor_closure < D >>
as an operand to thecv D
operator is undefined if overload resolution selects a program-defined|
function.operator | -
A pipeable sender adaptor object is a customization point object that accepts a
as its first argument and returns asender
.sender -
If a pipeable sender adaptor object accepts only one argument, then it is a pipeable sender adaptor closure object.
-
If a pipeable sender adaptor object
accepts more than one argument, then letadaptor
be an expression such thats
modelsdecltype (( s ))
, letsender
be arguments such thatargs ...
is a well-formed expression as specified in the rest of this subclause ([execution.senders.adaptor.objects]), and letadaptor ( s , args ...)
be a pack that denotesBoundArgs
. The expressiondecay_t < decltype (( args )) > ...
produces a pipeable sender adaptor closure objectadaptor ( args ...)
that is a perfect forwarding call wrapper with the following properties:f -
Its target object is a copy of
.adaptor -
Its bound argument entities
consist of objects of typesbound_args
direct-non-list-initialized withBoundArgs ...
, respectively.std :: forward < decltype (( args )) > ( args )... -
Its call pattern is
, whereadaptor ( r , bound_args ...)
is the argument used in a function call expression ofr
.f
-
The expression
is well-formed if and only if the initializations of the bound argument entities of the result, as specified above,
are all well-formed.
9.6.5.3. execution :: on
[execution.senders.adaptors.on]
-
is used to adapt a sender in a sender that will start the input sender on an execution agent belonging to a specific execution context.execution :: on -
The name
denotes a customization point object. For some subexpressionsexecution :: on
andsch
, lets
beSch
anddecltype (( sch ))
beS
. Ifdecltype (( s ))
does not satisfySch
, orexecution :: scheduler
does not satisfyS
,execution :: sender
is ill-formed. Otherwise, the expressionexecution :: on
is expression-equivalent to:execution :: on ( sch , s ) -
, if that expression is valid and its type satisfiestag_invoke ( execution :: on , sch , s )
. If the function selected above does not return a sender which startsexecution :: sender
on an execution agent of the associated execution context ofs
when started, the program is ill-formed with no diagnostic required.sch -
Otherwise, constructs a sender
. Whens1
is connected with some receivers1
, it:out_r -
Constructs a receiver
:r -
When
is called, it callsexecution :: set_value ( r )
, whereexecution :: connect ( s , r2 )
is as specified below, which results inr2
. It callsop_state3
. If any of these throws an exception, it callsexecution :: start ( op_state3 )
onexecution :: set_error
, passingout_r
as the second argument.current_exception () -
When
is called, it callsexecution :: set_error ( r , e )
.execution :: set_error ( out_r , e ) -
When
is called, it callsexecution :: set_done ( r )
.execution :: set_done ( out_r )
-
-
Calls
, which results inexecution :: schedule ( sch )
. It then callss2
, resulting inexecution :: connect ( s2 , r )
.op_state2 -
is wrapped by a new operation state,op_state2
, that is returned to the caller.op_state1
-
-
is a receiver that wraps a reference tor2
. It forwards all receiver completion signals and receiver queries toout_r
. Additionally, it implements theout_r
receiver query. The scheduler returned from the query is equivalent to theget_scheduler
argument that was passed tosch
.execution :: on -
When
is called onexecution :: start
, it callsop_state1
onexecution :: start
.op_state2 -
The lifetime of
, once constructed, lasts until eitherop_state2
is constructed orop_state3
is destroyed, whichever comes first. The lifetime ofop_state1
, once constructed, lasts untilop_state3
is destroyed.op_state1
-
9.6.5.4. execution :: transfer
[execution.senders.adaptors.transfer]
-
is used to adapt a sender into a sender with a different associatedexecution :: transfer
completion scheduler. [Note: it results in a transition between different execution contexts when executed. --end note]set_value -
The name
denotes a customization point object. For some subexpressionsexecution :: transfer
andsch
, lets
beSch
anddecltype (( sch ))
beS
. Ifdecltype (( s ))
does not satisfySch
, orexecution :: scheduler
does not satisfyS
,execution :: sender
is ill-formed. Otherwise, the expressionexecution :: transfer
is expression-equivalent to:execution :: transfer ( s , sch ) -
, if that expression is valid and its type satisfiestag_invoke ( execution :: transfer , get_completion_scheduler < set_value_t > ( s ), s , sch )
.execution :: sender -
Otherwise,
, if that expression is valid and its type satisfiestag_invoke ( execution :: transfer , s , sch )
.execution :: sender -
Otherwise,
.schedule_from ( sch , s )
If the function selected above does not return a sender which is a result of a call to
, whereexecution :: schedule_from ( sch , s2 )
is a sender which sends equivalent to those sent bys2
, the program is ill-formed with no diagnostic required.s -
-
Senders returned from
shall not propagate the sender queriesexecution :: transfer
to an input sender. They shall return a scheduler equivalent to theget_completion_scheduler < CPO >
argument from those queries.sch
9.6.5.5. execution :: schedule_from
[execution.senders.adaptors.schedule_from]
-
is used to schedule work dependent on the completion of a sender onto a scheduler’s associated execution context. [Note:execution :: schedule_from
is not meant to be used in user code; they are used in the implementation ofschedule_from
. -end note]transfer -
The name
denotes a customization point object. For some subexpressionsexecution :: schedule_from
andsch
, lets
beSch
anddecltype (( sch ))
beS
. Ifdecltype (( s ))
does not satisfySch
, orexecution :: scheduler
does not satisfyS
,execution :: typed_sender
is ill-formed. Otherwise, the expressionexecution :: schedule_from
is expression-equivalent to:execution :: schedule_from ( sch , s ) -
, if that expression is valid and its type satisfiestag_invoke ( execution :: schedule_from , sch , s )
. If the function selected byexecution :: sender
does not return a sender which completes on an execution agent belonging to the associated execution context oftag_invoke
and sends signals equivalent to those sent bysch
, the program is ill-formed with no diagnostic required.s -
Otherwise, constructs a sender
. Whens2
is connected with some receivers2
, it:out_r -
Constructs a receiver
.r -
Calls
, which results in an operation stateexecution :: connect ( s , r )
.op_state2 -
When a receiver completion-signal
is called, it constructs a receiverSignal ( r , args ...)
:r2 -
When
is called, it callsexecution :: set_value ( r2 )
.Signal ( out_r , args ...) -
When
is called, it callsexecution :: set_error ( r2 , e )
.execution :: set_error ( out_r , e ) -
When
is called, it callsexecution :: done ( r2 )
.execution :: set_done ( out_r )
It then calls
, resulting in a senderexecution :: schedule ( sch )
. It then callss3
, resulting in an operation stateexecution :: connect ( s3 , r2 )
. It then callsop_state3
. If any of these throws an exception, it catches it and callsexecution :: start ( op_state3 )
.execution :: set_error ( out_r , current_exception ()) -
-
Returns an operation state
that containsop_state
. Whenop_state2
is called, callsexecution :: start ( op_state )
. The lifetime ofexecution :: start ( op_state2 )
ends whenop_state3
is destroyed.op_state
-
-
-
Senders returned from
shall not propagate the sender queriesexecution :: schedule_from
to an input sender. They shall return a scheduler equivalent to theget_completion_scheduler < CPO >
argument from those queries.sch
9.6.5.6. execution :: then
[execution.senders.adaptors.then]
-
is used to attach invocables as continuation for successful completion of the input sender.execution :: then -
The name
denotes a customization point object. For some subexpressionsexecution :: then
ands
, letf
beS
. Ifdecltype (( s ))
does not satisfyS
,execution :: sender
is ill-formed. Otherwise, the expressionexecution :: then
is expression-equivalent to:execution :: then ( s , f ) -
, if that expression is valid and its type satisfiestag_invoke ( execution :: then , get_completion_scheduler < set_value_t > ( s ), s , f )
.execution :: sender -
Otherwise,
, if that expression is valid and its type satisfiestag_invoke ( execution :: then , s , f )
.execution :: sender -
Otherwise, constructs a sender
. Whens2
is connected with some receivers2
, it:out_r -
Constructs a receiver
:r -
When
is called, callsexecution :: set_value ( r , args ...)
and passes the resultinvoke ( f , args ...)
tov
. If any of these throws an exception, it catches it and callsexecution :: set_value ( out_r , v )
.execution :: set_error ( out_r , current_exception ()) -
When
is called, callsexecution :: set_error ( r , e )
.execution :: set_error ( out_r , e ) -
When
is called, callsexecution :: set_done ( r )
.execution :: set_done ( out_r )
-
-
Calls
, which results in an operation stateexecution :: connect ( s , r )
.op_state2 -
Returns an operation state
that containsop_state
. Whenop_state2
is called, callsexecution :: start ( op_state )
.execution :: start ( op_state2 )
-
If the function selected above does not return a sender which invokes
with the result of thef
signal ofset_value
, passing the return value as the value to any connected receivers, and propagates the other completion-signals sent bys
, the program is ill-formed with no diagnostic required.s -
9.6.5.7. execution :: upon_error
[execution.senders.adaptors.upon_error]
-
is used to attach invocables as continuation for unsuccessul completion of the input sender.execution :: upon_error -
The name
denotes a customization point object. For some subexpressionsexecution :: upon_error
ands
, letf
beS
. Ifdecltype (( s ))
does not satisfyS
,execution :: sender
is ill-formed. Otherwise, the expressionexecution :: upon_error
is expression-equivalent to:execution :: upon_error ( s , f ) -
, if that expression is valid and its type satisfiestag_invoke ( execution :: upon_error , get_completion_scheduler < set_error_t > ( s ), s , f )
.execution :: sender -
Otherwise,
, if that expression is valid and its type satisfiestag_invoke ( execution :: upon_error , s , f )
.execution :: sender -
Otherwise, constructs a sender
. Whens2
is connected with some receivers2
, it:out_r -
Constructs a receiver
:r -
When
is called, callsexecution :: set_value ( r , args ...)
.execution :: set_value ( out_r , args ...) -
When
is called, callsexecution :: set_error ( r , e )
and passes the resultinvoke ( f , e )
tov
. If any of these throws an exception, it catches it and callsexecution :: set_value ( out_r , v )
.execution :: set_error ( out_r , current_exception ()) -
When
is called, callsexecution :: set_done ( r )
.execution :: set_done ( out_r )
-
-
Calls
, which results in an operation stateexecution :: connect ( s , r )
.op_state2 -
Returns an operation state
that containsop_state
. Whenop_state2
is called, callsexecution :: start ( op_state )
.execution :: start ( op_state2 )
-
If the function selected above does not return a sender which invokes
with the result of thef
signal ofset_error
, passing the return value as the value to any connected receivers, and propagates the other completion-signals sent bys
, the program is ill-formed with no diagnostic required.s -
9.6.5.8. execution :: upon_done
[execution.senders.adaptors.upon_done]
-
is used to attach invocables as continuation for the completion of the input sender using the "done" channel.execution :: upon_done -
The name
denotes a customization point object. For some subexpressionsexecution :: upon_done
ands
, letf
beS
. Ifdecltype (( s ))
does not satisfyS
,execution :: sender
is ill-formed. Otherwise, the expressionexecution :: upon_done
is expression-equivalent to:execution :: upon_done ( s , f ) -
, if that expression is valid and its type satisfiestag_invoke ( execution :: upon_done , get_completion_scheduler < set_done_t > ( s ), s , f )
.execution :: sender -
Otherwise,
, if that expression is valid and its type satisfiestag_invoke ( execution :: upon_done , s , f )
.execution :: sender -
Otherwise, constructs a sender
. Whens2
is connected with some receivers2
, it:out_r -
Constructs a receiver
:r -
When
is called, callsexecution :: set_value ( r , args ...)
.execution :: set_value ( out_r , args ...) -
When
is called, callsexecution :: set_error ( r , e )
.execution :: set_error ( out_r , e ) -
When
is called, callsexecution :: set_done ( r )
and passes the resultinvoke ( f )
tov
. If any of these throws an exception, it catches it and callsexecution :: set_value ( out_r , v )
.execution :: set_error ( out_r , current_exception ())
-
-
Calls
, which results in an operation stateexecution :: connect ( s , r )
.op_state2 -
Returns an operation state
that containsop_state
. Whenop_state2
is called, callsexecution :: start ( op_state )
.execution :: start ( op_state2 )
-
If the function selected above does not return a sender which invokes
when thef
signal ofset_done
is called, passing the return value as the value to any connected receivers, and propagates the other completion-signals sent bys
, the program is ill-formed with no diagnostic required.s -
9.6.5.9. execution :: let_value
[execution.senders.adaptors.let_value]
-
is used to insert continuations creating more work dependent on the results of their input senders into a sender chain.execution :: let_value -
The name
denotes a customization point object. For some subexpressionsexecution :: let_value
ands
, letf
beS
. Ifdecltype (( s ))
does not satisfyS
,execution :: sender
is ill-formed. Otherwise, the expressionexecution :: let_value
is expression-equivalent to:execution :: let_value ( s , f ) -
, if that expression is valid and its type satisfiestag_invoke ( execution :: let_value , get_completion_scheduler < set_value_t > ( s ), s , f )
.execution :: sender -
Otherwise,
, if that expression is valid and its type satisfiestag_invoke ( execution :: let_value , s , f )
.execution :: sender -
Otherwise, constructs a sender
. Whens2
is connected with some receivers2
, it:out_r -
Constructs a receiver
.r -
When
is called, decay-copiesexecution :: set_value ( r , args ...)
intoargs ...
asop_state2
, then callsargs2 ...
, resulting in a senderinvoke ( f , args2 ...)
. It then callss3
, resulting in an operation stateexecution :: connect ( s3 , out_r )
.op_state3
is saved as a part ofop_state3
. It then callsop_state2
. If any of these throws an exception, it catches it and callsexecution :: start ( op_state3 )
.execution :: set_error ( out_r , current_exception ()) -
When
is called, callsexecution :: set_error ( r , e )
.execution :: set_error ( out_r , e ) -
When
is called, callsexecution :: set_done ( r )
.execution :: set_done ( out_r )
-
-
Calls
, which results in an operation stateexecution :: connect ( s , r )
.op_state2 -
Returns an operation state
that containsop_state
. Whenop_state2
is called, callsexecution :: start ( op_state )
.execution :: start ( op_state2 )
-
If the function selected above does not return a sender which invokes
whenf
is called, and making its completion dependent on the completion of a sender returned byset_value
, and propagates the other completion-signals sent byf
, the program is ill-formed with no diagnostic required.s -
9.6.5.10. execution :: let_error
[execution.senders.adaptors.let_error]
-
is used to insert continuations creating more work dependent on the results of their input senders into a sender chain.execution :: let_error -
The name
denotes a customization point object. For some subexpressionsexecution :: let_error
ands
, letf
beS
. Ifdecltype (( s ))
does not satisfyS
,execution :: sender
is ill-formed. Otherwise, the expressionexecution :: let_error
is expression-equivalent to:execution :: let_error ( s , f ) -
, if that expression is valid and its type satisfiestag_invoke ( execution :: let_error , get_completion_scheduler < set_error_t > ( s ), s , f )
.execution :: sender -
Otherwise,
, if that expression is valid and its type satisfiestag_invoke ( execution :: let_error , s , f )
.execution :: sender -
Otherwise, constructs a sender
. Whens2
is connected with some receivers2
, it:out_r -
Constructs a receiver
.r -
When
is called, callsexecution :: set_value ( r , args ...)
.execution :: set_value ( out_r , args ...) -
When
is called, decay-copiesexecution :: set_error ( r , e )
intoe
asop_state2
, then callse2
, resulting in a senderinvoke ( f , e2 )
. It then callss3
, resulting in an operation stateexecution :: connect ( s3 , out_r )
.op_state3
is saved as a part ofop_state3
. It then callsop_state2
. If any of these throws an exception, it catches it and callsexecution :: start ( op_state3 )
.execution :: set_error ( out_r , current_exception ()) -
When
is called, callsexecution :: set_done ( r )
.execution :: set_done ( out_r )
-
-
Calls
, which results in an operation stateexecution :: connect ( s , r )
.op_state2 -
Returns an operation state
that containsop_state
. Whenop_state2
is called, callsexecution :: start ( op_state )
.execution :: start ( op_state2 )
-
If the function selected above does not return a sender which invokes
whenf
is called, and making its completion dependent on the completion of a sender returned byset_error
, and propagates the other completion-signals sent byf
, the program is ill-formed with no diagnostic required.s -
9.6.5.11. execution :: let_done
[execution.senders.adaptors.let_done]
-
is used to insert continuations creating more work dependent on the results of their input senders into a sender chain.execution :: let_done -
The name
denotes a customization point object. For some subexpressionsexecution :: let_done
ands
, letf
beS
. Ifdecltype (( s ))
does not satisfyS
,execution :: sender
is ill-formed. Otherwise, the expressionexecution :: let_done
is expression-equivalent to:execution :: let_done ( s , f ) -
, if that expression is valid and its type satisfiestag_invoke ( execution :: let_done , get_completion_scheduler < set_done_t > ( s ), s , f )
.execution :: sender -
Otherwise,
, if that expression is valid and its type satisfiestag_invoke ( execution :: let_done , s , f )
.execution :: sender -
Otherwise, constructs a sender
. Whens2
is connected with some receivers2
, it:out_r -
Constructs a receiver
.r -
When
is called, callsexecution :: set_value ( r , args ...)
.execution :: set_value ( out_r , args ...) -
When
is called, callsexecution :: set_error ( r , e )
.execution :: set_error ( out_r , e ) -
When
is called, callsexecution :: set_done ( r )
, resulting in a senderinvoke ( f )
. It then callss3
, resulting in an operation stateexecution :: connect ( s3 , out_r )
.op_state3
is saved as a part ofop_state3
. It then callsop_state2
. If any of these throws an exception, it catches it and callsexecution :: start ( op_state3 )
.execution :: set_error ( out_r , current_exception ())
-
-
Calls
. which results in an operation stateexecution :: connect ( s , r )
.op_state2 -
Returns an operation state
that containsop_state
. Whenop_state2
is called, callsexecution :: start ( op_state )
.execution :: start ( op_state2 )
-
If the function selected above does not return a sender which invokes
whenf
is called, and making its completion dependent on the completion of a sender returned byset_done
, and propagates the other completion-signals sent byf
, the program is ill-formed with no diagnostic required.s -
9.6.5.12. execution :: bulk
[execution.senders.adaptors.bulk]
-
is used to run a task repeatedly for every index in an index space.execution :: bulk -
The name
denotes a customization point object. For some subexpressionsexecution :: bulk
,s
, andshape
, letf
beS
,decltype (( s ))
beShape
, anddecltype (( shape ))
beF
. Ifdecltype (( f ))
does not satisfyS
orexecution :: sender
does not satisfyShape
,integral
is ill-formed. Otherwise, the expressionexecution :: bulk
is expression-equivalent to:execution :: bulk ( s , shape , f ) -
, if that expression is valid and its type satisfiestag_invoke ( execution :: bulk , get_completion_scheduler < set_value_t > ( s ), s , shape , f )
.execution :: sender -
Otherwise,
, if that expression is valid and its type satisfiestag_invoke ( execution :: bulk , s , shape , f )
.execution :: sender -
Otherwise, constructs a sender
. Whens2
is connected with some receivers2
, it:out_r -
Constructs a receiver
:r -
When
is called, callsexecution :: set_value ( r , args ...)
for eachf ( i , args ...)
of typei
fromShape
to0
, then callsshape
. If any of these throws an exception, it catches it and callsexecution :: set_value ( out_r , args ...)
.execution :: set_error ( out_r , current_exception ()) -
When
is called, callsexecution :: set_error ( r , e )
.execution :: set_error ( out_r , e ) -
When
is called, callsexecution :: set_done ( r )
.execution :: set_done ( out_r , e )
-
-
Calls
, which results in an operation stateexecution :: connect ( s , r )
.op_state2 -
Returns an operation state
that containsop_state
. Whenop_state2
is called, callsexecution :: start ( op_state )
.execution :: start ( op_state2 )
-
If the function selected above does not return a sender which invokes
for eachf ( i , args ...)
of typei
fromShape
to0
when the input sender sends valuesshape
, or does not propagate the values of the signals sent by the input sender to a connected receiver, the program is ill-formed with no diagnostic required.args ... -
9.6.5.13. execution :: split
[execution.senders.adaptors.split]
-
is used to adapt an arbitrary sender into a sender that can be connected multiple times.execution :: split -
The name
denotes a customization point object. For some subexpressionexecution :: split
, lets
beS
. Ifdecltype (( s ))
does not satisfyS
,execution :: typed_sender
is ill-formed. Otherwise, the expressionexecution :: split
is expression-equivalent to:execution :: split ( s ) -
, if that expression is valid and its type satisfiestag_invoke ( execution :: split , get_completion_scheduler < set_value_t > ( s ), s )
.execution :: sender -
Otherwise,
, if that expression is valid and its type satisfiestag_invoke ( execution :: split , s )
.execution :: sender -
Otherwise, constructs a sender
, which:s2 -
Creates an object
. The lifetime ofsh_state
shall last for at least as long as the lifetime of the last operation state object returned fromsh_state
for some receiverexecution :: connect ( s , some_r )
.some_r -
Constructs a receiver
:r -
When
is called, saves the expressionsexecution :: set_value ( r , args ...)
as subobjects ofargs ...
.sh_state -
When
is called, saves the expressionexecution :: set_error ( r , e )
as a subobject ofe
.sh_state -
When
is called, saves this fact inexecution :: set_done ( r )
.sh_state
-
-
Calls
, resulting in an operation stateexecution :: connect ( s , r )
.op_state2
is saved as a subobject ofop_state2
.sh_state -
When
is connected with a receivers2
, it returns an operation state objectout_r
. Whenop_state
is called, it callsexecution :: start ( op_state )
, if this is the first time this expression would be evaluated. When bothexecution :: start ( op_state2 )
andexecution :: start ( op_state )
have been called, callsSignal ( r , args ...)
, whereSignal ( out_r , args2 ...)
is a pack of lvalues referencing the subobjects ofargs2 ...
that have been saved by the original call tosh_state
.Signal ( r , args ...)
-
If the function selected above does not return a sender which sends references to values sent by
, propagating the other channels, the program is ill-formed with no diagnostic required.s -
9.6.5.14. execution :: when_all
[execution.senders.adaptors.when_all]
-
is used to join multiple sender chains and create a sender whose execution is dependent on all of the input senders that only send a single set of values.execution :: when_all
is used to join multiple sender chains and create a sender whose execution is dependent on all of the input senders, each of which may have one or more sets of sent values.execution :: when_all_with_variant -
The name
denotes a customization point object. For some subexpressionsexecution :: when_all
, lets i ...
beS i ...
. The expressiondecltype (( s i ))...
is ill-formed if any of the following is true:execution :: when_all ( s i ...) -
If the number of subexpressions
is 0, ors i ... -
If any type
does not satisfyS i
, orexecution :: typed_sender -
If for any type
, the typeS i
is ill-formed, wherevalue_types_of_t < S i , tuple , zero - or - one >
is a template alias equivalent to the following:zero - or - one template < class ... Ts > requires ( sizeof ...( Ts ) <= 1 ) using zero - or - one = void ;
Otherwise, the expression
is expression-equivalent to:execution :: when_all ( s i ...) -
, if that expression is valid and its type satisfiestag_invoke ( execution :: when_all , s i ...)
. If the function selected byexecution :: sender
does not return a sender that sends a concatenation of values sent bytag_invoke
when they all complete withs i ...
, the program is ill-formed with no diagnostic required.set_value -
Otherwise, constructs a sender
of typew
. WhenW
is connected with some receiverw
of typeout_r
, it returns an operation stateOutR
specified as below:op_state -
For each sender
, constructs a receivers i
such that:r i -
If
is called for everyexecution :: set_value ( r i , t i ...)
,r i
's associated stop callback optional is reset andop_state
is called, whereexecution :: set_value ( out_r , t 0 ..., t 1 ..., ..., t n -1 . ..)
the number of subexpressions inn
.s i ... -
Otherwise,
orexecution :: set_error
was called for at least one receiverexecution :: set_done
. If the first such to complete did so with the callr i
,execution :: set_error ( r i , e )
is called onrequest_stop
's associated stop source. When all child operations have completed,op_state
's associated stop callback optional is reset andop_state
is called.execution :: set_error ( out_r , e ) -
Otherwise,
is called onrequest_stop
's associated stop source. When all child operations have completed,op_state
's associated stop callback optional is reset andop_state
is called.execution :: set_done ( out_r ) -
For each receiver
,r i
is well-formed and returns the results of callingexecution :: get_stop_token ( r i )
onget_token ()
's associated stop source.op_state
-
-
For each sender
, callss i
, resulting in operation statesexecution :: connect ( s i , r i )
.child_op i -
Returns an operation state
that contains:op_state -
Each operation state
,child_op i -
A stop source of type
,in_place_stop_source -
A stop callback of type
, whereoptional < stop_token_of_t < OutR &>:: callback_type < stop - callback - fn >>
is an implementation defined class type equivalent to the following:stop - callback - fn struct stop - callback - fn { in_place_stop_source & stop_src_ ; void operator ()() noexcept { stop_src_ . request_stop (); } };
-
-
When
is called it:execution :: start ( op_state ) -
Emplace constructs the stop callback optional with the arguments
andexecution :: get_stop_token ( out_r )
, wherestop - callback - fn { stop - src }
refers to the stop source ofstop - src
.op_state -
Then, it checks to see if
is true. If so, it callsstop - src . stop_requested ()
.execution :: set_done ( out_r ) -
Otherwise, calls
for eachexecution :: start ( child_op i )
.child_op i
-
-
The associated types of the sender
are as follows:W -
is:value_types_of_t < W , Tuple , Variant > -
if for any typeVariant <>
, the typeS i
isvalue_types_of_t < S i , Tuple , Variant >
.Variant <> -
Otherwise,
whereVariant < Tuple < V 0 ..., V 1 ,..., V n -1 . .. >>
is the count of types inn
, and whereS i ...
is a set of types such that for the typeV i ...
,S i
is an alias forvalue_types_of_t < S i , Tuple , Variant >
.Variant < Tuple < V i ... >>
-
-
iserror_types_of_t < W , Variant >
, whereVariant < exception_ptr , U i ... >
is the unique set of types inU i ...
, whereE 0 ..., E 1 ,..., E n -1 . ..
is a set of types such that for the typeE i ...
,S i
is an alias forerror_types_of_t < S i , Variant >
.Variant < E i ... > -
issender_traits < W >:: sends_done true
.
-
-
-
-
The name
denotes a customization point object. For some subexpressionsexecution :: when_all_with_variant
, lets ...
beS
. If any typedecltype (( s ))
inS i
does not satisfyS ...
,execution :: typed_sender
is ill-formed. Otherwise, the expressionexecution :: when_all_with_variant
is expression-equivalent to:execution :: when_all_with_variant ( s ...) -
, if that expression is valid and its type satisfiestag_invoke ( execution :: when_all_with_variant , s ...)
. If the function selected byexecution :: sender
does not return a sender which sends the typestag_invoke
when they all complete withinto - variant - type < S > ...
, the program is ill-formed with no diagnostic required.set_value -
Otherwise,
.execution :: when_all ( execution :: into_variant ( s )...)
-
-
Senders returned from adaptors defined in this subclause shall not expose the sender queries
.get_completion_scheduler < CPO >
9.6.5.15. execution :: transfer_when_all
[execution.senders.adaptors.transfer_when_all]
-
is used to join multiple sender chains and create a sender whose execution is dependent on all of the input senders that only send a single set of values each, while also making sure that they complete on the specified scheduler.execution :: transfer_when_all
is used to join multiple sender chains and create a sender whose execution is dependent on all of the input senders, which may have one or more sets of sent values. [Note: this can allow for better customization of the adaptors. --end note]execution :: transfer_when_all_with_variant -
The name
denotes a customization point object. For some subexpressionsexecution :: transfer_when_all
andsch
, lets ...
beSch
anddecltype ( sch )
beS
. Ifdecltype (( s ))
does not satisfySch
, or any typescheduler
inS i
does not satisfyS ...
, or the number of the argumentsexecution :: typed_sender
passes into thesender_traits < S i >:: value_types
template parameter is not 1,Variant
is ill-formed. Otherwise, the expressionexecution :: transfer_when_all
is expression-equivalent to:execution :: transfer_when_all ( sch , s ...) -
, if that expression is valid and its type satisfiestag_invoke ( execution :: transfer_when_all , sch , s ...)
. If the function selected byexecution :: sender
does not return a sender which sends a concatenation of values sent bytag_invoke
when they all complete withs ...
, or does not send its completion signals, other than ones resulting from a scheduling error, on an execution agent belonging to the associated execution context ofset_value
, the program is ill-formed with no diagnostic required.sch -
Otherwise,
.transfer ( when_all ( s ...), sch )
-
-
The name
denotes a customization point object. For some subexpressionsexecution :: transfer_when_all_with_variant
, lets ...
beS
. If any typedecltype (( s ))
inS i
does not satisfyS ...
,execution :: typed_sender
is ill-formed. Otherwise, the expressionexecution :: transfer_when_all_with_variant
is expression-equivalent to:execution :: transfer_when_all_with_variant ( s ...) -
, if that expression is valid and its type satisfiestag_invoke ( execution :: transfer_when_all_with_variant , s ...)
. If the function selected byexecution :: sender
does not return a sender which sends the typestag_invoke
when they all complete withinto - variant - type < S > ...
, the program is ill-formed with no diagnostic required.set_value -
Otherwise,
.execution :: transfer_when_all ( sch , execution :: into_variant ( s )...)
-
-
Senders returned from
shall not propagate the sender queriesexecution :: transfer_when_all
to input senders. They shall return a scheduler equivalent to theget_completion_scheduler < CPO >
argument from those queries.sch
9.6.5.16. execution :: into_variant
[execution.senders.adaptors.into_variant]
-
can be used to turn a typed sender which sends multiple sets of values into a sender which sends a variant of all of those sets of values.execution :: into_variant -
The template
is used to compute the type sent by a sender returned frominto - variant - type
.execution :: into_variant template < typed_sender S > using into - with - variant - type = value_types_of_t < S > ; template < typed_sender S > see - below into_variant ( S && s ); -
Effects: Returns a sender
. Whens2
is connected with some receivers2
, it:out_r -
Constructs a receiver
:r -
If
is called, callsexecution :: set_value ( r , ts ...)
.execution :: set_value ( out_r , into - variant - type < S > ( make_tuple ( ts ...))) -
If
is called, callsexecution :: set_error ( r , e )
.execution :: set_error ( out_r , e ) -
If
is called, callsexecution :: set_done ( r )
.execution :: set_done ( out_r )
-
-
Calls
, resulting in an operation stateexecution :: connect ( s , r )
.op_state2 -
Returns an operation state
that containsop_state
. Whenop_state2
is called, callsexecution :: start ( op_state )
.execution :: start ( op_state2 )
-
9.6.5.17. execution :: done_as_optional
[execution.senders.adaptors.done_as_optional]
-
is used to handle a done signal by mapping it into the value channel as an empty optional. The value channel is also converted into an optional. The result is a sender that never completes with done, reporting cancellation by completing with an empty optional.execution :: done_as_optional -
The name
denotes a customization point object. For some subexpressionexecution :: done_as_optional
, lets .
beS
. If the typedecltype (( s ))
does not satisfyS
,single - typed - sender
is ill-formed. Otherwise, the expressionexecution :: done_as_optional ( s )
is expression-equivalent to:execution :: done_as_optional ( s ) execution :: let_done ( execution :: then ( s , []( auto && t ) { return optional < decay_t < single - sender - value - type < S >>> { static_cast < decltype ( t ) > ( t ) }; } ), [] () noexcept { return execution :: just ( optional < decay_t < single - sender - value - type < S >>> {}); } )
9.6.5.18. execution :: done_as_error
[execution.senders.adaptors.done_as_error]
-
is used to handle a done signal by mapping it into the error channel as anexecution :: done_as_error
that refers to a custom exception type. The result is a sender that never completes with done, reporting cancellation by completing with an error.exception_ptr -
The template
is used to compute the type sent by a sender returned frominto - variant - type
.execution :: into_variant template < move_constructible Error , sender S > see - below done_as_error ( S && s , Error err = Error {}); -
Effects: Equivalent to:
return execution :: let_done ( static_cast < S &&> ( s ), [ err2 = std :: move ( err )] () mutable { return execution :: just_error ( std :: move ( err2 )); } )
9.6.5.19. execution :: ensure_started
[execution.senders.adaptors.ensure_started]
-
is used to eagerly start the execution of a sender, while also providing a way to attach further work to execute once it has completed.execution :: ensure_started -
The name
denotes a customization point object. For some subexpressionexecution :: ensure_started
, lets
beS
. Ifdecltype (( s ))
does not satisfyS
,execution :: typed_sender
is ill-formed. Otherwise, the expressionexecution :: ensure_started
is expression-equivalent to:execution :: ensure_started ( s ) -
, if that expression is valid and its type satisfiestag_invoke ( execution :: ensure_started , get_completion_scheduler < set_value_t > ( s ), s )
.execution :: sender -
Otherwise,
, if that expression is valid and its type satisfiestag_invoke ( execution :: ensure_started , s )
.execution :: sender -
Otherwise:
-
Constructs a receiver
.r -
Calls
, resulting in operation stateexecution :: connect ( s , r )
, and then callsop_state
.execution :: start ( op_state ) -
Constructs a sender
. Whens2
is connected with some receivers2
, it results in an operation stateout_r
. Once bothop_state2
and one of the receiver completion-signals has been called onexecution :: start ( op_state2 )
:r -
If
has been called, callsexecution :: set_value ( r , ts ...)
.execution :: set_value ( out_r , ts ...) -
If
has been called, callsexecution :: set_error ( r , e )
.execution :: set_error ( out_r , e ) -
If
has been called, callsexecution :: set_done ( r )
.execution :: set_done ( out_r )
The lifetime of
lasts until all three of the following have occured:op_state -
the lifetime of
has ended,op_state2 -
the lifetime of
has ended, ands2 -
a receiver completion-signal has been called on
.r
-
-
If the function selected above does not eagerly start the sender
and return a sender which propagates the signals sent bys
once started, the program is ill-formed with no diagnostic required.s -
Note: The wording for
is incomplete as it does not currently describe the required
semantics for sending a stop-request to the eagerly-launched operation if the sender is destroyed and detaches
from the operation before the operation completes.
9.6.6. Sender consumers [execution.senders.consumers]
9.6.6.1. execution :: start_detached
[execution.senders.consumer.start_detached]
-
is used to eagerly start a sender without the caller needing to manage the lifetimes of any objects.execution :: start_detached -
The name
denotes a customization point object. For some subexpressionexecution :: start_detached
, lets
beS
. Ifdecltype (( s ))
does not satisfyS
,execution :: sender
is ill-formed. Otherwise, the expressionexecution :: start_detached
is expression-equivalent to:execution :: start_detached ( s ) -
, if that expression is valid and its type istag_invoke ( execution :: start_detached , execution :: get_completion_scheduler < execution :: set_value_t > ( s ), s )
.void -
Otherwise,
, if that expression is valid and its type istag_invoke ( execution :: start_detached , s )
.void -
Otherwise:
-
Constructs a receiver
:r -
When
is called, it does nothing.set_value ( r , ts ...) -
When
is called, it callsset_error ( r , e )
.std :: terminate -
When
is called, it does nothing.set_done ( r )
-
-
Calls
, resulting in an operation stateexecution :: connect ( s , r )
, then callsop_state
. The lifetime ofexecution :: start ( op_state )
lasts until one of the receiver completion-signals ofop_state
is called.r
-
If the function selected above does not eagerly start the sender
after connecting it with a receiver which ignores thes
andset_value
signals and callsset_done
on thestd :: terminate
signal, the program is ill-formed with no diagnostic required.set_error -
9.6.6.2. this_thread :: sync_wait
[execution.senders.consumers.sync_wait]
-
andthis_thread :: sync_wait
are used to block a current thread until a sender passed into it as an argument has completed, and to obtain the values (if any) it completed with.this_thread :: sync_wait_with_variant -
The templates
andsync - wait - type
are used to determine the return types ofsync - wait - with - variant - type
andthis_thread :: sync_wait
.this_thread :: sync_wait_with_variant template < typed_sender S > using sync - wait - type = optional < execution :: value_types_of_t < S , tuple , type_identity_t >> ; template < typed_sender S > using sync - wait - with - variant - type = optional < into - variant - type < S >> ; -
The name
denotes a customization point object. For some subexpressionthis_thread :: sync_wait
, lets
beS
. Ifdecltype (( s ))
does not satisfyS
, or the number of the argumentsexecution :: typed_sender
passes into thesender_traits < S >:: value_types
template parameter is not 1,Variant
is ill-formed. Otherwise,this_thread :: sync_wait
is expression-equivalent to:this_thread :: sync_wait -
, if this expression is valid and its type istag_invoke ( this_thread :: sync_wait , execution :: get_completion_scheduler < execution :: set_value_t > ( s ), s )
.sync - wait - type < S > -
Otherwise,
, if this expression is valid and its type istag_invoke ( this_thread :: sync_wait , s )
.sync - wait - type < S > -
Otherwise:
-
Constructs a receiver
.r -
Calls
, resulting in an operation stateexecution :: connect ( s , r )
, then callsop_state
.execution :: start ( op_state ) -
Blocks the current thread until a receiver completion-signal of
is called. When it is:r -
If
has been called, returnsexecution :: set_value ( r , ts ...)
.sync - wait - type < S > ( make_tuple ( ts ...)) > -
If
has been called, ifexecution :: set_error ( r , e ...)
isremove_cvref_t ( decltype ( e ))
, callsexception_ptr
. Otherwise, throwsstd :: rethrow_exception ( e )
.e -
If
has been called, returnsexecution :: set_done ( r )
.sync - wait - type < S ( nullopt ) >
-
-
-
-
The name
denotes a customization point object. For some subexpressionthis_thread :: sync_wait_with_variant
, lets
beS
. Ifdecltype (( s ))
does not satisfyS
,execution :: typed_sender
is ill-formed. Otherwise,this_thread :: sync_wait_with_variant
is expression-equivalent to:this_thread :: sync_wait_with_variant -
, if this expression is valid and its type istag_invoke ( this_thread :: sync_wait_with_variant , execution :: get_completion_scheduler < execution :: set_value_t > ( s ), s )
.sync - wait - with - variant - type < S > -
Otherwise,
, if this expression is valid and its type istag_invoke ( this_thread :: sync_wait_with_variant , s )
.sync - wait - with - variant - type < S > -
Otherwise,
.this_thread :: sync_wait ( execution :: into_variant ( s ))
-
-
Any receiver
created by an implementation ofr
andsync_wait
shall implement thesync_wait_with_variant
receiver query. The scheduler returned from the query for the receiver created by the default implementation shall return an implementation-defined scheduler that is driven by the waiting thread, such that scheduled tasks run on the thread of the caller. [Note: The scheduler for a local instance ofget_scheduler
is one valid implementation. -- end note]execution :: run_loop
9.7. execution :: execute
[execution.execute]
-
is used to create fire-and-forget tasks on a specified scheduler.execution :: execute -
The name
denotes a customization point object. For some subexpressionsexecution :: execute
andsch
, letf
beSch
anddecltype (( sch ))
beF
. Ifdecltype (( f ))
does not satisfySch
orexecution :: scheduler
does not satisfyF
,invocable <>
is ill-formed. Otherwise,execution :: execute
is expression-equivalent to:execution :: execute -
, if that expression is valid and its type istag_invoke ( execution :: execute , sch , f )
. If the function selected byvoid
does not invoke the functiontag_invoke
on an execution agent belonging to the associated execution context off
, or if it does not callsch
if an error occurs after control is returned to the caller, the program is ill-formed with no diagnostic required.std :: terminate -
Otherwise,
.execution :: start_detached ( execution :: then ( execution :: schedule ( sch ), f ))
-
9.8. Sender/receiver utilities [execution.snd_rec_utils]
-
This section makes use of the following exposition-only entities:
template < class T , class ... Us > concept none - of = (( ! same_as < T , Us > ) && ...); // [ Editorial note: copy_cvref_t as in [[P1450R3]] -- end note ] // Mandates: is_base_of_v<T, remove_reference_t<U>> is true template < class T , class U > copy_cvref_t < U && , T > c - style - cast ( U && u ) noexcept requires decays - to < T , T > { return ( copy_cvref_t < U && , T > ) static_cast < U &&> ( u ); } -
[Note: The C-style cast in c-style-cast is to disable accessibility checks. -- end note]
9.8.1. execution :: receiver_adaptor
[execution.snd_rec_utils.receiver_adaptor]
template < class - type Derived , receiver Base = unspecified > using receiver_adaptor = see below ;
-
is used to simplify the implementation of one receiver type in terms of another. It definesreceiver_adaptor
overloads that forward to named members if they exist, and to the adapted receiver otherwise.tag_invoke -
This section makes use of the following exposition-only entities:
template < class Receiver , class ... As > concept has - set - value = requires ( Receiver && r , As && ... as ) { static_cast < Receiver &&> ( r ). set_value ( static_cast < As &&> ( as ...); }; template < class Receiver , class A > concept has - set - error = requires ( Receiver && r , A && a ) { static_cast < Receiver &&> ( r ). set_error ( static_cast < A &&> ( a ); }; template < class Receiver > concept has - set - done = requires ( Receiver && r ) { static_cast < Receiver &&> ( r ). set_done (); }; -
If
is an alias for the unspecified default template parameter, then:Base -
Let
beHAS - BASE false
, and -
Let
beGET - BASE ( d )
.c - style - cast < receiver - adaptor > ( d )
otherwise, let:
-
Let
beHAS - BASE true
, and -
Let
beGET - BASE ( d )
.d . base ()
Let
be the type ofBASE - TYPE ( D )
.GET - BASE ( declval < D > ()) -
-
is an alias for a non-template class type equivalent to the following:receiver_adaptor < Derived , Base > class receiver - adaptor { // exposition only friend Derived ; public : // Constructors receiver - adaptor () = default ; template < class B > requires HAS - BASE && constructible_from < Base , B > explicit receiver - adaptor ( B && base ) : base_ ( static_cast < B &&> ( base )) {} private : using set_value = unspecified ; using set_error = unspecified ; using set_done = unspecified ; // Member functions Base & base () & noexcept requires HAS - BASE { return base_ ; } const Base & base () const & noexcept requires HAS - BASE { return base_ ; } Base && base () && noexcept requires HAS - BASE { return static_cast < Base &&> ( base_ ); } // [execution.snd_rec_utils.receiver_adaptor.nonmembers] Non-member functions template < class D = Derived , class ... As > friend void tag_invoke ( set_value_t , Derived && self , As && ... as ) noexcept ( see below ); template < class E , class D = Derived > friend void tag_invoke ( set_error_t , Derived && self , E && e ) noexcept ; template < class D = Derived > friend void tag_invoke ( set_done_t , Derived && self ) noexcept ; template < none - of < set_value_t , set_error_t , set_done_t > Tag , class D = Derived , class ... As > requires invocable < Tag , BASE - TYPE ( const D & ), As ... > friend auto tag_invoke ( Tag tag , const Derived & self , As && ... as ) noexcept ( is_nothrow_invocable_v < Tag , BASE - TYPE ( const D & ), As ... > ) -> invoke_result_t < Tag , BASE - TYPE ( const D & ), As ... > { return static_cast < Tag &&> ( tag )( GET - BASE ( self ), static_cast < As &&> ( as )...); } [[ no_unique_address ]] Base base_ ; // present if and only if HAS-BASE is true }; -
[Example:
template < execution :: receiver_of < int > R > class my_receiver : execution :: receiver_adaptor < my_receiver < R > , R > { friend execution :: receiver_adaptor < my_receiver , R > ; void set_value () && noexcept { execution :: set_value ( std :: move ( * this ). base (), 42 ); } public : using execution :: receiver_adaptor < my_receiver , R >:: receiver_adaptor ; }; -- end example]
9.8.1.1. Non-member functions [execution.snd_rec_utils.receiver_adaptor.nonmembers]
template < class D = Derived , class ... As > friend void tag_invoke ( set_value_t , Derived && self , As && ... as ) noexcept ( see below );
-
Constraints: Either
ishas - set - value < D , As ... > true
or
isrequires { typename D :: set_value ;} && receiver_of < BASE - TYPE ( D ), As ... > true
. -
If
ishas - set - value < D , As ... > true
:-
Effects: Equivalent to
static_cast < Derived &&> ( self ). set_value ( static_cast < As &&> ( as )...) -
Remarks: The exception specification is equivalent to
.noexcept ( static_cast < Derived &&> ( self ). set_value ( static_cast < As &&> ( as )...))
-
-
Otherwise:
-
Effects: Equivalent to
execution :: set_value ( GET - BASE ( static_cast < Derived &&> ( self )), static_cast < As &&> ( as )...) -
Remarks: The exception specification is equivalent to
noexcept ( set_value ( GET - BASE ( static_cast < Derived &&> ( self )), static_cast < As &&> ( as )...))
-
template < class E , class D = Derived > friend void tag_invoke ( set_error_t , Derived && self , E && e ) noexcept ;
-
Constraints: Either
ishas - set - error < D , E > true
or
isrequires { typename D :: set_error ;} && receiver < BASE - TYPE ( D ), E > true
. -
Effects: Equivalent to:
-
ifstatic_cast < Derived &&> ( self ). set_error ( static_cast < E &&> ( e ))
ishas - set - error < D , E > true
, -
Otherwise,
execution :: set_error ( GET - BASE ( static_cast < Derived &&> ( self )), static_cast < E &&> ( e ))
-
template < class D = Derived > friend void tag_invoke ( set_done_t , Derived && self ) noexcept ;
-
Constraints: Either
ishas - set - done < D > true
or
isrequires { typename D :: set_done ;} true
. -
Effects: Equivalent to:
-
ifstatic_cast < Derived &&> ( self ). set_done ()
ishas - set - done < D > true
, -
Otherwise,
execution :: set_done ( GET - BASE ( static_cast < Derived &&> ( self )))
-
9.9. Execution contexts [execution.contexts]
-
This section specifies some execution contexts on which work can be scheduled.
9.9.1. run_loop
[execution.contexts.run_loop]
-
A
is an execution context on which work can be scheduled. It maintains a simple, thread-safe first-in-first-out queue of work. Itsrun_loop
member function removes elements from the queue and executes them in a loop on whatever thread of execution callsrun ()
.run () -
A
instance has an associated count that corresponds to the number of work items that are in its queue. Additionally, arun_loop
has an associated state that can be one of starting, running, or finishing.run_loop -
Concurrent invocations of the member functions of
, other thanrun_loop
and its destructor, do not introduce data races. The member functionsrun
,pop_front
, andpush_back
execute atomically.finish -
[Note: Implementations are encouraged to use an intrusive queue of operation states to hold the work units to make scheduling allocation-free. — end note]
class run_loop { // [execution.contexts.run_loop.types] Associated types class run - loop - scheduler ; // exposition only class run - loop - sender ; // exposition only struct run - loop - opstate - base { // exposition only virtual void execute () = 0 ; run_loop * loop_ ; run - loop - opstate - base * next_ ; }; template < receiver_of R > using run - loop - opstate = unspecified ; // exposition only // [execution.contexts.run_loop.members] Member functions: run - loop - opstate - base * pop_front (); // exposition only void push_back ( run - loop - opstate - base * ); // exposition only public : // [execution.contexts.run_loop.ctor] construct/copy/destroy run_loop () noexcept ; run_loop ( run_loop && ) = delete ; ~ run_loop (); // [execution.contexts.run_loop.members] Member functions: run - loop - scheduler get_scheduler (); void run (); void finish (); };
9.9.1.1. Associated types [execution.contexts.run_loop.types]
class run - loop - scheduler ;
-
is an implementation defined type that models therun - loop - scheduler
concept.scheduler -
Instances of
remain valid until the end of the lifetime of therun - loop - scheduler
instance from which they were obtained.run_loop -
Two instances of
compare equal if and only if they were obtained from the samerun - loop - scheduler
instance.run_loop -
Let
be an expression of typesch
. The expressionrun - loop - scheduler
is not potentially throwing and has typeexecution :: schedule ( sch )
.run - loop - sender
class run - loop - sender ;
-
is an implementation defined type that models therun - loop - sender
concept; i.e.,sender_of
issender_of < run - loop - sender > true
. Additionally, the types reported by its
associated type iserror_types
, and the value of itsexception_ptr
trait issends_done true
. -
An instance of
remains valid until the end of the lifetime of its associatedrun - loop - sender
instance.execution :: run_loop -
Let
be an expression of types
, letrun - loop - sender
be an expression such thatr
models thedecltype ( r )
concept, and letreceiver_of
be one ofC
,set_value_t
, orset_error_t
. Then:set_done_t -
The expression
has typeexecution :: connect ( s , r )
and is potentially throwing if and only if the initialiation ofrun - loop - opstate < decay_t < decltype ( r ) >>
fromdecay_t < decltype ( r ) >
is potentially throwing.r -
The expression
is not potentially throwing, has typeget_completion_scheduler < C > ( s )
, and compares equal to therun - loop - scheduler
instance from whichrun - loop - scheduler
was obtained.s
-
template < receiver_of R > using run - loop - opstate = unspecified ;
-
is an alias for an unspecified non-template class type that inherits unambiguously fromrun - loop - opstate < R >
.run - loop - opstate - base -
Let
be a non-o
lvalue of typeconst
, and letrun - loop - opstate < R >
be a non-REC ( o )
lvalue reference to an instance of typeconst
that was initialized with the expressionR
passed to the invocation ofr
that returnedexecution :: connect
. Then:o -
The object to which
refers remains valid for the lifetime of the object to whichREC ( o )
refers.o -
The type
overridesrun - loop - opstate < R >
such thatrun - loop - opstate - base :: execute ()
is equivalent to the following:o . execute () try { if ( execution :: get_stop_token ( REC ( o )). stop_requested ()) { execution :: set_done ( std :: move ( REC ( o ))); } else { execution :: set_value ( std :: move ( REC ( o ))); } } catch (...) { execution :: set_error ( std :: move ( REC ( o )), current_exception ()); } -
The expression
is equivalent to the following:execution :: start ( o ) try { o . loop_ -> push_back ( & o ); } catch (...) { execution :: set_error ( std :: move ( REC ( o )), current_exception ()); }
-
9.9.1.2. Constructor and destructor[execution.contexts.run_loop.ctor]
run_loop :: run_loop () noexcept ;
-
Postconditions: count is
and state is starting.0
run_loop ::~ run_loop ();
-
Effects: If count is not
or if state is running, invokes0
. Otherwise, has no effects.terminate ()
9.9.1.3. Member functions [execution.contexts.run_loop.members]
run - loop - opstate - base * run_loop :: pop_front ();
-
Effects: Blocks ([defns.block]) until one of the following conditions is
true
:-
count is
and state is finishing, in which case0
returnspop_front
; ornullptr -
count is greater than
, in which case an item is removed from the front of the queue, count is decremented by0
, and the removed item is returned.1
-
void run_loop :: push_back ( run - loop - opstate - base * item );
-
Effects: Adds
to the back of the queue and increments count byitem
.1 -
Synchronization: This operation synchronizes with the
operation that obtainspop_front
.item
run - loop - scheduler run_loop :: get_scheduler ();
-
Returns: an instance of
that can be used to schedule work onto thisrun - loop - scheduler
instance.run_loop
void run_loop :: run ();
-
Effects: Equivalent to:
while ( auto * op = pop_front ()) { op -> execute (); } -
Precondition: state is starting.
-
Postcondition: state is finishing.
-
Remarks: While the loop is executing, state is running. When state changes, it does so without introducing data races.
void run_loop :: finish ();
-
Effects: Changes state to finishing.
-
Synchronization: This operation synchronizes with all
operations on this object.pop_front
9.10. Coroutine utilities [execution.coro_utils]
9.10.1. execution :: as_awaitable
[execution.coro_utils.as_awaitable]
-
is used to transform an object into one that is awaitable within a particular coroutine. This section makes use of the following exposition-only entities:as_awaitable template < class S > using single - sender - value - type = see below ; template < class S > concept single - typed - sender = typed_sender < S > && requires { typename single - sender - value - type < S > ; }; template < class S , class P > concept awaitable - sender = single - typed - sender < S > && sender_to < S , awaitable - receiver > && // see below requires ( P & p ) { { p . unhandled_done () } -> convertible_to < coroutine_handle <>> ; }; template < class S > using sender - awaitable = see below ; -
Alias template single-sender-value-type is defined as follows:
-
If
would have the formvalue_types_of_t < S , Tuple , Variant >
, thenVariant < Tuple < T >>
is an alias for typesingle - sender - value - type < S >
.T -
Otherwise, if
would have the formvalue_types_of_t < S , Tuple , Variant >
orVariant < Tuple <>>
, thenVariant <>
is an alias for typesingle - sender - value - type < S >
.void -
Otherwise,
is ill-formed.single - sender - value - type < S >
-
-
The type
names an unspecified non-template class type equivalent to the following:sender - awaitable < S > class sender - awaitable - impl { struct unit {}; using value_t = single - sender - value - type < S > ; using result_t = conditional_t < is_void_v < value_t > , unit , value_t > ; struct awaitable - receiver ; variant < monostate , result_t , exception_ptr > result_ {}; connect_result_t < S , awaitable - receiver > state_ ; public : sender - awaitable - impl ( S && s , P & p ); bool await_ready () const noexcept { return false; } void await_suspend ( coro :: coroutine_handle < P > ) noexcept { start ( state_ ); } value_t await_resume (); }; -
is an implementation-defined non-template class type equivalent to the following:awaitable - receiver struct awaitable - receiver { variant < monostate , result_t , exception_ptr >* result_ptr_ ; coroutine_handle < P > continuation_ ; // ... see below }; Let
be an rvalue expression of typer
, letawaitable - receiver
be acr
lvalue that refers toconst
, letr
be an expression of typev
, letresult_t
be an arbitrary expression of typeerr
, letErr
be a customization point object, and letc
be a pack of arguments. Then:as ... -
If
isvalue_t
, thenvoid
is expression-equivalent toexecution :: set_value ( r )
; otherwise,( result_ptr_ -> emplace < 1 > (), continuation_ . resume ())
is expression-equivalent toexecution :: set_value ( r , v )
.( result_ptr_ -> emplace < 1 > ( v ), continuation_ . resume ()) -
is expression-equivalent toexecution :: set_error ( r , e )
, where( result_ptr_ -> emplace < 2 > ( AS_EXCEPT_PTR ( err )), continuation_ . resume ())
is:AS_EXCEPT_PTR ( err ) -
iferr
names the same type asdecay_t < Err >
,exception_ptr -
Otherwise,
ifmake_exception_ptr ( system_error ( err ))
names the same type asdecay_t < Err >
,error_code -
Otherwise,
.make_exception_ptr ( err )
-
-
is expression-equivalent toexecution :: set_done ( r )
.continuation_ . promise (). unhandled_done (). resume () -
is expression-equivalent totag_invoke ( c , cr , as ...)
when the type ofc ( as_const ( p ), as ...)
is not one ofc
,execution :: set_value_t
, orexecution :: set_error_t
.execution :: set_done_t
-
-
sender - awaitable - impl :: sender - awaitable - impl ( S && s , P & p ) -
Effects: initializes
withstate_
.connect (( S && ) s , awaitable - receiver { & result_ , coroutine_handle < P >:: from_promise ( p )})
-
-
value_t sender - awaitable - impl :: await_resume () -
Effects: equivalent to:
if ( result_ . index ()) == 2 ) rethrow_exception ( std :: get < 2 > ( result_ )); if constexpr ( ! is_void_v < value_t > ) return static_cast < value_t &&> ( std :: get < 1 > ( result_ ));
-
-
-
-
is a customization point object. For some subexpressionsas_awaitable
ande
wherep
is an lvalue,p
names the typeE
anddecltype (( e ))
names the typeP
,decltype (( p ))
is expression-equivalent to the following:as_awaitable ( e , p ) -
if that expression is well-formed andtag_invoke ( as_awaitable , e , p )
isis - awaitable < tag_invoke_result_t < as_awaitable_t , E , P > , decay_t < P >> true
. -
Otherwise,
ife
isis - awaitable < E > true
. -
Otherwise,
ifsender - awaitable { e , p }
isawaitable - sender < E , P > true
. -
Otherwise,
.e
-
9.10.2. execution :: with_awaitable_senders
[execution.coro_utils.with_awaitable_senders]
-
, when used as the base class of a coroutine promise type, makes senders awaitable in that coroutine type.with_awaitable_senders In addition, it provides a default implementation of
such that if a sender completes by callingunhandled_done ()
, it is treated as if an uncatchable "done" exception were thrown from the await-expression. In practice, the coroutine is never resumed, and theexecution :: set_done
of the coroutine caller’s promise type is called.unhandled_done template < class Promise > requires is_class_v < Promise > && same_as < Promise , remove_cvref_t < Promise >> struct with_awaitable_senders { template < OtherPromise > requires ( ! same_as < OtherPromise , void > ) void set_continuation ( coroutine_handle < OtherPromise > h ) noexcept ; coroutine_handle <> continuation () const noexcept { return continuation_ ; } coroutine_handle <> unhandled_done () noexcept { return done_handler_ ( continuation_ . address ()); } template < class Value > see - below await_transform ( Value && value ); private : // exposition only [[ noreturn ]] static coroutine_handle <> default_unhandled_done ( void * ) noexcept { terminate (); } coroutine_handle <> continuation_ {}; // exposition only // exposition only coroutine_handle <> ( * done_handler_ )( void * ) noexcept = & default_unhandled_done ; }; -
void set_continuation ( coroutine_handle < OtherPromise > h ) noexcept -
Effects: equivalent to:
continuation_ = h ; if constexpr ( requires ( OtherPromise & other ) { other . unhandled_done (); } ) { done_handler_ = []( void * p ) noexcept -> coroutine_handle <> { return coroutine_handle < OtherPromise >:: from_address ( p ) . promise (). unhandled_done (); }; } else { done_handler_ = default_unhandled_done ; }
-
-
decltype ( auto ) await_transform ( Value && value ) -
Effects: equivalent to:
return as_awaitable ( static_cast < Value &&> ( value ), static_cast < Promise &> ( * this ));
-