diff --git a/.github/workflows/_test_common.sh b/.github/workflows/_test_common.sh
deleted file mode 100755
index 90ceba4..0000000
--- a/.github/workflows/_test_common.sh
+++ /dev/null
@@ -1,53 +0,0 @@
-#!/bin/bash
-
-_realpath() {
- [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"
-}
-_CURDIR=$(_realpath $(dirname "$0"))
-ROOTDIR="${_CURDIR}/../../"
-
-# Export the general params that need to be added do 'docker run'
-DOCKER_RUN_PARAMS="--rm -it --workdir /github/workspace -v ${ROOTDIR}:/github/workspace"
-
-RED="\033[0;31m"
-GREEN="\033[0;32m"
-ORANGE="\033[0;33m"
-BLUE="\033[0;34m"
-PURPLE="\033[0;35m"
-CYAN="\033[0;36m"
-LIGHTGRAY="\033[0;37m"
-DARKGRAY="\033[1;30m"
-LIGHTRED="\033[1;31m"
-LIGHTGREEN="\033[1;32m"
-YELLOW="\033[1;33m"
-LIGHTBLUE="\033[1;34m"
-LIGHTPURPLE="\033[1;35m"
-LIGHTCYAN="\033[1;36m"
-WHITE="\033[1;37m"
-CLEAR="\033[0m"
-
-CUR_TEST=""
-
-# Prints the status of a test
-printStatus() {
- STATUS=$1
-
- if [ $STATUS -eq 0 ]; then
- echo
- echo -e "${CUR_TEST}: [ ${LIGHTGREEN}OK${CLEAR} ]"
- echo
- else
- echo
- echo -e "${CUR_TEST} [ ${LIGHTRED}FAILED${CLEAR} ]"
- echo
- exit 1
- fi
-}
-
-# Prints the current test name
-startTest() {
- CUR_TEST=$1
- echo
- echo -e "${LIGHTBLUE}Starting test: ${WHITE}${CUR_TEST}${CLEAR}"
- echo
-}
diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml
deleted file mode 100644
index ea0cd9f..0000000
--- a/.github/workflows/gh-pages.yml
+++ /dev/null
@@ -1,21 +0,0 @@
-name: Github Pages
-
-on:
- push:
- branches:
- - main
-
-jobs:
- build-and-publish:
- runs-on: ubuntu-22.04
- steps:
- - uses: actions/checkout@v2
- - run: sudo apt-get install make
- - run: make
- - name: Deploy
- uses: peaceiris/actions-gh-pages@v3
- if: github.ref == 'refs/heads/main'
- with:
- github_token: ${{ secrets.GITHUB_TOKEN }}
- publish_dir: ./build
- commit_message: "Publish: ${{ github.event.head_commit.message }}"
diff --git a/.gitignore b/.gitignore
deleted file mode 100644
index e5d8620..0000000
--- a/.gitignore
+++ /dev/null
@@ -1,4 +0,0 @@
-build*/
-.cache
-.vscode
-.gitattributes
diff --git a/.nojekyll b/.nojekyll
new file mode 100644
index 0000000..e69de29
diff --git a/LICENSE.txt b/LICENSE.txt
deleted file mode 100644
index bd8b243..0000000
--- a/LICENSE.txt
+++ /dev/null
@@ -1,218 +0,0 @@
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright [yyyy] [name of copyright owner]
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-
-
---- LLVM Exceptions to the Apache 2.0 License ----
-
-As an exception, if, as a result of your compiling your source code, portions
-of this Software are embedded into an Object form of such source code, you
-may redistribute such embedded portions in such Object form without complying
-with the conditions of Sections 4(a), 4(b) and 4(d) of the License.
-
-In addition, if you combine or link compiled forms of this Software with
-software that is licensed under the GPLv2 ("Combined Software") and if a
-court of competent jurisdiction determines that the patent provision (Section
-3), the indemnity provision (Section 9) or other Section of the License
-conflicts with the conditions of the GPLv2, you may retroactively and
-prospectively choose to deem waived or otherwise exclude such Section(s) of
-the License, but only in their entirety and only with respect to the Combined
-Software.
diff --git a/Makefile b/Makefile
deleted file mode 100644
index e7e47e8..0000000
--- a/Makefile
+++ /dev/null
@@ -1,17 +0,0 @@
-BUILDDIR = build
-
-all: remote
-
-setup:
- mkdir -p $(BUILDDIR)/
-
-remote: setup
- find . -maxdepth 1 -name "execution.bs" -type f | sed 's/\.bs$$//' | xargs -I{} -t -n 1 sh -c "curl https://api.csswg.org/bikeshed/ -F force=1 -F file=@{}.bs > $(BUILDDIR)/\`basename {}\`.html"
-
-local: setup
- find . -maxdepth 1 -name "execution.bs" -type f | sed 's/\.bs$$//' | xargs -I{} -t -n 1 sh -c "bikeshed -f spec {}.bs $(BUILDDIR)/\`basename {}\`.html"
-
-clean:
- rm $(BUILDDIR)/*
- rmdir $(BUILDDIR)
-
diff --git a/P2300R0.html b/P2300R0.html
deleted file mode 100644
index b00afce..0000000
--- a/P2300R0.html
+++ /dev/null
@@ -1,5343 +0,0 @@
-
-
ISO/IEC JTC1/SC22/WG21 14882: Programming Language — C++
-
Audience:
-
SG1, LEWG
-
-
-
-
-
-
-
-
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 parallelism, 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 (std::atomic, std::mutex, std::counting_semaphore, etc) and lower level building blocks (std::thread, etc), we lack a Standard vocabulary and framework for asynchrony and parallelism that C++ programmers desperately need. std::async/std::future/std::promise, 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 both lazy and eager execution in a way that does not compromise the efficiency of either and allows users to write code that is agnostic to eagerness.
-
-
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.
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.11.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.12.2 execution::then is a sender adaptor that takes an input sender and a std::invocable, and calls the std::invocable on the signal sent by the input sender. The sender returned by then sends the result of that invocation. In this case, the input sender came from schedule, so its void, meaning it won’t send us a value, so our std::invocable takes no parameters. But we return an int, which will be sent to the next recipient.
-
-
Now, we add another operation to the chain, again using § 4.12.2 execution::then. This time, we get sent a value - the int from the previous step. We add 42 to it, and then return the result.
-
-
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.13.2 std::this_thread::sync_wait, which will either return a std::optional<std::tuple<...>> with the value sent by the last sender, or an empty std::optional if the last sender sent a done signal, or it throws an exception if the last sender sent an error.
This example builds an asynchronous computation of an inclusive scan:
-
-
-
It scans a sequence of doubles (represented as the std::span<constdouble>input) and stores the result in another sequence of doubles (represented as std::span<double>output).
-
-
It takes a scheduler, which specifies what execution context the scan should be launched on.
-
-
It also takes a tile_count parameter that controls the number of execution agents that will be spawned.
-
-
First we need to allocate temporary storage needed for the algorithm, which we’ll do with a std::vector, partials. We need one double of temporary storage for each execution agent we create.
-
-
Next we’ll create our initial sender with § 4.11.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 sch, which means the next item in the chain will use sch.
-
-
Senders and sender adaptors support composition via operator|, similar to C++ ranges. We’ll use operator| to attach the next piece of work, which will spawn tile_count execution agents using § 4.12.8 execution::bulk (see § 4.10 Most sender adaptors are pipeable for details).
-
-
Each agent will call a std::invocable, passing it two arguments. The first is the agent’s index (i) in the § 4.12.8 execution::bulk operation, in this case a unique integer in [0,tile_count). The second argument is what the input sender sent - the temporary storage.
-
-
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 std::inclusive_scan over our elements. We store the scan result for our last element, which is the sum of all of our elements, in our temporary storage partials.
-
-
After all computation in that initial § 4.12.8 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 partials. We’ll do that with a single execution agent which will execute after the § 4.12.8 execution::bulk completes. We create that execution agent with § 4.12.2 execution::then.
-
-
§ 4.12.2 execution::then takes an input sender and an std::invocable and calls the std::invocable with the value sent by the input sender. Inside our std::invocable, we call std::inclusive_scan on partials, which the input senders will send to us.
-
-
Then we return partials, which the next phase will need.
-
-
Finally we do another § 4.12.8 execution::bulk of the same shape as before. In this § 4.12.8 execution::bulk, we will use the scanned values in partials to integrate the sums from other tiles into our elements, completing the inclusive scan.
-
-
async_inclusive_scan returns a sender that sends the output std::span<double>. A consumer of the algorithm can chain additional work that uses the scan result. At the point at which async_inclusive_scan returns, the computation may not have completed. In fact, it may not have even started.
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:
-
-
-
async_read 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 a std::span<std::byte>, and a handle to an I/O context. It will asynchronously read into the input buffer, up to the size of the std::span. It returns a sender which will send the number of bytes read once the read completes.
-
-
async_read_array 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 a dynamic_buffer object that owns the data that was sent.
-
-
dynamic_buffer is an aggregate struct that contains a std::unique_ptr<std::byte[]> and a size.
-
-
The first thing we do inside of async_read_array is create a sender that will send a new, empty dynamic_array object using § 4.11.2 execution::just. We can attach more work to the pipeline using operator| composition (see § 4.10 Most sender adaptors are pipeable for details).
-
-
We need the lifetime of this dynamic_array object to last for the entire pipeline. So, we use let_value, which takes an input sender and a std::invocable that must return a sender itself (see § 4.12.4 execution::let_* for details). let_value sends the value from the input sender to the std::invocable. Critically, the lifetime of the sent object will last until the sender returned by the std::invocable completes.
-
-
Inside of the let_valuestd::invocable, we have the rest of our logic. First, we want to initiate an async_read of the buffer size. To do that, we need to send a std::span pointing to buf.size. We can do that with § 4.11.2 execution::just.
Next, we pipe a std::invocable that will be invoked after the async_read completes using § 4.12.2 execution::then.
-
-
That std::invocable gets sent the number of bytes read.
-
-
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 std::span<std::byte> to the storage for the data from the std::invocable. This will be sent to the next recipient in the pipeline.
-
-
And that recipient will be another async_read, which will read the data.
-
-
Once the data has been read, in another § 4.12.2 execution::then, we confirm that we read the right number of bytes.
-
-
Finally, we move out of and return our dynamic_buffer object. It will get sent by the sender returned by async_read_array. We can attach more things to that sender to use the data in the buffer.
-
-
1.4. 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.5. Design changes from P0443
-
-
-
The executor concept has been removed and all of its proposed functionality is now based on schedulers and senders, as per SG1 direction.
-
-
Properties are not included in this paper. We see them as a possible future extension, if the committee gets more comfortable with them.
-
-
Users now have a choice between using a strictly lazy vs a possibly eager version of most sender algorithms.
-
-
Senders now advertise what scheduler, if any, their evaluation will complete on.
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.6. Prior art
-
This proposal builds upon and learns from years of prior art with asynchronous and parallel programming frameworks in C++.
-
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.
-
Coroutines suffer many of the same problems, but can avoid synchronizing when chaining dependent work because they typically start suspended. In many cases, coroutine frames require unavoidable dynamic allocation. Consequently, coroutines in embedded or heterogeneous environments require great attention to detail. Nor are coroutines good candidates for cancellation because the early and safe termination of coroutines requires unsatisfying solutions. On the one hand, exceptions are inefficient and disallowed in many environments. Alternatively, clumsy ad-hoc mechanisms, whereby co_yield returns a status code, hinder correctness. See [P1662R0] for a complete discussion.
-
Callbacks are the 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, but the lack of a standard obstructs generic design. Additionally, few of these possibilities accommodate cancellation signals when the user requests upstream work to stop and clean up.
-
1.7. Field experience
-
This proposal draws heavily from our field experience with libunifex, Thrust, and Agency. It is also inspired by the needs of countless other C++ frameworks for asynchrony, parallelism, and concurrency, including:
Before this proposal is approved, we will present a new implementation of this proposal written from the specification and intended as a contribution to libc++. This implementation will demonstrate the viability of the design across the use cases and execution contexts that the committee has identified as essential.
-
2. Revision history
-
2.1. R0
-
Initial revision, still in progress.
-
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]: std::execution; however, for brevity, the std:: part of this name is omitted. When you see execution::foo, treat that as std::execution::foo.
-
-
Universal references and explicit calls to std::move/std::forward are omitted in code samples and signatures for simplicity; assume universal references and perfect forwarding unless stated otherwise.
-
-
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 std::invocable 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 std::invocable 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, schedule, 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::schedulerautosch=get_thread_pool().scheduler();
-execution::senderautosnd=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 execution::schedule() API.
-
Future papers will propose additional scheduler concepts that extend scheduler to add other capabilities. For example:
-
-
-
A time_scheduler concept that extends scheduler to support time-based scheduling.
-Such a concept might provide access to schedule_after(sched,duration), schedule_at(sched,time_point) and now(sched) APIs.
-
-
Concepts that extend scheduler to support opening, reading and writing files asynchronously.
-
-
Concepts that extend scheduler to support connecting, sending data and receiving data over the network asynchronously.
-
-
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.
execution::schedulerautosch=get_thread_pool().scheduler();
-execution::senderautosnd=execution::schedule(sch);
-execution::senderautocont=execution::then(snd,[]{
- std::fstreamfile{"result.txt"};
- file<<compute_result;
-});
-
-std::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 execution::connect) senders and return a sender.
-
-
sender consumers, algorithms that take (and potentially execution::connect) senders and do not return a sender.
-
-
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 std::thread (which they aren’t).
-
-
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 on 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
-
get_completion_scheduler is a query that retrieves the completion scheduler for a specific completion signal from a sender.
-Calling get_completion_scheduler on a sender that does not have a completion scheduler for a given signal is ill-formed.
-If a scheduler 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::schedulerautocpu_sched=new_thread_scheduler{};
-execution::schedulerautogpu_sched=cuda::scheduler();
-
-execution::senderautosnd0=execution::schedule(cpu_sched);
-execution::schedulerautocompletion_sch0=
- execution::get_completion_scheduler<execution::set_value_t>(snd0);
-// completion_sch0 is equivalent to cpu_sched
-
-execution::senderautosnd1=execution::then(snd0,[]{
- std::cout<<"I am running on cpu_sched!\n";
-});
-execution::schedulerautocompletion_sch1=
- execution::get_completion_scheduler<execution::set_value_t>(snd1);
-// completion_sch1 is equivalent to cpu_sched
-
-execution::senderautosnd2=execution::transfer(snd1,gpu_sched);
-execution::senderautosnd3=execution::then(snd2,[]{
- std::cout<<"I am running on gpu_sched!\n";
-});
-execution::schedulerautocompletion_sch3=
- execution::get_completion_scheduler<execution::set_value_t>(then3);
-// completion_sch3 is equivalent to cpu_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 execution::schedule, 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 execution::transfer sender adaptor performs a transition from one execution context to another:
-
execution::schedulerautosch1=...;
-execution::schedulerautosch2=...;
-
-execution::senderautosnd1=execution::schedule(sch1);
-execution::senderautothen1=execution::then(snd1,[]{
- std::cout<<"I am running on sch1!\n";
-});
-
-execution::senderautosnd2=execution::transfer(then1,sch2);
-execution::senderautothen2=execution::then(snd2,[]{
- std::cout<<"I am running on sch2!\n";
-});
-
-std::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 std::unique_ptr that it will be transferring ownership of to the
-operation-state returned by a call to execution::connect so that the operation has access to
-this resource. In such a sender, calling execution::connect 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 execution::connect only has overloads for an rvalue-qualified sender. Callers must pass the sender
-as an rvalue to the call to execution::connect, indicating that the call consumes the sender.
-
A multi-shot sender can be connected to multiple receivers and can be launched multiple
-times. Mult-shot senders customise execution::connect to accept an lvalue reference to the
-sender. Callers can indicate that they want the sender to remain valid after the call to execution::connect by passing an lvalue reference to the sender to call these overloads. Multi-shot senders should also define
-overloads of execution::connect that accept rvalue-qualified enders 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 execution::connect.
-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 execution::connect. 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 execution::connect on the input sender, such as in this_thread::sync_wait or execution::start_detached.
-
Some multi-use sender algorithms may require that an input sender be copy-constructible but will only call execution::connect() 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 execution::connect 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 split sender adaptor facilitates connecting to a sender multiple times, regardless of whether it is single-shot or multi-shot:
-
autosome_algorithm(execution::senderauto&&input){
- execution::senderautomulti_shot=split(input);
- // "multi_shot" is guaranteed to be multi-shot,
- // regardless of whether "input" was multi-shot or not
-
- returnwhen_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.
-
when_all 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. when_all returns a sender that also does not have an associated scheduler.
-
transfer_when_all 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 when_all. You can think of it as a composition of transform(when_all(inputs...),scheduler), but one that allows for better efficiency through customization.
-
4.10. Most sender adaptors are pipeable
-
To facilitate an intuitive syntax for composition, most sender adaptors are pipeable; they can be composed (piped) together with operator|.
-This mechanism is similar to the operator| 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.
-
a|b will pass the sender a as the first argument to the pipeable sender adaptor b. Pipeable sender adaptors support partial application of the parameters after the first. For example, all of the following are equivalent:
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:
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.
-
-
-
execution::when_all and execution::when_all_with_variant: 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::on and execution::lazy_on: 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 to 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.11. User-facing sender factories
-
A sender factory is an algorithm that takes no senders as parameters and returns a sender.
execution::schedulerautosch1=get_system_thread_pool().scheduler();
-
-execution::senderautosnd1=execution::schedule(sch1);
-// snd1 describes the creation of a new task on the system thread pool
-
Returns a sender with no completion schedulers, which sends the provided values. If a provided value is an lvalue reference, a copy is made inside the returned sender and a non-const lvalue reference to the copy is sent. If the provided value is an rvalue reference, it is moved into the returned sender and an rvalue reference to it is sent.
Returns a sender whose value completion scheduler is the provided scheduler, which sends the provided values in the same manner as just.
-
execution::senderautovals=execution::transfer_just(
- get_system_thread_pool().scheduler(),
- 1,2,3
-);
-execution::senderautosnd=execution::then(pred,[](auto...args){
- std::print(args..);
-});
-// when snd is executed, it will print "123"
-
-
This adaptor is included as it greatly simplifies lifting values into senders.
-
4.12. User-facing sender adaptors
-
A sender adaptor is an algorithm that takes one or more senders, which it may execution::connect, as parameters, and returns a sender, whose completion is related to the sender arguments it has received.
-
Many sender adaptors come in two versions: a strictly lazy one, which is never allowed to submit any work for execution prior to the returned sender being started later on, and a potentially eager one, which is allowed to submit work prior to
-the returned sender being started. Sender consumers such as § 4.12.12 execution::ensure_started, § 4.13.1 execution::start_detached, and § 4.13.2 std::this_thread::sync_wait start senders; the implementations of non-lazy versions of the sender adaptors are allowed,
-but not guaranteed, to start senders.
-
The strictly lazy versions of the adaptors below (that is, all the versions whose names start with lazy_) are guaranteed to not start any input senders passed into them.
execution::schedulerautocpu_sched=get_system_thread_pool().scheduler();
-execution::schedulerautogpu_sched=cuda::scheduler();
-
-execution::senderautocpu_task=execution::schedule(cpu_sched);
-// cpu_task describes the creation of a new task on the system thread pool
-
-execution::senderautogpu_task=execution::transfer(cpu_task,gpu_sched);
-// gpu_task describes the transition of the task graph described by cpu_task to the gpu
-
then 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.
-
lazy_then is guaranteed to not begin executing function until the returned sender is started.
-
execution::senderautoinput=get_input();
-execution::senderautosnd=execution::then(input,[](auto...args){
- std::print(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.
upon_error and upon_done are similar to then, but where then works with values sent by the input sender, upon_error works with errors, and upon_done is invoked when the "done" signal is sent.
let_value is very similar to then: 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 then sends exactly what that function ends up returning - let_value requires that the function return a sender, and the sender returned by let_value 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.
-
lazy_let_value is guaranteed to not begin executing function until the returned sender is started.
-
let_error and let_done are similar to let_value, but where let_value works with values sent by the input sender, let_error works with errors, and let_done is invoked when the "done" signal is sent.
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.
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.
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.
when_all returns a sender which completes once all of the input senders have completed. The values send by this sender are the values sent by each of the input, in order of the arguments passed to when_all.
-
when_all_with_variant does the same, but it adapts all the input senders using into_variant.
execution::schedulerautosched=get_thread_pool().scheduler();
-
-execution::senderautosends_1=...;
-execution::senderautosends_abc=...;
-
-execution::senderautoboth=execution::when_all(sched,
- sends_1,
- sends_abc
-);
-
-execution::senderautofinal=execution::then(both,[](auto...args){
- std::cout<<std::format("the two args: {}, {}",args...);
-});
-// when final executes, it will print "the two args: 1, abc"
-
Once ensure_started returns, it is known that the provided sender has been connected and start 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.
-
4.13. User-facing sender consumers
-
A sender consumer is an algorithm that takes one or more senders, which it may execution::connect, as parameters, and does not return a sender.
std::this_thread::sync_wait is a sender consumer that submits the work described by the provided sender for execution, similarly to ensure_started, except that it blocks the current std::thread or thread of main 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.11.1 execution::schedule and § 4.11.3 execution::transfer_just are meant to enter the domain of senders, sync_wait 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, sync_wait throws that error as an exception, or rethrows the original exception if the error is of type std::exception_ptr.
-
If the provided sender sends the "done" signal instead of values, sync_wait returns an empty optional.
-
For an explanation of the requires clause, see § 5.8 Most senders are typed. That clause also explains another sender consumer, built on top of sync_wait: sync_wait_with_variant.
-
Note: Notice that this function is specified inside std::this_thread, and not inside execution. This is because sync_wait 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 std::this_thread, 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 std::this_fiber::sync_wait would be provided. We also expect that runtimes with execution agents that use different synchronization mechanisms than std::thread's will provide their own flavors of sync_wait as well (assuming their execution agents have the means
-to block in a non-deadlock manner).
-
4.14. 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.
A receiver is a callback that supports more than one channel. In fact, it supports three of them:
-
-
-
set_value, which is the moral equivalent of an operator() or a function call, which signals successful completion of the operation its execution depends on;
-
-
set_error, 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; and
-
-
set_done, which signals that the operation completed without succeeding (set_value) and without failing (set_error). 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.
-
-
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 set_value failed with an exception, either set_error or set_done 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 std::promise, which provides the first two signals as set_value and set_error, 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.
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: start, 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 execution::ensure_started and std::this_thread::sync_wait, 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.
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 connect will be fulfilled.
-
execution::senderautosnd=someinputsender;
-execution::receiverautorcv=somereceiver;
-execution::operation_stateautostate=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 then, 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 execution::then(sender,invocable) to be equivalent to:
-
-
-
sender.then(invocable), if that expression is well formed; otherwise
-
-
then(sender,invocable), performed in a context where this call always performs ADL, if that expression is well formed; otherwise
-
-
a default implementation of then, which returns a sender adaptor, and then define the exact semantics of said adaptor.
-
-
However, this definition is problematic. Imagine another sender adaptor, bulk, 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 bulk to have specialized behavior, which invokes a kernel of more than one thread (with its size defined by the call to bulk); therefore, we would like to customize bulk for CUDA senders to achieve this. However, there’s no reason for CUDA kernels to
-necessarily customize the then sender adaptor, as the generic implementation is perfectly sufficient. This creates a problem, though; consider the following snippet:
-
execution::schedulerautocuda_sch=cuda_scheduler{};
-
-execution::senderautoinitial=execution::schedule(cuda_sch);
-// the type of initial is a type defined by the cuda_scheduler
-// let’s call it cuda::schedule_sender<>
-
-execution::senderautonext=execution::then(cuda_sch,[]{return1;});
-// 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::senderautokernel_sender=execution::bulk(next,shape,[](inti){...});
-
-
How can we specialize the bulk sender adaptor for our wrapped schedule_sender? 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):
However, if the input sender is not just a then_sender_adaptor like in the example above, but another sender that overrides bulk 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 execution::<sender-algorithm>(sender,args...), for any given sender algorithm that accepts a sender as its first argument, should be
-equivalent to:
-
-
-
tag_invoke(<sender-algorithm>,get_completion_scheduler<Signal>(sender),sender,args...), if that expression is well-formed; otherwise
-
-
tag_invoke(<sender-algorithm>,sender,args...), if that expression is well-formed; otherwise
-
-
a default implementation, if there exists a default implementation of the given sender algorithm.
-
-
where Signal is one of set_value, set_error, or set_done; for most sender algorithms, the completion scheduler for set_value would be used, but for some (like upon_error or let_done), one of the others would be used.
-
For sender algorithms which accept concepts other than sender as their first argument, we propose that the customization scheme remains as it has been in [P0443R14] so far, except it should also use tag_invoke.
-
5.5. Laziness is defined by sender adaptors
-
We distinguish two different guarantees about when work is submitted to an execution context:
-
-
-
strictly lazy submission, which means that there is a guarantee that no work is submitted to an execution context before a receiver is connected to a sender, and execution::start is called on the resulting operation state;
-
-
potentially eager submission, which means that work may be submitted to an execution context as soon as all the information necessary to perform it is provided.
-
-
If a sender adaptor requires potentially eager submission, strictly lazy submission is acceptable as an implementation, because it does fulfill the potentially eager guarantee. This is why the default implementations for the non-strictly-lazy sender adaptors are specified
-to dispatch to the strictly lazy ones; for an author of a specific sender, it is sufficient to specialize the strictly lazy version, to also achieve a specialization of the potentially eager one.
-
As has been described in § 4.12 User-facing sender adaptors, whether a sender adaptor is guaranteed to perform strictly lazy submission or not is defined by the adaptor used to perform it; the adaptors whose names begin with lazy_ provide the strictly lazy guarantee.
-
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.12.12 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 lazy § 4.12.8 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 execution::transfer 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 transfer 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 transfer.
-
To allow for such customization from both ends, we propose the inclusion of a secondary transitioning sender adaptor, called schedule_from. This adaptor is a form of schedule, 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 transfer, to ensure that both schedulers have a say in how the transitions are made. Any scheduler that specializes transfer(snd,sch) shall ensure that the
-return value of their customization is equivalent to schedule_from(sch,snd2), where snd2 is a successor of snd that sends values equivalent to those sent by snd.
-
The default implementation of transfer(snd,sched) is schedule_from(sched,snd).
-
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 imput sender is typed or not in common sender adaptors such as execution::then is
-hard.
-
The mechanism for this advertisement is the same as in [P0443R14]; the way to query the types is through sender_traits::value_types<tuple_like,variant_like>.
-
sender_traits::value_types 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 when_all). 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.13.2 std::this_thread::sync_wait: it returns a tuple of values sent by the sender passed to it, wrapped in std::optional to handle the set_done signal. However, this assumes that those values can be represented as a
-tuple, like here:
-
execution::senderautosends_1=...;
-execution::senderautosends_2=...;
-execution::senderautosends_3=...;
-
-auto[a,b,c]=std::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 value_types of a sender which sends Types... to be as follows:
If senders could only ever send one specific set of values, this would probably need to be the required form of value_types 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) set_value 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 value_types, one that represents a variant-like type. If we permit such senders, we would almost certainly need to require that the canonical form of value_types 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 Types1..., Types2..., ..., TypesN... to be as follows:
This, however, introduces a couple of complications:
-
-
-
A just(1) sender would also need to follow this structure, so the correct type for storing the value sent by it would be std::variant<std::tuple<int>> 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 where value_types 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.
-
-
As a consequence of (1): because sync_wait needs to store the above type, it can no longer return just a std::tuple<int> for just(1); it has to return std::variant<std::tuple<int>>. 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 of sync_wait must be the same across all sender types.
-
-
One possible solution to (2) above is to place a requirement on sync_wait that it can only accept senders which send only a single set of values, therefore removing the need for std::variant to appear in its API; because of this, we propose to expose both sync_wait, which is a simple, user-friendly version of the sender consumer, but requires that value_types have only one possible variant, and sync_wait_with_variant, which accepts any sender, but returns an optional whose value type is the variant of all the
-possible tuples sent by the input sender:
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: sync_wait. We imagine that if, in the future, C++ was to
-gain fibers support, we would want to also have std::this_fiber::sync_wait, in addition to std::this_thread::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.
-
-
This paper proposes to instead use the mechanism described in [P1895R0]: tag_invoke; the wording for tag_invoke has been incorporated into the proposed specification in this paper.
-
In short, instead of using globally reserved names, tag_invoke uses the type of the customization point object itself as the mechanism to find customizations. It globally reserves only a single name - tag_invoke - which itself is used the same way that
-ranges-style customization points are used. All other customization points are defined in terms of tag_invoke. For example, the customization for std::this_thread::sync_wait(s) will call tag_invoke(std::this_thread::sync_wait,s), instead of attempting
-to call s.sync_wait(), and then sync_wait(s) if the member call is not valid.
-
Using tag_invoke 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<typenameTag,typename...Args>
-friendautotag_invoke(Tag&&tag,wrapper&self,Args&&...args){
- returnstd::forward<Tag>(tag)(self.subobject,std::forward<Args>(args)...);
-}
-
-// but override one of them with a specific value
-friendautotag_invoke(specific_customization_point_t,wrapper&self){
- returnself.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 tag_invoke, dispatching to manually constructed vtables containing pointers to specialized implementations for the wrapped objects. For an example of such a polymorphic wrapper, see unifex::any_unique (example).
-
-
6. Specification
-
Much of this wording follows the wording of [P0443R14].
Insert this section as a new subclause, between Searchers [func.search] and Class template hash[unord.hash].
-
-
-
-
-
The name std::tag_invoke denotes a customization point object. For some subexpressions tag and args..., tag_invoke(tag,args...) is expression-equivalent to an unqualified call to tag_invoke(decay-copy(tag),args...) with overload
-resolution performed in a context that includes the declaration:
-
voidtag_invoke();
-
-
and that does not include the the std::tag_invoke name.
-
-
-
-
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 <stop_token>. 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.
Insert this section as a new subclause between Header <stop_token> synopsis [thread.stoptoken.syn] and Class stop_token[stoptoken].
-
-
-
-
-
The stoppable_token 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, T::callback_type<CB>, 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. The stoppable_token_for concept checks for a stop token type compatible with a given
-callback type. The unstoppable_token concept checks for a stop token type that does not allow stopping.
Let t and u be distinct object of type T. The type T models stoppable_token only if:
-
-
-
All copies of a stoppable_token reference the same logical shared stop state and shall report values consistent with each other.
-
-
If t.stop_possible() evaluates to false then, if u, references the same logical shared stop state, u.stop_possible() shall also subsequently evaluate to false and u.stop_requested() shall also subsequently evaluate to false.
-
-
If t.stop_requested() evaluates to true then, if u, references the same logical shared stop state, u.stop_requested() shall also subsequently evaluate to true and u.stop_possible() shall also subsequently evaluate to true.
-
-
Given a callback-type, CB, and a callback-initializer argument, init, of type Initializer then constructing an instance, cb, of type T::callback_type<CB>, passing t as the first argument and init as the second argument to the constructor, shall,
-if t.stop_possible() is true, construct an instance, callback, of type CB, direct-initialized with init, and register callback with t’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.
-
-
-
If t.stop_requested() is true at the time callback is registered then callback may be invoked immediately inline inside the call to cb’s constructor.
-
-
If callback is invoked then, if u references the same shared stop state as t, an evaluation of u.stop_requested() will be true if the beginning of the invocation of callback strongly-happens-before the evaluation of u.stop_requested().
-
-
If t.stop_possible() evaluates to false then the construction of cb is not required to construct and initialize callback.
-
-
-
Construction of a T::callback_type<CB> instance shall only throw exceptions thrown by the initialization of the CB instance from the value of type Initializer.
-
-
Destruction of the T::callback_type<CB> object, cb, deregisters callback from the shared stop state such that callback will not be invoked after the destructor returns.
-
-
-
If callback is currently being invoked on another thread then the destructor of cb will block until the invocation of callback returns such that the return from the invocation of callback strongly-happens-before the destruction of callback.
-
-
Destruction of a callback cb shall not block on the completion of the invocation of some other callback registered with the same shared stop state.
-
-
-
-
-
-
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.
-
-
-
Table 1: Execution control library summary [tab:execution.summary]
None of a scheduler’s copy constructor, destructor, equality comparison, or swap member functions shall exit via an exception.
-
-
None of these member functions, nor a scheduler type’s schedule function, shall introduce data races as a result of concurrent invocations of those functions from different
-threads.
-
-
For any two (possibly const) values s1 and s2 of some scheduler type S, s1==s2 shall return true only if both s1 and s2 are handles to the same associated execution context.
-
-
A scheduler type’s destructor shall not block pending completion of any receivers connected to the sender objects returned from schedule. [Note: The ability to wait for completion of submitted function objects may be provided by the associated execution
-context of the scheduler. —end note]
-
-
9.4. Receivers [execution.receiver]
-
-
-
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, set_error, and set_done. These are collectively known as a receiver’s completion-signal operations.
-
-
The receiver concept defines the requirements for a receiver type with an unknown set of value types. The receiver_of concept defines the requirements for a receiver type with a known set of value types, whose error type is std::exception_ptr.
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 execution::start has been called on the operation state object that was returned by execution::connect to connect that receiver to a sender.
-
-
Once execution::start 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.
-
-
If execution::set_value exits with an exception, it is still valid to call execution::set_error or execution::set_done on the receiver, but it is no longer valid to call execution::set_value on the receiver.
-
-
-
Once one of a receiver’s completion-signal operations has completed non-exceptionally, the receiver contract has been satisfied.
-
-
9.4.1. Set value algorithm [execution.receiver.set_value]
-
-
-
execution::set_value is used to send a value completion signal to a receiver.
-
-
The name execution::set_value denotes a customization point object. The expression execution::set_value(R,Vs...) for some subexpressions R and Vs... is expression-equivalent to:
-
-
-
tag_invoke(execution::set_value,R,Vs...), if that expression is valid. If the function selected by tag_invoke does not send the value(s) Vs... to the receiver R’s value channel, the program is ill-formed with no diagnostic required.
-
-
Otherwise, execution::set_value(R,Vs...) is ill-formed.
-
-
-
9.4.2. Set error algorithm [execution.receiver.set_error]
-
-
-
execution::set_error is used to send a error signal to a receiver.
-
-
The name execution::set_error denotes a customization point object. The expression execution::set_error(R,E) for some subexpressions R and E is expression-equivalent to:
-
-
-
tag_invoke(execution::set_error,R,E), if that expression is valid. If the function selected by tag_invoke does not send the error E to the receiver R’s error channel, the program is ill-formed with no diagnostic required.
-
-
Otherwise, execution::set_error(R,E) is ill-formed.
-
-
-
9.4.3. Set done algorithm [execution.receiver.set_done]
-
-
-
execution::set_done is used to send a done signal to a receiver.
-
-
The name execution::set_done denotes a customization point object. The expression execution::set_done(R) for some subexpression R is expression-equivalent to:
-
-
-
tag_invoke(execution::set_done,R), if that expression is valid. If the function selected by tag_invoke does not signal the receiver R’s done channel, the program is ill-formed with no diagnostic required.
execution::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]
-
-
The name execution::get_scheduler denotes a customization point object. For some subexpression r, let R be decltype((r)). If R does not satisfy execution::receiver, execution::get_scheduler is ill-formed. Otherwise, execution::get_scheduler(r) is
-expression equivalent to:
-
-
-
tag_invoke(execution::get_scheduler,as_const(r)), if this expression is well formed and satisfies execution::scheduler, and is noexcept.
-
-
Otherwise, execution::get_scheduler(r) is ill-formed.
execution::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]
-
-
The name execution::get_allocator denotes a customization point object. For some subexpression r, let R be decltype((r)). If R does not satisfy execution::receiver, execution::get_allocator is ill-formed. Otherwise, execution::get_allocator(r) is
-expression equivalent to:
-
-
-
tag_invoke(execution::get_allocator,as_const(r)), if this expression is well formed and models Allocator, and is noexcept.
-
-
Otherwise, execution::get_allocator(r) is ill-formed.
execution::get_stop_token is used to ask a receiver object for an associated stop token 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]
-
-
The name execution::get_stop_token denotes a customization point object. For some subexpression r, let R be decltype((r)). If R does not satisfy execution::receiver, execution::get_stop_token is ill-formed. Otherwise, execution::get_stop_token(r) is expression equivalent to:
-
-
-
tag_invoke(execution::get_stop_token,as_const(r)), if this expression is well formed and satisfies stoppable_token, and is noexcept.
-
-
Otherwise, never_stop_token{}.
-
-
-
9.5. Operation states [execution.op_state]
-
-
-
The operation_state concept defines the requirements for an operation state type, which allows for starting the execution of work.
execution::start is used to start work represented by an operation state object.
-
-
The name execution::start denotes a customization point object. The expression execution::start(O) for some lvalue subexpression O is expression-equivalent to:
-
-
-
tag_invoke(execution::start,O), if that expression is valid. If the function selected by tag_invoke does not start the work represented by the operation state O, the program is ill-formed with no diagnostic required.
-
-
Otherwise, execution::start(O) is ill-formed.
-
-
-
The caller of execution::start(O) must guarantee that the lifetime of the operation state object O extends at least until one of the receiver completion-signal functions of a receiver R passed into the execution::connect call that produced O 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]
-
-
9.6. Senders [execution.sender]
-
-
-
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 sender concept defines the requirements for a sender type. The sender_to concept defines the requirements for a sender type capable of being connected with a specific receiver type.
The class sender_base is used as a base class to tag sender types which do not expose member templates value_types, error_types, and a static member constant expression sends_done.
-
-
The class template sender_traits is used to query a sender type for facts associated with the signal it sends.
-
-
The primary class template sender_traits<S> is defined as if inheriting from an implementation-defined class template sender-traits-base<S> defined as follows:
-
-
-
If has-sender-types<S> is true, then sender-traits-base<S> is equivalent to:
Otherwise, if derived_from<S,sender_base> is true, then sender-traits-base<S> is equivalent to
-
template<classS>
- structsender-traits-base{};
-
-
-
Otherwise, sender-traits-base<S> is equivalent to
-
template<classS>
- structsender-traits-base{
- using__unspecialized=void;// exposition only
- };
-
-
-
-
If sender_traits<S>::value_types<Tuple,Variant> for some sender type S is well formed, it shall be a type Variant<Tuple<Args0...,Args1...,...,ArgsN...>>, where the type packs Args0 through ArgsN are the packs of types the sender S passes as
-arguments to execution::set_value after a receiver object. If such sender S invokes execution::set_value(r,args...) for some receiver r, where decltype(args) is not one of the type packs Args0 through ArgsN, the program is ill-formed with no
-diagnostic required.
-
-
If sender_traits<S>::error_types<Variant> for some sender type S is well formed, it shall be a type Variant<E0,E1,...,EN>, where the types E0 through EN are the types the sender S passes as arguments to execution::set_error after a receiver
-object. If such sender S invokes execution::set_error(r,e) for some receiver r, where decltype(e) is not one of the types E0 through EN, the program is ill-formed with no diagnostic required.
-
-
If sender_traits<S>::sends_done is well formed and true, and such sender S invokes execution::set_done(r) for some receiver r, the program is ill-formed with no diagnostic required.
-
-
Users may specialize sender_traits on program-defined types.
execution::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.
-
-
The name execution::connect denotes a customization point object. For some subexpressions s and r, let S be decltype((s)) and R be decltype((r)). If R does not satisfy execution::receiver or S does not satisfy execution::sender, execution::connect(s,r) is ill-formed. Otherwise, the expression execution::connect(s,r) is expression-equivalent to:
-
-
-
tag_invoke(execution::connect,s,r), if that expression is valid and its type satisfies execution::operation_state. If the function selected by tag_invoke does not return an operation state for which execution::start starts work described by s, the program
-is ill-formed with no diagnostic required.
-
-
Otherwise, execution::connect(s,r) is ill-formed.
-
-
-
Standard sender types shall always expose an rvalue-qualified overload of a customization of execution::connect. Standard sender types shall only expose an lvalue-qualified overload of a customization of execution::connect if they are copyable.
execution::get_completion_scheduler is used to ask a sender object for the completion scheduler for one of its signals.
-
-
The name execution::get_completion_scheduler denotes a customization point object template. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::sender, execution::get_completion_scheduler is ill-formed. If the template
-argument CPO in execution::get_completion_scheduler<CPO> is not one of execution::set_value_t, execution::set_error_t, or execution::set_done_t, execution::get_completion_scheduler<CPO> is ill-formed. Otherwise, execution::get_completion_scheduler<CPO>(s) is expression-equivalent to:
-
-
-
tag_invoke(execution::get_completion_scheduler<CPO>,as_const(s)), if this expression is well formed and satisfies execution::scheduler, and is noexcept.
-
-
Otherwise, execution::get_completion_scheduler<CPO>(s) is ill-formed.
-
-
-
If, for some sender s and customization point object CPO, execution::get_completion_scheduler<decltype(CPO)>(s) is well-formed and results in a scheduler sch, and the sender s invokes CPO(r,args...), for some receiver r which has been connected to s, with additional arguments args..., on an execution agent which does not belong to the associated execution context of sch, the behavior is undefined.
execution::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.
-
-
The name execution::schedule denotes a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::scheduler, execution::schedule is ill-formed. Otherwise, the expression execution::schedule(s) is expression-equivalent to:
-
-
-
tag_invoke(execution::schedule,s), if that expression is valid and its type satisfies execution::sender. If the function selected by tag_invoke does not return a sender whose set_value completion scheduler is equivalent to s, the program is ill-formed with no
-diagnostic required.
-
-
Otherwise, execution::schedule(s) is ill-formed.
-
-
-
9.6.4.3. Just sender factory [execution.sender.just]
-
-
-
execution::just is used to create a sender that propagates a set of values to a connected receiver.
execution::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.
-
-
The name execution::transfer_just denotes a customization point object. For some subexpressions s and vs..., let S be decltype((s)) and Vs... be decltype((vs)). If S does not satisfy execution::scheduler, or any type V in Vs does not
-satisfy <i>moveable-value</i>, execution::transfer_just(s,vs...) is ill-formed. Otherwise, execution::transfer_just(s,vs...) is expression-equivalent to:
-
-
-
tag_invoke(execution::transfer_just,s,vs...), if that expression is valid and its type satisfies execution::typed_sender. If the function selected by tag_invoke does not return a sender whose set_value completion scheduler is equivalent to s and sends
-values equivalent to vs... to a receiver connected to it, the program is ill-formed with no diagnostic required.
9.6.5.1. General [execution.sender.adaptors.general]
-
-
-
Subclause [execution.sender.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.
-
-
Most sender adaptors have two versions, an potentially eager version, and a strictly lazy version. For such sender adaptors, adaptor is the potentially eager version, and lazy_adaptor is the strictly
-lazy version.
-
-
A strictly lazy version of 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 execution::connect, and execution::start is called on the resulting operation state. This requirement applies to any function that is selected by the implementation of the sender adaptor.
-
-
Unless otherwise specified, all sender adaptors which accept a single sender 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.
-
-
Unless otherwise specified, whenever a strictly lazy 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 execution::connect. This requirements
-applies to any sender returned from a function that is selected by the implementation of a strictly lazy sender adaptor.
A pipeable sender adaptor closure object is a function object that accepts one or more sender arguments and returns a sender. For a sender adaptor closure object C and an expression S such that decltype((S)) models sender, the following
-expressions are equivalent and yield a sender:
-
C(S)
-S|C
-
-
Given an additional pipeable sender adaptor closure object D, the expression C|D is well-formed and produces another range adaptor closure object such that the following two expressions are equivalent:
-
S|C|D
-S|(C|D)
-
-
-
A pipeable sender adaptor object is a customization point object that accepts a sender as its first argument and returns a 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 the following expressions are equivalent:
In that case, adaptor(args...) is a pipeable sender adaptor closure object.
-
-
9.6.5.3. On adaptor [execution.sender.adaptors.on]
-
-
-
execution::on and execution::lazy_on are used to adapt a sender in a sender that will start the input sender on an execution agent belonging to a specific execution context.
-
-
The name execution::on denotes a customization point object. For some subexpressions sch and s, let Sch be decltype((sch)) and S be decltype((s)). If Sch does not satisfy execution::scheduler, or S does not satisfy execution::sender, execution::on is ill-formed. Otherwise, the expression execution::on(sch,s) is expression-equivalent to:
-
-
-
tag_invoke(execution::on,sch,s), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, lazy_on(sch,s).
-
-
If the function selected above does not return a sender which starts s on an execution agent of the associated execution context of sch, the program is ill-formed with no diagnostic required.
-
-
The name execution::lazy_on denotes a customization point object. For some subexpressions sch and s, let Sch be decltype((sch)) and S be decltype((s)). If Sch does not satisfy execution::scheduler, or S does not satisfy execution::sender, execution::lazy_on is ill-formed. Otherwise, the expression execution::lazy_on(sch,s) is expression-equivalent to:
-
-
-
tag_invoke(execution::lazy_on,sch,s), if that expression is valid and its type satisfies execution::sender. If the function selected above does not return a sender which starts s on an execution agent of the associated execution context of sch when
-started, the program is ill-formed with no diagnostic required.
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it results in an operation state op_state. When execution::start is called on op_state, it:
-
-
-
Constructs a receiver r:
-
-
-
When execution::set_value(r) is called, it calls execution::connect(s,out_r), which results in op_state2. It calls execution::start(op_state2). If any of these throws an exception, it calls execution::set_error on out_r,
-passing current_exception() as the second argument.
-
-
When execution::set_error(r,e) is called, it calls execution::set_error(out_r,e).
-
-
When execution::set_done(r) is called, it calls execution::set_done(out_r).
-
-
-
Calls execution::schedule(sch), which results in s3. It then calls execution::connect(s3,r), resulting in op_state3, and then it calls execution::start(op_state3). If any of these throws an exception, it catches it and calls execution::set_error(out_r,current_exception()).
-
-
-
-
Any receiver r created by an implementation of on and lazy_on shall implement the get_scheduler receiver query. The scheduler returned from the query for all such receivers should be equivalent to the sch argument passed into the on or lazy_on call.
-
-
9.6.5.4. transfer adaptor [execution.sender.adaptors.transfer]
-
-
-
execution::transfer and execution::lazy_transfer are used to adapt a sender into a sender with a different associated set_value completion scheduler. [Note: it results in a transition between different execution contexts when executed. --end note]
-
-
The name execution::transfer denotes a customization point object. For some subexpressions sch and s, let Sch be decltype((sch)) and S be decltype((s)). If Sch does not satisfy execution::scheduler, or S does not satisfy execution::sender, execution::transfer is ill-formed. Otherwise, the expression execution::transfer(s,sch) is expression-equivalent to:
-
-
-
tag_invoke(execution::transfer,get_completion_scheduler<set_value_t>(s),s,sch), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::transfer,s,sch), if that expression is valid and its type satisfies 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 execution::schedule_from(sch,s2), where s2 is a sender which sends equivalent to those sent by s, the program is ill-formed with no diagnostic required.
-
-
The name execution::lazy_transfer denotes a customization point object. For some subexpressions sch and s, let Sch be decltype((sch)) and S be decltype((s)). If Sch does not satisfy execution::scheduler, or S does not satisfy execution::sender, execution::lazy_transfer is ill-formed. Otherwise, the expression execution::lazy_transfer(s,sch) is expression-equivalent to:
-
-
-
tag_invoke(execution::lazy_transfer,get_completion_scheduler<set_value_t>(s),s,sch), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::lazy_transfer,s,sch), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, lazy_schedule_from(sch,s).
-
-
If the function selected above does not return a sender which is a result of a call to execution::lazy_schedule_from(sch,s2), where s2 is a sender which sends equivalent to those sent by s, the program is ill-formed with no diagnostic required.
-
-
Senders returned from execution::transfer and execution::lazy_transfer shall not propagate the sender queries get_completion_scheduler<CPO> to an input sender. They shall return a sender equivalent to the sch argument from those queries.
execution::schedule_from and execution::lazy_schedule_from are used to schedule work dependent on the completion of a sender onto a scheduler’s associated execution context. [Note: schedule_from and lazy_schedule_from are not meant to be used in
-user code; they are used in the implementation of transfer and lazy_transfer. -end note]
-
-
The name execution::schedule_from denotes a customization point object. For some subexpressions sch and s, let Sch be decltype((sch)) and S be decltype((s)). If Sch does not satisfy execution::scheduler, or S does not satisfy execution::typed_sender, execution::schedule_from is ill-formed. Otherwise, the expression execution::schedule_from(sch,s) is expression-equivalent to:
-
-
-
tag_invoke(execution::schedule_from,sch,s), if that expression is valid and its type satisfies execution::sender. If the function selected by tag_invoke does not return a sender which completes on an execution agent belonging to the associated
-execution context of sch and sends signals equivalent to those sent by s, the program is ill-formed with no diagnostic required.
-
-
Otherwise, lazy_schedule_from(sch,s).
-
-
-
The name execution::lazy_schedule_from denotes a customization point object. For some subexpressions sch and s, let Sch be decltype((sch)) and S be decltype((s)). If Sch does not satisfy execution::scheduler, or S does not satisfy execution::typed_sender, execution::lazy_schedule_from is ill-formed. Otherwise, the expression execution::lazy_schedule_from(sch,s) is expression-equivalent to:
-
-
-
tag_invoke(execution::lazy_schedule_from,sch,s), if that expression is valid and its type satisfies execution::sender. If the function selected by tag_invoke does not return a sender which completes on an execution agent belonging to the associated
-execution context of sch and sends signals equivalent to those sent by s, the program is ill-formed with no diagnostic required.
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r.
-
-
Calls execution::connect(s,r), which results in an operation state op_state2. If any of these throws an exception, calls execution::set_error on out_r, passing current_exception() as the second argument.
-
-
When a receiver completion-signal Signal(r,args...) is called, it constructs a receiver r2:
-
-
-
When execution::set_value(r2) is called, it calls Signal(out_r,args...).
-
-
When execution::set_error(r2,e) is called, it calls execution::set_error(out_r,e).
-
-
When execution::done(r2) is called, it calls execution::set_done(out_r).
-
-
It then calls execution::schedule(sch), resulting in a sender s3. It then calls execution::connect(s3,r2), resulting in an operation state op_state3. It then calls execution::start(op_state3). If any of these throws an exception,
-it catches it and calls execution::set_error(out_r,current_exception()).
-
-
Returns an operation state op_state that contains op_state2. When execution::start(op_state) is called, calls execution::start(op_state2).
-
-
-
-
Senders returned from execution::transfer and execution::lazy_transfer shall not propagate the sender queries get_completion_scheduler<CPO> to an input sender. They shall return a scheduler equivalent to the sch argument from those queries.
-
-
9.6.5.6. Then adaptor [execution.sender.adaptors.then]
-
-
-
execution::then and execution::lazy_then are used to attach invocables as continuation for successful completion of the input sender.
-
-
The name execution::then denotes a customization point object. For some subexpressions s and f, let S be decltype((s)). If S does not satisfy execution::sender, execution::then is ill-formed. Otherwise, the expression execution::then(s,f) is
-expression-equivalent to:
-
-
-
tag_invoke(execution::then,get_completion_scheduler<set_value_t>(s),s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::then,s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, lazy_then(s,f).
-
-
If the function selected above does not return a sender which invokes f with the result of the set_value signal of s, passing the return value as the value to any connected receivers, and propagates the other completion-signals sent by s, the program is
- ill-formed with no diagnostic required.
-
-
The name execution::lazy_then denotes a customization point object. For some subexpressions s and f, let S be decltype((s)). If S does not satisfy execution::sender, execution::lazy_then is ill-formed. Otherwise, the expression execution::lazy_then(s,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::lazy_then,get_completion_scheduler<set_value_t>(s),s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::lazy_then,s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r:
-
-
-
When execution::set_value(r,args...) is called, calls invoke(f,args...) and passes the result v to execution::set_value(out_r,v). If any of these throws an exception, it catches it and calls execution::set_error(out_r,current_exception()).
-
-
When execution::set_error(r,e) is called, calls execution::set_error(out_r,e).
-
-
When execution::set_done(r) is called, calls execution::set_done(out_r).
-
-
-
Calls execution::connect(s,r), which results in an operation state op_state2.
-
-
Returns an operation state op_state that contains op_state2. When execution::start(op_state) is called, calls execution::start(op_state2).
-
-
-
If the function selected above does not return a sender which invokes f with the result of the set_value signal of s, passing the return value as the value to any connected receivers, and propagates the other completion-signals sent by s, the program is
- ill-formed with no diagnostic required.
execution::upon_error and execution::lazy_upon_error are used to attach invocables as continuation for successful completion of the input sender.
-
-
The name execution::upon_error denotes a customization point object. For some subexpressions s and f, let S be decltype((s)). If S does not satisfy execution::sender, execution::upon_error is ill-formed. Otherwise, the expression execution::upon_error(s,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::upon_error,get_completion_scheduler<set_error_t>(s),s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::upon_error,s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, lazy_upon_error(s,f).
-
-
If the function selected above does not return a sender which invokes f with the result of the set_error signal of s, passing the return value as the value to any connected receivers, and propagates the other completion-signals sent by s, the program is
- ill-formed with no diagnostic required.
-
-
The name execution::lazy_upon_error denotes a customization point object. For some subexpressions s and f, let S be decltype((s)). If S does not satisfy execution::sender, execution::lazy_upon_error is ill-formed. Otherwise, the expression execution::lazy_upon_error(s,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::lazy_upon_error,get_completion_scheduler<set_error_t>(s),s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::lazy_upon_error,s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r:
-
-
-
When execution::set_value(r,args...) is called, calls execution::set_value(out_r,args...).
-
-
When execution::set_error(r,e) is called, calls invoke(f,e) and passes the result v to execution::set_value(out_r,v). If any of these throws an exception, it catches it and calls execution::set_error(out_r,current_exception()).
-
-
When execution::set_done(r) is called, calls execution::set_done(out_r).
-
-
-
Calls execution::connect(s,r), which results in an operation state op_state2.
-
-
Returns an operation state op_state that contains op_state2. When execution::start(op_state) is called, calls execution::start(op_state2).
-
-
-
If the function selected above does not return a sender which invokes f with the result of the set_error signal of s, passing the return value as the value to any connected receivers, and propagates the other completion-signals sent by s, the program is
- ill-formed with no diagnostic required.
execution::upon_done and execution::lazy_upon_done are used to attach invocables as continuation for successful completion of the input sender.
-
-
The name execution::upon_done denotes a customization point object. For some subexpressions s and f, let S be decltype((s)). If S does not satisfy execution::sender, execution::upon_done is ill-formed. Otherwise, the expression execution::upon_done(s,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::upon_done,get_completion_scheduler<set_done_t>(s),s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::upon_done,s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, lazy_upon_done(s,f).
-
-
If the function selected above does not return a sender which invokes f when the set_done signal of s is called, passing the return value as the value to any connected receivers, and propagates the other completion-signals sent by s, the program is
- ill-formed with no diagnostic required.
-
-
The name execution::lazy_upon_done denotes a customization point object. For some subexpressions s and f, let S be decltype((s)). If S does not satisfy execution::sender, execution::lazy_upon_done is ill-formed. Otherwise, the expression execution::lazy_upon_done(s,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::lazy_upon_done,get_completion_scheduler<set_done_t>(s),s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::lazy_upon_done,s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r:
-
-
-
When execution::set_value(r,args...) is called, calls execution::set_value(out_r,args...).
-
-
When execution::set_error(r,e) is called, calls execution::set_error(out_r,e).
-
-
When execution::set_done(r) is called, calls invoke(f) and passes the result v to execution::set_value(out_r,v). If any of these throws an exception, it catches it and calls execution::set_error(out_r,current_exception()).
-
-
-
Calls execution::connect(s,r), which results in an operation state op_state2.
-
-
Returns an operation state op_state that contains op_state2. When execution::start(op_state) is called, calls execution::start(op_state2).
-
-
-
If the function selected above does not return a sender which invokes f when the set_done signal of s is called, passing the return value as the value to any connected receivers, and propagates the other completion-signals sent by s, the program is
- ill-formed with no diagnostic required.
execution::let_value and execution::lazy_let_value are used to insert continuations creating more work dependent on the results of their input senders into a sender chain.
-
-
The name execution::let_value denotes a customization point object. For some subexpressions s and f, let S be decltype((s)). If S does not satisfy execution::sender, execution::let_value is ill-formed. Otherwise, the expression execution::let_value(s,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::let_value,get_completion_scheduler<set_value_t>(s),s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::let_value,s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, lazy_let_value(s,f).
-
-
If the function selected above does not return a sender which invokes f when set_value is called, and making its completion dependent on the completion of a sender returned by f, and propagates the other completion-signals sent by s, the program is
- ill-formed with no diagnostic required.
-
-
The name execution::lazy_let_value denotes a customization point object. For some subexpressions s and f, let S be decltype((s)). If S does not satisfy execution::sender, execution::lazy_let_value is ill-formed. Otherwise, the expression execution::lazy_let_value(s,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::lazy_let_value,get_completion_scheduler<set_value_t>(s),s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::lazy_let_value,s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r.
-
-
-
When execution::set_value(r,args...) is called, copies args... into op_state2 as args2..., then calls invoke(f,args2...), resulting in a sender s3. It then calls execution::connect(s3,out_r), resulting in an operation state op_state3. op_state3 is saved as a part of op_state2. It then calls execution::start(op_state3). If any of these throws an exception, it catches it and calls execution::set_error(out_r,current_exception()).
-
-
When execution::set_error(r,e) is called, calls execution::set_error(out_r,e).
-
-
When execution::set_done(r,e) is called, calls execution::set_done(out_r).
-
-
-
Calls execution::connect(s,r), which results in an operation state op_state2.
-
-
Returns an operation state op_state that contains op_state2. When execution::start(op_state) is called, calls execution::start(op_state2).
-
-
-
If the function selected above does not return a sender which invokes f when set_value is called, and making its completion dependent on the completion of a sender returned by f, and propagates the other completion-signals sent by s, the program is
- ill-formed with no diagnostic required.
execution::let_error and execution::lazy_let_error are used to insert continuations creating more work dependent on the results of their input senders into a sender chain.
-
-
The name execution::let_error denotes a customization point object. For some subexpressions s and f, let S be decltype((s)). If S does not satisfy execution::sender, execution::let_error is ill-formed. Otherwise, the expression execution::let_error(s,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::let_error,get_completion_scheduler<set_error_t>(s),s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::let_error,s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, lazy_let_error(s,f).
-
-
If the function selected above does not return a sender which invokes f when set_error is called, and making its completion dependent on the completion of a sender returned by f, and propagates the other completion-signals sent by s, the program is
- ill-formed with no diagnostic required.
-
-
The name execution::lazy_let_error denotes a customization point object. For some subexpressions s and f, let S be decltype((s)). If S does not satisfy execution::sender, execution::lazy_let_error is ill-formed. Otherwise, the expression execution::lazy_let_error(s,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::lazy_let_error,get_completion_scheduler<set_error_t>(s),s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::lazy_let_error,s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r.
-
-
-
When execution::set_value(r,args...) is called, calls execution::set_value(out_r,args...).
-
-
When execution::set_error(r,e) is called, copies e into op_statee2 as e, then calls invoke(f,e), resulting in a sender s3. It then calls execution::connect(s3,out_r), resulting in an operation state op_state3. op_state3 is saved
-as a part of op_state2. It then calls execution::start(op_state3). If any of these throws an exception, it catches it and calls execution::set_error(out_r,current_exception()).
-
-
When execution::set_done(r,e) is called, calls execution::set_done(out_r).
-
-
-
Calls execution::connect(s,r), which results in an operation state op_state2.
-
-
Returns an operation state op_state that contains op_state2. When execution::start(op_state) is called, calls execution::start(op_state2).
-
-
-
If the function selected above does not return a sender which invokes f when set_error is called, and making its completion dependent on the completion of a sender returned by f, and propagates the other completion-signals sent by s, the program is
- ill-formed with no diagnostic required.
execution::let_done and execution::lazy_let_done are used to insert continuations creating more work dependent on the results of their input senders into a sender chain.
-
-
The name execution::let_done denotes a customization point object. For some subexpressions s and f, let S be decltype((s)). If S does not satisfy execution::sender, execution::let_done is ill-formed. Otherwise, the expression execution::let_done(s,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::let_done,get_completion_scheduler<set_done_t>(s),s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::let_done,s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, lazy_let_done(s,f).
-
-
If the function selected above does not return a sender which invokes f when set_done is called, and making its completion dependent on the completion of a sender returned by f, and propagates the other completion-signals sent by s, the program is
- ill-formed with no diagnostic required.
-
-
The name execution::lazy_let_done denotes a customization point object. For some subexpressions s and f, let S be decltype((s)). If S does not satisfy execution::sender, execution::lazy_let_done is ill-formed. Otherwise, the expression execution::lazy_let_done(s,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::lazy_let_done,get_completion_scheduler<set_done_t>(s),s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::lazy_let_done,s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r.
-
-
-
When execution::set_value(r,args...) is called, calls execution::set_value(out_r,args...).
-
-
When execution::set_error(r,e) is called, calls execution::set_error(out_r,e).
-
-
When execution::set_done(r) is called, calls invoke(f), resulting in a sender s3. It then calls execution::connect(s3,out_r), resulting in an operation state op_state3. op_state3 is saved as a part of op_state2.
-It then calls execution::start(op_state3). If any of these throws an exception, it catches it and calls execution::set_error(out_r,current_exception()).
-
-
-
Calls execution::connect(s,r). which results in an operation state op_state2.
-
-
Returns an operation state op_state that contains op_state2. When execution::start(op_state) is called, calls execution::start(op_state2).
-
-
-
If the function selected above does not return a sender which invokes f when set_done is called, and making its completion dependent on the completion of a sender returned by f, and propagates the other completion-signals sent by s, the program is
- ill-formed with no diagnostic required.
execution::bulk and execution::lazy_bulk are used to run a task repeatedly for every index in an index space.
-
-
The name execution::bulk denotes a customization point object. For some subexpressions s, shape, and f, let S be decltype((s)), Shape be decltype((shape)), and F be decltype((f)). If S does not satisfy execution::sender or Shape does not
-satisfy integral, execution::bulk is ill-formed. Otherwise, the expression execution::bulk(s,shape,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::bulk,get_completion_scheduler<set_value_t>(s),s,shape,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::bulk,s,shape,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, lazy_bulk(s,shape,f).
-
-
-
The name execution::lazy_bulk denotes a customization point object. For some subexpressions s, shape, and f, let S be decltype((s)), Shape be decltype((shape)), and F be decltype((f)). If S does not satisfy execution::sender or Shape does not satisfy integral, execution::bulk is ill-formed. Otherwise, the expression execution::bulk(s,shape,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::bulk,get_completion_scheduler<set_value_t>(s),s,shape,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::bulk,s,shape,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r:
-
-
-
When execution::set_value(r,args...) is called, calls f(i,args...) for each i of type Shape from 0 to shape, then calls execution::set_value(out_r,args...). If any of these throws an exception, it catches it and calls execution::set_error(out_r,current_exception()).
-
-
When execution::set_error(r,e) is called, calls execution::set_error(out_r,e).
-
-
When execution::set_done(r,e) is called, calls execution::set_done(out_r,e).
-
-
-
Calls execution::connect(s,r), which results in an operation state op_state2.
-
-
Returns an operation state op_state that contains op_state2. When execution::start(op_state) is called, calls execution::start(op_state2).
-
-
-
If the function selected above does not return a sender which invokes f(i,args...) for each i of type Shape from 0 to shape when the input sender sends values args..., 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.
execution::split and execution::lazy_split are used to adapt an arbitrary sender into a sender that can be connected multiple times.
-
-
The name execution::split denotes a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::typed_sender, execution::split is ill-formed. Otherwise, the expression execution::split(s) is
-expression-equivalent to:
-
-
-
tag_invoke(execution::split,get_completion_scheduler<set_value_t>(s),s), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::split,s), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, lazy_split(s).
-
-
If the function selected above does not return a sender which sends references to values sent by s, propagating the other channels, the program is ill-formed with no diagnostic required.
-
-
The name execution::lazy_split denotes a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::typed_sender, execution::lazy_split is ill-formed. Otherwise, the expression execution::lazy_split(s) is expression-equivalent to:
-
-
-
tag_invoke(execution::lazy_split,get_completion_scheduler<set_value_t>(s),s), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::lazy_split,s), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, constructs a sender s2, which:
-
-
-
Creates an object sh_state. The lifetime of sh_state shall last for at least as long as the lifetime of the last operation state object returned from execution::connect(s,some_r) for some receiver some_r.
-
-
Constructs a receiver r:
-
-
-
When execution::set_value(r,args...) is called, saves the expressions args... as subobjects of sh_state.
-
-
When execution::set_error(r,e) is called, saves the expression e as a subobject of sh_state.
-
-
When execution::set_done(r) is called, saves this fact in sh_state.
-
-
-
Calls execution::connect(s,r), resulting in an operation state op_state2. op_state2 is saved as a subobject of sh_state.
-
-
When s2 is connected with a receiver out_r, it returns an operation state object op_state. When execution::start(op_state) is called, it calls execution::start(op_state2), if this is the first time this expression would be evaluated. When both execution::start(op_state) and Signal(r,args...) have been called, calls Signal(out_r,args2...), where args2... is a pack of lvalues referencing the subobjects of sh_state that have been saved by the
-original call to Signal(r,args...).
-
-
-
If the function selected above does not return a sender which sends references to values sent by s, propagating the other channels, the program is ill-formed with no diagnostic required.
execution::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_with_variant 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.
-
-
The name execution::when_all denotes a customization point object. For some subexpressions s..., let S be decltype((s)). If any type Si in S... does not satisfy execution::typed_sender, or the number of the arguments sender_traits<Si>::value_types passes into the Variant template parameter is not 1, execution::when_all is ill-formed. Otherwise, the expression execution::when_all(s...) is expression-equivalent to:
-
-
-
tag_invoke(execution::when_all,s...), if that expression is valid and its type satisfies execution::sender. If the function selected by tag_invoke does not return a sender which sends a concatenation of values sent by s... when they all complete with set_value, the program is ill-formed with no diagnostic required.
-
-
Otherwise, constructs a sender s. When s is connected with some receiver out_r, it:
-
-
-
For each sender si in s..., constructs a receiver ri:
-
-
-
If execution::set_value(ri,ti...) is called for every ri, execution::set_value(out_r,t0...,t1...,...,tn...) is called, where n is sizeof...(s)-1.
-
-
Otherwise, if execution::set_error(ri,e) is called for any ri, execution::set_error(out_r,e) is called.
-
-
Otherwise, if execution::set_done(ri) is called for any ri, execution::set_done(out_r) is called.
-
-
-
For each sender si in s..., calls execution::connect(si,ri), resulting in operation states op_statei.
-
-
Returns an operation state op_state that contains each operation state op_statei. When execution::start(op_state) is called, calls execution::start(op_statei) for each op_statei.
-
-
-
-
The name execution::when_all_with_variant denotes a customization point object. For some subexpressions s..., let S be decltype((s)). If any type Si in S... does not satisfy execution::typed_sender, execution::when_all_with_variant is ill-formed. Otherwise, the expression execution::when_all_with_variant(s...) is expression-equivalent to:
-
-
-
tag_invoke(execution::when_all_with_variant,s...), if that expression is valid and its type satisfies execution::sender. If the function selected by tag_invoke does not return a sender which sends the types into-variant-type<S>... when they all complete with set_value, the program is ill-formed with no diagnostic required.
Adaptors defined in this subclause are strictly lazy.
-
-
Senders returned from adaptors defined in this subclause shall not expose the sender queries get_completion_scheduler<CPO>.
-
-
tag_invoke expressions used in the definitions of the sender adaptors in this subclause shall not consider member functions of their first non-tag arguments.
execution::transfer_when_all and execution::lazy_transfer_when_all are 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_with_variant and execution::lazy_transfer_when_all_with_variant are 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 adaptor. --end note]
-
-
The name execution::transfer_when_all denotes a customization point object. For some subexpressions sch and s..., let Sch be decltype(sch) and S be decltype((s)). If Sch does not satisfy scheduler, or any type Si in S... does not satisfy execution::typed_sender, or the number of the arguments sender_traits<Si>::value_types passes into the Variant template parameter is not 1 execution::transfer_when_all is ill-formed.
-Otherwise, the expression execution::transfer_when_all(sch,s...) is expression-equivalent to:
-
-
-
tag_invoke(execution::transfer_when_all,sch,s...), if that expression is valid and its type satisfies execution::sender. If the function selected by tag_invoke does not return a sender which sends a concatenation of values sent by s... when they
-all complete with set_value, 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 of sch, the program is ill-formed with no diagnostic required.
-
-
Otherwise, transfer(when_all(s...),sch).
-
-
-
The name execution::lazy_transfer_when_all denotes a customization point object. For some subexpressions sch and s..., let Sch be decltype(sch) and S be decltype((s)). If Sch does not satisfy scheduler, or any type Si in S... does not satisfy execution::typed_sender, or the number of the arguments sender_traits<Si>::value_types passes into the Variant template parameter is not 1, execution::lazy_transfer_when_all is ill-formed. Otherwise, the expression execution::lazy_transfer_when_all(sch,s...) is expression-equivalent to:
-
-
-
tag_invoke(execution::lazy_transfer_when_all,sch,s...), if that expression is valid and its type satisfies execution::sender. If the function selected by tag_invoke does not return a sender which sends a concatenation of values sent by s... when
-they all complete with set_value, 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 of sch, the program is ill-formed with no diagnostic
-required.
-
-
Otherwise, lazy_transfer(when_all(s...),sch).
-
-
-
The name execution::transfer_when_all_with_variant denotes a customization point object. For some subexpressions s..., let S be decltype((s)). If any type Si in S... does not satisfy execution::typed_sender, execution::transfer_when_all_with_variant is ill-formed. Otherwise, the expression execution::transfer_when_all_with_variant(s...) is expression-equivalent to:
-
-
-
tag_invoke(execution::transfer_when_all_with_variant,s...), if that expression is valid and its type satisfies execution::sender. If the function selected by tag_invoke does not return a sender which sends the types into-variant-type<S>... when they all complete with set_value, the program is ill-formed with no diagnostic required.
The name execution::lazy_transfer_when_all_with_variant denotes a customization point object. For some subexpressions s..., let S be decltype((s)). If any type Si in S... does not satisfy execution::typed_sender, execution::lazy_transfer_when_all_with_variant is ill-formed. Otherwise, the expression execution::lazy_transfer_when_all_with_variant(s...) is expression-equivalent to:
-
-
-
tag_invoke(execution::lazy_transfer_when_all_with_variant,s...), if that expression is valid and its type satisfies execution::sender. If the function selected by tag_invoke does not return a sender which sends the types into-variant-type<S>... when they all complete with set_value, the program is ill-formed with no diagnostic required.
Senders returned from execution::transfer_when_all and execution::lazy_transfer_when_all shall not propagate the sender queries get_completion_scheduler<CPO> to input senders. They shall return a scheduler equivalent to the sch argument from
-those queries.
execution::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.
-
-
The template into-variant-type is used to compute the type sent by a sender returned from execution::into_variant.
execution::unschedule can be used to remove the information about completion schedulers from a sender, in effect removing any scheduler-specific customizations of sender algorithms from that sender.
Effects: If S provides none of the get_completion_scheduler<CPO> sender queries, returns std::forward<S>(s). Otherwise, returns a sender adaptor s2 containing a subobject of type remove_cvref_t<S>, initialized with std::forward<S>(s). s2 propagates all customizations, except for get_completion_scheduler<CPO>, to s.
-
-
Remarks: The expression in the noexcept-specifier is equivalent to
execution::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.
-
-
The name execution::ensure_started denotes a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::typed_sender, execution::ensure_started is ill-formed. Otherwise, the expression execution::ensure_started(s) is expression-equivalent to:
-
-
-
tag_invoke(execution::ensure_started,get_completion_scheduler<set_value_t>(s),s), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::ensure_started,s), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise:
-
-
-
Constructs a receiver r.
-
-
Calls execution::connect(s,r), resulting in operation state op_state, and then calls execution::start(op_state). If any of these throws an exception, it catches it and calls execution::set_error(r,current_exception()).
-
-
Constructs a sender s2. When s2 is connected with some receiver out_r, it results in an operation state op_state2. Once both execution::start(op_state2) and one of the receiver completion-signals has been called on r:
-
-
-
If execution::set_value(r,ts...) has been called, calls execution::set_value(out_r,ts...).
-
-
If execution::set_error(r,e) has been called, calls execution::set_error(out_r,e).
-
-
If execution::set_done(r) has been called, calls execution::set_done(out_r).
-
-
-
-
If the function selected above does not eagerly start the sender s and return a sender which propagates the signals sent by s once started, the program is ill-formed with no diagnostic required.
execution::start_detached is used to eagerly start a sender without the caller needing to manage the lifetimes of any objects.
-
-
The name execution::start_detached denotes a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::sender, execution::start_detached is ill-formed. Otherwise, the expression execution::start_detached(s) is expression-equivalent to:
-
-
-
tag_invoke(execution::start_detached,execution::get_completion_scheduler<execution::set_value_t>(s),s), if that expression is valid and its type is void.
-
-
Otherwise, tag_invoke(execution::start_detached,s), if that expression is valid and its type is void.
-
-
Otherwise:
-
-
-
Constructs a receiver r:
-
-
-
When set_value(r,ts...) is called, it does nothing.
-
-
When set_error(r,e) is called, it calls std::terminate().
-
-
When set_done(r) is called, it does nothing.
-
-
-
Calls execution::connect(s,r), resulting in an operation state op_state, then calls execution::start(op_state).
-
-
-
If the function selected above does not eagerly start the sender s after connecting it with a receiver which ignores the set_value and set_done signals and calls std::terminate() on the set_error signal, the program is ill-formed with no diagnostic
- required.
this_thread::sync_wait and this_thread::sync_wait_with_variant 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.
-
-
The templates sync-wait-type and sync-wait-with-variant-type are used to determine the return types of this_thread::sync_wait and this_thread::sync_wait_with_variant.
The name this_thread::sync_wait denotes a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::typed_sender, or the number of the arguments sender_traits<S>::value_types passes into the Variant template parameter is not 1, this_thread::sync_wait is ill-formed. Otherwise, this_thread::sync_wait is expression-equivalent to:
-
-
-
tag_invoke(this_thread::sync_wait,execution::get_completion_scheduler<execution::set_value_t>(s),s), if this expression is valid and its type is sync-wait-type<S>.
-
-
Otherwise, tag_invoke(this_thread::sync_wait,s), if this expression is valid and its type is sync-wait-type<S>.
-
-
Otherwise:
-
-
-
Constructs a receiver r.
-
-
Calls execution::connect(s,r), resulting in an operation state op_state, then calls execution::start(op_state).
-
-
Blocks the current thread until a receiver completion-signal of r is called. When it is:
-
-
-
If execution::set_value(r,ts...) has been called, returns sync-wait-type<S>(make_tuple(ts...))>.
-
-
If execution::set_error(r,e...) has been called, if remove_cvref_t(decltype(e)) is exception_ptr, calls std::rethrow_exception(e). Otherwise, throws e.
-
-
If execution::set_done(r) has been called, returns sync-wait-type<S(nullopt)>.
-
-
-
-
-
The name this_thread::sync_wait_with_variant denotes a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::typed_sender, this_thread::sync_wait_with_variant is ill-formed. Otherwise, this_thread::sync_wait_with_variant is expression-equivalent to:
-
-
-
tag_invoke(this_thread::sync_wait_with_variant,execution::get_completion_scheduler<execution::set_value_t>(s),s), if this expression is valid and its type is sync-wait-with-variant-type<S>.
-
-
Otherwise, tag_invoke(this_thread::sync_wait_with_variant,s), if this expression is valid and its type is sync-wait-with-variant-type<S>.
Any receiver r created by an implementation of sync_wait and sync_wait_with_variant shall implement the get_scheduler 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.
-
-
9.7. One-way execution [execution.execute]
-
-
-
execution::execute is used to create fire-and-forget tasks on a specified scheduler.
-
-
The name execution::execute denotes a customization point object. For some subexpressions sch and f, let Sch be decltype((sch)) and F be decltype((f)). If Sch does not satisfy execution::scheduler or F does not satisfy invocable<>, execution::execute is ill-formed. Otherwise, execution::execute is expression-equivalent to:
-
-
-
tag_invoke(execution::execute,sch,f), if that expression is valid and its type is void. If the function selected by tag_invoke does not invoke the function f on an execution agent belonging to the associated execution context of sch, or if it
-does not call std::terminate() if an error occurs after control is returned to the caller, the program is ill-formed with no diagnostic required.
ISO/IEC JTC1/SC22/WG21 14882: Programming Language — C++
-
Audience:
-
SG1, LEWG
-
-
-
-
-
-
-
-
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 (std::atomic, std::mutex, std::counting_semaphore, etc) and lower level building blocks (std::thread, etc), we lack a Standard vocabulary and framework for asynchrony and parallelism that C++ programmers desperately need. std::async/std::future/std::promise, 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 both lazy and eager execution in a way that does not compromise the efficiency of either and allows users to write code that is agnostic to eagerness.
-
-
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.
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.12.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.13.2 execution::then is a sender adaptor that takes an input sender and a std::invocable, and calls the std::invocable on the signal sent by the input sender. The sender returned by then sends the result of that invocation. In this case, the input sender came from schedule, so its void, meaning it won’t send us a value, so our std::invocable takes no parameters. But we return an int, which will be sent to the next recipient.
-
-
Now, we add another operation to the chain, again using § 4.13.2 execution::then. This time, we get sent a value - the int from the previous step. We add 42 to it, and then return the result.
-
-
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.14.2 this_thread::sync_wait, which will either return a std::optional<std::tuple<...>> with the value sent by the last sender, or an empty std::optional if the last sender sent a done signal, or it throws an exception if the last sender sent an error.
This example builds an asynchronous computation of an inclusive scan:
-
-
-
It scans a sequence of doubles (represented as the std::span<constdouble>input) and stores the result in another sequence of doubles (represented as std::span<double>output).
-
-
It takes a scheduler, which specifies what execution context the scan should be launched on.
-
-
It also takes a tile_count parameter that controls the number of execution agents that will be spawned.
-
-
First we need to allocate temporary storage needed for the algorithm, which we’ll do with a std::vector, partials. We need one double of temporary storage for each execution agent we create.
-
-
Next we’ll create our initial sender with § 4.12.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 sch, which means the next item in the chain will use sch.
-
-
Senders and sender adaptors support composition via operator|, similar to C++ ranges. We’ll use operator| to attach the next piece of work, which will spawn tile_count execution agents using § 4.13.7 execution::bulk (see § 4.11 Most sender adaptors are pipeable for details).
-
-
Each agent will call a std::invocable, passing it two arguments. The first is the agent’s index (i) in the § 4.13.7 execution::bulk operation, in this case a unique integer in [0,tile_count). The second argument is what the input sender sent - the temporary storage.
-
-
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 std::inclusive_scan over our elements. We store the scan result for our last element, which is the sum of all of our elements, in our temporary storage partials.
-
-
After all computation in that initial § 4.13.7 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 partials. We’ll do that with a single execution agent which will execute after the § 4.13.7 execution::bulk completes. We create that execution agent with § 4.13.2 execution::then.
-
-
§ 4.13.2 execution::then takes an input sender and an std::invocable and calls the std::invocable with the value sent by the input sender. Inside our std::invocable, we call std::inclusive_scan on partials, which the input senders will send to us.
-
-
Then we return partials, which the next phase will need.
-
-
Finally we do another § 4.13.7 execution::bulk of the same shape as before. In this § 4.13.7 execution::bulk, we will use the scanned values in partials to integrate the sums from other tiles into our elements, completing the inclusive scan.
-
-
async_inclusive_scan returns a sender that sends the output std::span<double>. A consumer of the algorithm can chain additional work that uses the scan result. At the point at which async_inclusive_scan returns, the computation may not have completed. In fact, it may not have even started.
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:
-
-
-
async_read 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 a std::span<std::byte>, and a handle to an I/O context. It will asynchronously read into the input buffer, up to the size of the std::span. It returns a sender which will send the number of bytes read once the read completes.
-
-
async_read_array 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 a dynamic_buffer object that owns the data that was sent.
-
-
dynamic_buffer is an aggregate struct that contains a std::unique_ptr<std::byte[]> and a size.
-
-
The first thing we do inside of async_read_array is create a sender that will send a new, empty dynamic_array object using § 4.12.2 execution::just. We can attach more work to the pipeline using operator| composition (see § 4.11 Most sender adaptors are pipeable for details).
-
-
We need the lifetime of this dynamic_array object to last for the entire pipeline. So, we use let_value, which takes an input sender and a std::invocable that must return a sender itself (see § 4.13.4 execution::let_* for details). let_value sends the value from the input sender to the std::invocable. Critically, the lifetime of the sent object will last until the sender returned by the std::invocable completes.
-
-
Inside of the let_valuestd::invocable, we have the rest of our logic. First, we want to initiate an async_read of the buffer size. To do that, we need to send a std::span pointing to buf.size. We can do that with § 4.12.2 execution::just.
Next, we pipe a std::invocable that will be invoked after the async_read completes using § 4.13.2 execution::then.
-
-
That std::invocable gets sent the number of bytes read.
-
-
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 std::span<std::byte> to the storage for the data from the std::invocable. This will be sent to the next recipient in the pipeline.
-
-
And that recipient will be another async_read, which will read the data.
-
-
Once the data has been read, in another § 4.13.2 execution::then, we confirm that we read the right number of bytes.
-
-
Finally, we move out of and return our dynamic_buffer object. It will get sent by the sender returned by async_read_array. We can attach more things to that sender to use the data in the buffer.
-
-
1.4. 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.5. Design changes from P0443
-
-
-
The executor concept has been removed and all of its proposed functionality is now based on schedulers and senders, as per SG1 direction.
-
-
Properties are not included in this paper. We see them as a possible future extension, if the committee gets more comfortable with them.
-
-
Users now have a choice between using a strictly lazy vs a possibly eager version of most sender algorithms.
-
-
Senders now advertise what scheduler, if any, their evaluation will complete on.
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.6. Prior art
-
This proposal builds upon and learns from years of prior art with asynchronous and parallel programming frameworks in C++.
-
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.
-
Coroutines suffer many of the same problems, but can avoid synchronizing when chaining dependent work because they typically start suspended. In many cases, coroutine frames require unavoidable dynamic allocation. Consequently, coroutines in embedded or heterogeneous environments require great attention to detail. Nor are coroutines good candidates for cancellation because the early and safe termination of coroutines requires unsatisfying solutions. On the one hand, exceptions are inefficient and disallowed in many environments. Alternatively, clumsy ad-hoc mechanisms, whereby co_yield returns a status code, hinder correctness. See [P1662R0] for a complete discussion.
-
Callbacks are the 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, but the lack of a standard obstructs generic design. Additionally, few of these possibilities accommodate cancellation signals when the user requests upstream work to stop and clean up.
-
1.7. Field experience
-
This proposal draws heavily from our field experience with libunifex, Thrust, and Agency. It is also inspired by the needs of countless other C++ frameworks for asynchrony, parallelism, and concurrency, including:
Before this proposal is approved, we will present a new implementation of this proposal written from the specification and intended as a contribution to libc++. This implementation will demonstrate the viability of the design across the use cases and execution contexts that the committee has identified as essential.
-
2. Revision history
-
2.1. 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 unschedule adaptor.
-
-
Various fixes of typos and bugs.
-
-
2.2. 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]: std::execution; however, for brevity, the std:: part of this name is omitted. When you see execution::foo, treat that as std::execution::foo.
-
-
Universal references and explicit calls to std::move/std::forward are omitted in code samples and signatures for simplicity; assume universal references and perfect forwarding unless stated otherwise.
-
-
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 std::invocable 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 std::invocable 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, schedule, 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::schedulerautosch=get_thread_pool().scheduler();
-execution::senderautosnd=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 execution::schedule API.
-
Future papers will propose additional scheduler concepts that extend scheduler to add other capabilities. For example:
-
-
-
A time_scheduler concept that extends scheduler to support time-based scheduling.
-Such a concept might provide access to schedule_after(sched,duration), schedule_at(sched,time_point) and now(sched) APIs.
-
-
Concepts that extend scheduler to support opening, reading and writing files asynchronously.
-
-
Concepts that extend scheduler to support connecting, sending data and receiving data over the network asynchronously.
-
-
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.
execution::schedulerautosch=get_thread_pool().scheduler();
-execution::senderautosnd=execution::schedule(sch);
-execution::senderautocont=execution::then(snd,[]{
- std::fstreamfile{"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 execution::connect) senders and return a sender.
-
-
sender consumers, algorithms that take (and potentially execution::connect) senders and do not return a sender.
-
-
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 std::thread (which they aren’t).
-
-
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 on 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
-
get_completion_scheduler is a query that retrieves the completion scheduler for a specific completion signal from a sender.
-Calling get_completion_scheduler 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::schedulerautocpu_sched=new_thread_scheduler{};
-execution::schedulerautogpu_sched=cuda::scheduler();
-
-execution::senderautosnd0=execution::schedule(cpu_sched);
-execution::schedulerautocompletion_sch0=
- execution::get_completion_scheduler<execution::set_value_t>(snd0);
-// completion_sch0 is equivalent to cpu_sched
-
-execution::senderautosnd1=execution::then(snd0,[]{
- std::cout<<"I am running on cpu_sched!\n";
-});
-execution::schedulerautocompletion_sch1=
- execution::get_completion_scheduler<execution::set_value_t>(snd1);
-// completion_sch1 is equivalent to cpu_sched
-
-execution::senderautosnd2=execution::transfer(snd1,gpu_sched);
-execution::senderautosnd3=execution::then(snd2,[]{
- std::cout<<"I am running on gpu_sched!\n";
-});
-execution::schedulerautocompletion_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 execution::schedule, 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 execution::transfer sender adaptor performs a transition from one execution context to another:
-
execution::schedulerautosch1=...;
-execution::schedulerautosch2=...;
-
-execution::senderautosnd1=execution::schedule(sch1);
-execution::senderautothen1=execution::then(snd1,[]{
- std::cout<<"I am running on sch1!\n";
-});
-
-execution::senderautosnd2=execution::transfer(then1,sch2);
-execution::senderautothen2=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 std::unique_ptr that it will be transferring ownership of to the
-operation-state returned by a call to execution::connect so that the operation has access to
-this resource. In such a sender, calling execution::connect 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 execution::connect only has overloads for an rvalue-qualified sender. Callers must pass the sender
-as an rvalue to the call to execution::connect, indicating that the call consumes the sender.
-
A multi-shot sender can be connected to multiple receivers and can be launched multiple
-times. Mult-shot senders customise execution::connect to accept an lvalue reference to the
-sender. Callers can indicate that they want the sender to remain valid after the call to execution::connect by passing an lvalue reference to the sender to call these overloads. Multi-shot senders should also define
-overloads of execution::connect that accept rvalue-qualified enders 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 execution::connect.
-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 execution::connect. 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 execution::connect on the input sender, such as in this_thread::sync_wait or execution::start_detached.
-
Some multi-use sender algorithms may require that an input sender be copy-constructible but will only call execution::connect 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 execution::connect 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 split sender adaptor facilitates connecting to a sender multiple times, regardless of whether it is single-shot or multi-shot:
-
autosome_algorithm(execution::senderauto&&input){
- execution::senderautomulti_shot=split(input);
- // "multi_shot" is guaranteed to be multi-shot,
- // regardless of whether "input" was multi-shot or not
-
- returnwhen_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.
-
when_all 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. when_all returns a sender that also does not have an associated scheduler.
-
transfer_when_all 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 when_all. You can think of it as a composition of transfer(when_all(inputs...),scheduler), but one that allows for better efficiency through customization.
-
4.10. 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, get_forward_progress_guarantee, which returns one of the enumerators of a new enum type, forward_progress_guarantee. Each enumerator of forward_progress_guarantee corresponds to one of the aforementioned
-guarantees.
-
4.11. Most sender adaptors are pipeable
-
To facilitate an intuitive syntax for composition, most sender adaptors are pipeable; they can be composed (piped) together with operator|.
-This mechanism is similar to the operator| 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.
-
a|b will pass the sender a as the first argument to the pipeable sender adaptor b. Pipeable sender adaptors support partial application of the parameters after the first. For example, all of the following are equivalent:
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:
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.
-
-
-
execution::when_all and execution::when_all_with_variant: 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::on and execution::lazy_on: 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 to 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.12. User-facing sender factories
-
A sender factory is an algorithm that takes no senders as parameters and returns a sender.
execution::schedulerautosch1=get_system_thread_pool().scheduler();
-
-execution::senderautosnd1=execution::schedule(sch1);
-// snd1 describes the creation of a new task on the system thread pool
-
Returns a sender with no completion schedulers, which sends the provided values. If a provided value is an lvalue reference, a copy is made inside the returned sender and a non-const lvalue reference to the copy is sent. If the provided value is an rvalue reference, it is moved into the returned sender and an rvalue reference to it is sent.
Returns a sender whose value completion scheduler is the provided scheduler, which sends the provided values in the same manner as just.
-
execution::senderautovals=execution::transfer_just(
- get_system_thread_pool().scheduler(),
- 1,2,3
-);
-execution::senderautosnd=execution::then(pred,[](auto...args){
- std::print(args..);
-});
-// when snd is executed, it will print "123"
-
-
This adaptor is included as it greatly simplifies lifting values into senders.
-
4.13. User-facing sender adaptors
-
A sender adaptor is an algorithm that takes one or more senders, which it may execution::connect, as parameters, and returns a sender, whose completion is related to the sender arguments it has received.
-
Many sender adaptors come in two versions: a strictly lazy one, which is never allowed to submit any work for execution prior to the returned sender being started later on, and a potentially eager one, which is allowed to submit work prior to
-the returned sender being started. Sender consumers such as § 4.13.11 execution::ensure_started, § 4.14.1 execution::start_detached, and § 4.14.2 this_thread::sync_wait start senders; the implementations of non-lazy versions of the sender adaptors are allowed,
-but not guaranteed, to start senders.
-
The strictly lazy versions of the adaptors below (that is, all the versions whose names start with lazy_) are guaranteed to not start any input senders passed into them.
execution::schedulerautocpu_sched=get_system_thread_pool().scheduler();
-execution::schedulerautogpu_sched=cuda::scheduler();
-
-execution::senderautocpu_task=execution::schedule(cpu_sched);
-// cpu_task describes the creation of a new task on the system thread pool
-
-execution::senderautogpu_task=execution::transfer(cpu_task,gpu_sched);
-// gpu_task describes the transition of the task graph described by cpu_task to the gpu
-
then 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.
-
lazy_then is guaranteed to not begin executing function until the returned sender is started.
-
execution::senderautoinput=get_input();
-execution::senderautosnd=execution::then(input,[](auto...args){
- std::print(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.
upon_error and upon_done are similar to then, but where then works with values sent by the input sender, upon_error works with errors, and upon_done is invoked when the "done" signal is sent.
let_value is very similar to then: 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 then sends exactly what that function ends up returning - let_value requires that the function return a sender, and the sender returned by let_value 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.
-
lazy_let_value is guaranteed to not begin executing function until the returned sender is started.
-
let_error and let_done are similar to let_value, but where let_value works with values sent by the input sender, let_error works with errors, and let_done is invoked when the "done" signal is sent.
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.
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.
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.
when_all returns a sender which completes once all of the input senders have completed. The values send by this sender are the values sent by each of the input, in order of the arguments passed to when_all.
-
when_all_with_variant does the same, but it adapts all the input senders using into_variant.
execution::schedulerautosched=get_thread_pool().scheduler();
-
-execution::senderautosends_1=...;
-execution::senderautosends_abc=...;
-
-execution::senderautoboth=execution::when_all(sched,
- sends_1,
- sends_abc
-);
-
-execution::senderautofinal=execution::then(both,[](auto...args){
- std::cout<<std::format("the two args: {}, {}",args...);
-});
-// when final executes, it will print "the two args: 1, abc"
-
Once ensure_started returns, it is known that the provided sender has been connected and start 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.
-
4.14. User-facing sender consumers
-
A sender consumer is an algorithm that takes one or more senders, which it may execution::connect, as parameters, and does not return a sender.
this_thread::sync_wait is a sender consumer that submits the work described by the provided sender for execution, similarly to ensure_started, except that it blocks the current std::thread or thread of main 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.12.1 execution::schedule and § 4.12.3 execution::transfer_just are meant to enter the domain of senders, sync_wait 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, sync_wait throws that error as an exception, or rethrows the original exception if the error is of type std::exception_ptr.
-
If the provided sender sends the "done" signal instead of values, sync_wait returns an empty optional.
-
For an explanation of the requires clause, see § 5.8 Most senders are typed. That clause also explains another sender consumer, built on top of sync_wait: sync_wait_with_variant.
-
Note: This function is specified inside std::this_thread, and not inside execution. This is because sync_wait 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 std::this_thread, 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 std::this_fiber::sync_wait would be provided. We also expect that runtimes with execution agents that use different synchronization mechanisms than std::thread's will provide their own flavors of sync_wait as well (assuming their execution agents have the means
-to block in a non-deadlock manner).
-
4.15. 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.
A receiver is a callback that supports more than one channel. In fact, it supports three of them:
-
-
-
set_value, which is the moral equivalent of an operator() or a function call, which signals successful completion of the operation its execution depends on;
-
-
set_error, 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; and
-
-
set_done, which signals that the operation completed without succeeding (set_value) and without failing (set_error). 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.
-
-
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 set_value failed with an exception, either set_error or set_done 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 std::promise, which provides the first two signals as set_value and set_error, 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.
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: start, 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 execution::ensure_started and this_thread::sync_wait, 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.
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 connect will be fulfilled.
-
execution::senderautosnd=someinputsender;
-execution::receiverautorcv=somereceiver;
-execution::operation_stateautostate=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 then, 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 execution::then(sender,invocable) to be equivalent to:
-
-
-
sender.then(invocable), if that expression is well formed; otherwise
-
-
then(sender,invocable), performed in a context where this call always performs ADL, if that expression is well formed; otherwise
-
-
a default implementation of then, which returns a sender adaptor, and then define the exact semantics of said adaptor.
-
-
However, this definition is problematic. Imagine another sender adaptor, bulk, 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 bulk to have specialized behavior, which invokes a kernel of more than one thread (with its size defined by the call to bulk); therefore, we would like to customize bulk for CUDA senders to achieve this. However, there’s no reason for CUDA kernels to
-necessarily customize the then sender adaptor, as the generic implementation is perfectly sufficient. This creates a problem, though; consider the following snippet:
-
execution::schedulerautocuda_sch=cuda_scheduler{};
-
-execution::senderautoinitial=execution::schedule(cuda_sch);
-// the type of initial is a type defined by the cuda_scheduler
-// let’s call it cuda::schedule_sender<>
-
-execution::senderautonext=execution::then(cuda_sch,[]{return1;});
-// 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::senderautokernel_sender=execution::bulk(next,shape,[](inti){...});
-
-
How can we specialize the bulk sender adaptor for our wrapped schedule_sender? 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):
However, if the input sender is not just a then_sender_adaptor like in the example above, but another sender that overrides bulk 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 execution::<sender-algorithm>(sender,args...), for any given sender algorithm that accepts a sender as its first argument, should be
-equivalent to:
-
-
-
tag_invoke(<sender-algorithm>,get_completion_scheduler<Signal>(sender),sender,args...), if that expression is well-formed; otherwise
-
-
tag_invoke(<sender-algorithm>,sender,args...), if that expression is well-formed; otherwise
-
-
a default implementation, if there exists a default implementation of the given sender algorithm.
-
-
where Signal is one of set_value, set_error, or set_done; for most sender algorithms, the completion scheduler for set_value would be used, but for some (like upon_error or let_done), one of the others would be used.
-
For sender algorithms which accept concepts other than sender as their first argument, we propose that the customization scheme remains as it has been in [P0443R14] so far, except it should also use tag_invoke.
-
5.5. Laziness is defined by sender adaptors
-
We distinguish two different guarantees about when work is submitted to an execution context:
-
-
-
strictly lazy submission, which means that there is a guarantee that no work is submitted to an execution context before a receiver is connected to a sender, and execution::start is called on the resulting operation state;
-
-
potentially eager submission, which means that work may be submitted to an execution context as soon as all the information necessary to perform it is provided.
-
-
If a sender adaptor requires potentially eager submission, strictly lazy submission is acceptable as an implementation, because it does fulfill the potentially eager guarantee. This is why the default implementations for the non-strictly-lazy sender adaptors are specified
-to dispatch to the strictly lazy ones; for an author of a specific sender, it is sufficient to specialize the strictly lazy version, to also achieve a specialization of the potentially eager one.
-
As has been described in § 4.13 User-facing sender adaptors, whether a sender adaptor is guaranteed to perform strictly lazy submission or not is defined by the adaptor used to perform it; the adaptors whose names begin with lazy_ provide the strictly lazy guarantee.
-
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.13.11 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 lazy § 4.13.7 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 execution::transfer 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 transfer 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 transfer.
-
To allow for such customization from both ends, we propose the inclusion of a secondary transitioning sender adaptor, called schedule_from. This adaptor is a form of schedule, 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 transfer, to ensure that both schedulers have a say in how the transitions are made. Any scheduler that specializes transfer(snd,sch) shall ensure that the
-return value of their customization is equivalent to schedule_from(sch,snd2), where snd2 is a successor of snd that sends values equivalent to those sent by snd.
-
The default implementation of transfer(snd,sched) is schedule_from(sched,snd).
-
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 imput sender is typed or not in common sender adaptors such as execution::then is
-hard.
-
The mechanism for this advertisement is the same as in [P0443R14]; the way to query the types is through sender_traits::value_types<tuple_like,variant_like>.
-
sender_traits::value_types 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 when_all). 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.14.2 this_thread::sync_wait: it returns a tuple of values sent by the sender passed to it, wrapped in std::optional to handle the set_done signal. However, this assumes that those values can be represented as a
-tuple, like here:
-
execution::senderautosends_1=...;
-execution::senderautosends_2=...;
-execution::senderautosends_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 value_types of a sender which sends Types... to be as follows:
If senders could only ever send one specific set of values, this would probably need to be the required form of value_types 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) set_value 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 value_types, one that represents a variant-like type. If we permit such senders, we would almost certainly need to require that the canonical form of value_types 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 Types1..., Types2..., ..., TypesN... to be as follows:
This, however, introduces a couple of complications:
-
-
-
A just(1) sender would also need to follow this structure, so the correct type for storing the value sent by it would be std::variant<std::tuple<int>> 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 where value_types 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.
-
-
As a consequence of (1): because sync_wait needs to store the above type, it can no longer return just a std::tuple<int> for just(1); it has to return std::variant<std::tuple<int>>. 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 of sync_wait must be the same across all sender types.
-
-
One possible solution to (2) above is to place a requirement on sync_wait that it can only accept senders which send only a single set of values, therefore removing the need for std::variant to appear in its API; because of this, we propose to expose both sync_wait, which is a simple, user-friendly version of the sender consumer, but requires that value_types have only one possible variant, and sync_wait_with_variant, which accepts any sender, but returns an optional whose value type is the variant of all the
-possible tuples sent by the input sender:
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: sync_wait. We imagine that if, in the future, C++ was to
-gain fibers support, we would want to also have std::this_fiber::sync_wait, in addition to std::this_thread::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.
-
-
This paper proposes to instead use the mechanism described in [P1895R0]: tag_invoke; the wording for tag_invoke has been incorporated into the proposed specification in this paper.
-
In short, instead of using globally reserved names, tag_invoke uses the type of the customization point object itself as the mechanism to find customizations. It globally reserves only a single name - tag_invoke - which itself is used the same way that
-ranges-style customization points are used. All other customization points are defined in terms of tag_invoke. For example, the customization for std::this_thread::sync_wait(s) will call tag_invoke(std::this_thread::sync_wait,s), instead of attempting
-to invoke s.sync_wait(), and then sync_wait(s) if the member call is not valid.
-
Using tag_invoke 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<typenameTag,typename...Args>
-friendautotag_invoke(Tag&&tag,wrapper&self,Args&&...args){
- returnstd::forward<Tag>(tag)(self.subobject,std::forward<Args>(args)...);
-}
-
-// but override one of them with a specific value
-friendautotag_invoke(specific_customization_point_t,wrapper&self){
- returnself.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 tag_invoke, dispatching to manually constructed vtables containing pointers to specialized implementations for the wrapped objects. For an example of such a polymorphic wrapper, see unifex::any_unique (example).
-
-
6. Specification
-
Much of this wording follows the wording of [P0443R14].
Insert this section as a new subclause, between Searchers [func.search] and Class template hash[unord.hash].
-
-
-
-
-
The name std::tag_invoke denotes a customization point object. For some subexpressions tag and args..., tag_invoke(tag,args...) is expression-equivalent to an unqualified call to tag_invoke(decay-copy(tag),args...) with overload
-resolution performed in a context that includes the declaration:
-
voidtag_invoke();
-
-
and that does not include the the std::tag_invoke name.
-
-
-
-
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 <stop_token>. 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.
Insert this section as a new subclause between Header <stop_token> synopsis [thread.stoptoken.syn] and Class stop_token[stoptoken].
-
-
-
-
-
The stoppable_token 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, T::callback_type<CB>, 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. The stoppable_token_for concept checks for a stop token type compatible with a given
-callback type. The unstoppable_token concept checks for a stop token type that does not allow stopping.
Let t and u be distinct object of type T. The type T models stoppable_token only if:
-
-
-
All copies of a stoppable_token reference the same logical shared stop state and shall report values consistent with each other.
-
-
If t.stop_possible() evaluates to false then, if u, references the same logical shared stop state, u.stop_possible() shall also subsequently evaluate to false and u.stop_requested() shall also subsequently evaluate to false.
-
-
If t.stop_requested() evaluates to true then, if u, references the same logical shared stop state, u.stop_requested() shall also subsequently evaluate to true and u.stop_possible() shall also subsequently evaluate to true.
-
-
Given a callback-type, CB, and a callback-initializer argument, init, of type Initializer then constructing an instance, cb, of type T::callback_type<CB>, passing t as the first argument and init as the second argument to the constructor, shall,
-if t.stop_possible() is true, construct an instance, callback, of type CB, direct-initialized with init, and register callback with t’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.
-
-
-
If t.stop_requested() is true at the time callback is registered then callback may be invoked immediately inline inside the call to cb’s constructor.
-
-
If callback is invoked then, if u references the same shared stop state as t, an evaluation of u.stop_requested() will be true if the beginning of the invocation of callback strongly-happens-before the evaluation of u.stop_requested().
-
-
If t.stop_possible() evaluates to false then the construction of cb is not required to construct and initialize callback.
-
-
-
Construction of a T::callback_type<CB> instance shall only throw exceptions thrown by the initialization of the CB instance from the value of type Initializer.
-
-
Destruction of the T::callback_type<CB> object, cb, removes callback from the shared stop state such that callback will not be invoked after the destructor returns.
-
-
-
If callback is currently being invoked on another thread then the destructor of cb will block until the invocation of callback returns such that the return from the invocation of callback strongly-happens-before the destruction of callback.
-
-
Destruction of a callback cb shall not block on the completion of the invocation of some other callback registered with the same shared stop state.
-
-
-
-
-
-
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.
-
-
-
Table 1: Execution control library summary [tab:execution.summary]
None of a scheduler’s copy constructor, destructor, equality comparison, or swap member functions shall exit via an exception.
-
-
None of these member functions, nor a scheduler type’s schedule function, shall introduce data races as a result of concurrent invocations of those functions from different
-threads.
-
-
For any two (possibly const) values s1 and s2 of some scheduler type S, s1==s2 shall return true only if both s1 and s2 are handles to the same associated execution context.
-
-
A scheduler type’s destructor shall not block pending completion of any receivers connected to the sender objects returned from schedule. [Note: The ability to wait for completion of submitted function objects may be provided by the associated execution
-context of the scheduler. —end note]
execution::get_forward_progress_guarantee is used to ask a scheduler about the forward progress guarantees of execution agents created by that scheduler.
-
-
The name execution::get_forward_progress_guarantee denotes a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::scheduler, execution::get_forward_progress_guarantee is ill-formed.
-Otherwise, execution::get_forward_progress_guarantee(s) is expression equivalent to:
-
-
-
tag_invoke(execution::get_forward_progress_guarantee,as_const(s)), if this expression is well formed and its type is execution::forward_progress_guarantee, and is noexcept.
If execution::get_forward_progress_guarantee(s) for some scheduler s returns execution::forward_progress_guarantee::concurrent, all execution agents created by that scheduler shall provide the concurrent forward progress guarantee. If it returns execution::forward_progress_guarantee::parallel, all execution agents created by that scheduler shall provide at least the parallel forward progress guarantee.
this_thread::execute_may_block_caller is used to ask a scheduler s whether a call execution::execute(s,f) with any invocable f may block the thread where such a call occurs.
-
-
The name this_thread::execute_may_block_caller denotes a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::scheduler, this_thread::execute_may_block_caller is ill-formed. Otherwise, this_thread::execute_may_block_caller(s) is expression equivalent to:
-
-
-
tag_invoke(this_thread::execute_may_block_caller,as_const(s)), if this expression is well formed and its type is bool, and is noexcept.
-
-
Otherwise, true.
-
-
-
If this_thread::execute_may_block_caller(s) for some scheduler s returns false, no execution::execute(s,f) call with some invocable f shall block the calling thread.
-
-
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, set_error, and set_done. These are collectively known as a receiver’s completion-signal operations.
-
-
The receiver concept defines the requirements for a receiver type with an unknown set of value types. The receiver_of concept defines the requirements for a receiver type with a known set of value types, whose error type is std::exception_ptr.
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 execution::start has been called on the operation state object that was returned by execution::connect to connect that receiver to a sender.
-
-
Once execution::start 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.
-
-
If execution::set_value exits with an exception, it is still valid to call execution::set_error or execution::set_done on the receiver, but it is no longer valid to call execution::set_value on the receiver.
-
-
-
Once one of a receiver’s completion-signal operations has completed non-exceptionally, the receiver contract has been satisfied.
execution::set_value is used to send a value completion signal to a receiver.
-
-
The name execution::set_value denotes a customization point object. The expression execution::set_value(R,Vs...) for some subexpressions R and Vs... is expression-equivalent to:
-
-
-
tag_invoke(execution::set_value,R,Vs...), if that expression is valid. If the function selected by tag_invoke does not send the value(s) Vs... to the receiver R’s value channel, the program is ill-formed with no diagnostic required.
-
-
Otherwise, execution::set_value(R,Vs...) is ill-formed.
execution::set_error is used to send a error signal to a receiver.
-
-
The name execution::set_error denotes a customization point object. The expression execution::set_error(R,E) for some subexpressions R and E is expression-equivalent to:
-
-
-
tag_invoke(execution::set_error,R,E), if that expression is valid. If the function selected by tag_invoke does not send the error E to the receiver R’s error channel, the program is ill-formed with no diagnostic required.
-
-
Otherwise, execution::set_error(R,E) is ill-formed.
execution::set_done is used to send a done signal to a receiver.
-
-
The name execution::set_done denotes a customization point object. The expression execution::set_done(R) for some subexpression R is expression-equivalent to:
-
-
-
tag_invoke(execution::set_done,R), if that expression is valid. If the function selected by tag_invoke does not signal the receiver R’s done channel, the program is ill-formed with no diagnostic required.
execution::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]
-
-
The name execution::get_scheduler denotes a customization point object. For some subexpression r, let R be decltype((r)). If R does not satisfy execution::receiver, execution::get_scheduler is ill-formed. Otherwise, execution::get_scheduler(r) is
-expression equivalent to:
-
-
-
tag_invoke(execution::get_scheduler,as_const(r)), if this expression is well formed and satisfies execution::scheduler, and is noexcept.
-
-
Otherwise, execution::get_scheduler(r) is ill-formed.
execution::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]
-
-
The name execution::get_allocator denotes a customization point object. For some subexpression r, let R be decltype((r)). If R does not satisfy execution::receiver, execution::get_allocator is ill-formed. Otherwise, execution::get_allocator(r) is
-expression equivalent to:
-
-
-
tag_invoke(execution::get_allocator,as_const(r)), if this expression is well formed and models Allocator, and is noexcept.
-
-
Otherwise, execution::get_allocator(r) is ill-formed.
execution::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]
-
-
The name execution::get_stop_token denotes a customization point object. For some subexpression r, let R be decltype((r)). If R does not satisfy execution::receiver, execution::get_stop_token is ill-formed. Otherwise, execution::get_stop_token(r) is expression equivalent to:
-
-
-
tag_invoke(execution::get_stop_token,as_const(r)), if this expression is well formed and satisfies stoppable_token, and is noexcept.
-
-
Otherwise, never_stop_token{}.
-
-
-
Let r be a receiver, s be a sender, and op_state be an operation state resulting from an execution::connect(s,r) call. Let token be a stop token resulting from an execution::get_stop_token(r) call. token must remain valid at least until a call to
-a receiver completion-signal function of r returns successfully. [Note: this means that, unless it knows about further guarantees provided by the receiver r, the implementation of op_state should not use token after it makes a call to a receiver
-completion-signal function of r. This also implies that stop callbacks registered on token by the implementation of op_state or s must be destroyed before such a call to a receiver completion-signal function of r. --end note]
-
-
9.5. Operation states [execution.op_state]
-
-
-
The operation_state concept defines the requirements for an operation state type, which allows for starting the execution of work.
execution::start is used to start work represented by an operation state object.
-
-
The name execution::start denotes a customization point object. The expression execution::start(O) for some lvalue subexpression O is expression-equivalent to:
-
-
-
tag_invoke(execution::start,O), if that expression is valid. If the function selected by tag_invoke does not start the work represented by the operation state O, the program is ill-formed with no diagnostic required.
-
-
Otherwise, execution::start(O) is ill-formed.
-
-
-
The caller of execution::start(O) must guarantee that the lifetime of the operation state object O extends at least until one of the receiver completion-signal functions of a receiver R passed into the execution::connect call that produced O 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]
-
-
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 sender concept defines the requirements for a sender type. The sender_to concept defines the requirements for a sender type capable of being connected with a specific receiver type.
The class sender_base is used as a base class to tag sender types which do not expose member templates value_types, error_types, and a static member constant expression sends_done.
-
-
The class template sender_traits is used to query a sender type for facts associated with the signal it sends.
-
-
The primary class template sender_traits<S> is defined as if inheriting from an implementation-defined class template sender-traits-base<S> defined as follows:
-
-
-
If has-sender-types<S> is true, then sender-traits-base<S> is equivalent to:
Otherwise, if derived_from<S,sender_base> is true, then sender-traits-base<S> is equivalent to
-
template<classS>
- structsender-traits-base{};
-
-
-
Otherwise, sender-traits-base<S> is equivalent to
-
template<classS>
- structsender-traits-base{
- using__unspecialized=void;// exposition only
- };
-
-
-
-
If sender_traits<S>::value_types<Tuple,Variant> for some sender type S is well formed, it shall be a type Variant<Tuple<Args0...,Args1...,...,ArgsN...>>, where the type packs Args0 through ArgsN are the packs of types the sender S passes as
-arguments to execution::set_value after a receiver object. If such sender S invokes execution::set_value(r,args...) for some receiver r, where decltype(args) is not one of the type packs Args0 through ArgsN, the program is ill-formed with no
-diagnostic required.
-
-
If sender_traits<S>::error_types<Variant> for some sender type S is well formed, it shall be a type Variant<E0,E1,...,EN>, where the types E0 through EN are the types the sender S passes as arguments to execution::set_error after a receiver
-object. If such sender S invokes execution::set_error(r,e) for some receiver r, where decltype(e) is not one of the types E0 through EN, the program is ill-formed with no diagnostic required.
-
-
If sender_traits<S>::sends_done is well formed and true, and such sender S invokes execution::set_done(r) for some receiver r, the program is ill-formed with no diagnostic required.
-
-
Users may specialize sender_traits on program-defined types.
execution::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.
-
-
The name execution::connect denotes a customization point object. For some subexpressions s and r, let S be decltype((s)) and R be decltype((r)). If R does not satisfy execution::receiver or S does not satisfy execution::sender, execution::connect(s,r) is ill-formed. Otherwise, the expression execution::connect(s,r) is expression-equivalent to:
-
-
-
tag_invoke(execution::connect,s,r), if that expression is valid and its type satisfies execution::operation_state. If the function selected by tag_invoke does not return an operation state for which execution::start starts work described by s, the program
-is ill-formed with no diagnostic required.
-
-
Otherwise, execution::connect(s,r) is ill-formed.
-
-
-
Standard sender types shall always expose an rvalue-qualified overload of a customization of execution::connect. Standard sender types shall only expose an lvalue-qualified overload of a customization of execution::connect if they are copyable.
execution::get_completion_scheduler is used to ask a sender object for the completion scheduler for one of its signals.
-
-
The name execution::get_completion_scheduler denotes a customization point object template. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::sender, execution::get_completion_scheduler is ill-formed. If the template
-argument CPO in execution::get_completion_scheduler<CPO> is not one of execution::set_value_t, execution::set_error_t, or execution::set_done_t, execution::get_completion_scheduler<CPO> is ill-formed. Otherwise, execution::get_completion_scheduler<CPO>(s) is expression-equivalent to:
-
-
-
tag_invoke(execution::get_completion_scheduler<CPO>,as_const(s)), if this expression is well formed and satisfies execution::scheduler, and is noexcept.
-
-
Otherwise, execution::get_completion_scheduler<CPO>(s) is ill-formed.
-
-
-
If, for some sender s and customization point object CPO, execution::get_completion_scheduler<decltype(CPO)>(s) is well-formed and results in a scheduler sch, and the sender s invokes CPO(r,args...), for some receiver r which has been connected to s, with additional arguments args..., on an execution agent which does not belong to the associated execution context of sch, the behavior is undefined.
execution::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.
-
-
The name execution::schedule denotes a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::scheduler, execution::schedule is ill-formed. Otherwise, the expression execution::schedule(s) is expression-equivalent to:
-
-
-
tag_invoke(execution::schedule,s), if that expression is valid and its type satisfies execution::sender. If the function selected by tag_invoke does not return a sender whose set_value completion scheduler is equivalent to s, the program is ill-formed with no
-diagnostic required.
-
-
Otherwise, execution::schedule(s) is ill-formed.
-
-
-
9.6.4.3. execution::just[execution.senders.just]
-
-
-
execution::just is used to create a sender that propagates a set of values to a connected receiver.
execution::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.
-
-
The name execution::transfer_just denotes a customization point object. For some subexpressions s and vs..., let S be decltype((s)) and Vs... be decltype((vs)). If S does not satisfy execution::scheduler, or any type V in Vs does not
-satisfy moveable-value, execution::transfer_just(s,vs...) is ill-formed. Otherwise, execution::transfer_just(s,vs...) is expression-equivalent to:
-
-
-
tag_invoke(execution::transfer_just,s,vs...), if that expression is valid and its type satisfies execution::typed_sender. If the function selected by tag_invoke does not return a sender whose set_value completion scheduler is equivalent to s and sends
-values equivalent to vs... to a receiver connected to it, the program is ill-formed with no diagnostic required.
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.
-
-
Most sender adaptors have two versions, an potentially eager version, and a strictly lazy version. For such sender adaptors, adaptor is the potentially eager version, and lazy_adaptor is the strictly
-lazy version.
-
-
A strictly lazy version of 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 execution::connect, and execution::start is called on the resulting operation state. This requirement applies to any function that is selected by the implementation of the sender adaptor.
-
-
Unless otherwise specified, all sender adaptors which accept a single sender 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.
-
-
Unless otherwise specified, whenever a strictly lazy 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 execution::connect. This requirements
-applies to any sender returned from a function that is selected by the implementation of a strictly lazy sender adaptor.
A pipeable sender adaptor closure object is a function object that accepts one or more sender arguments and returns a sender. For a sender adaptor closure object C and an expression S such that decltype((S)) models sender, the following
-expressions are equivalent and yield a sender:
-
C(S)
-S|C
-
-
Given an additional pipeable sender adaptor closure object D, the expression C|D is well-formed and produces another range adaptor closure object such that the following two expressions are equivalent:
-
S|C|D
-S|(C|D)
-
-
-
A pipeable sender adaptor object is a customization point object that accepts a sender as its first argument and returns a 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 the following expressions are equivalent:
execution::on and execution::lazy_on are used to adapt a sender in a sender that will start the input sender on an execution agent belonging to a specific execution context.
-
-
The name execution::on denotes a customization point object. For some subexpressions sch and s, let Sch be decltype((sch)) and S be decltype((s)). If Sch does not satisfy execution::scheduler, or S does not satisfy execution::sender, execution::on is ill-formed. Otherwise, the expression execution::on(sch,s) is expression-equivalent to:
-
-
-
tag_invoke(execution::on,sch,s), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, lazy_on(sch,s).
-
-
If the function selected above does not return a sender which starts s on an execution agent of the associated execution context of sch, the program is ill-formed with no diagnostic required.
-
-
The name execution::lazy_on denotes a customization point object. For some subexpressions sch and s, let Sch be decltype((sch)) and S be decltype((s)). If Sch does not satisfy execution::scheduler, or S does not satisfy execution::sender, execution::lazy_on is ill-formed. Otherwise, the expression execution::lazy_on(sch,s) is expression-equivalent to:
-
-
-
tag_invoke(execution::lazy_on,sch,s), if that expression is valid and its type satisfies execution::sender. If the function selected above does not return a sender which starts s on an execution agent of the associated execution context of sch when
-started, the program is ill-formed with no diagnostic required.
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it results in an operation state op_state. When execution::start is called on op_state, it:
-
-
-
Constructs a receiver r:
-
-
-
When execution::set_value(r) is called, it calls execution::connect(s,out_r), which results in op_state2. It calls execution::start(op_state2). If any of these throws an exception, it calls execution::set_error on out_r,
-passing current_exception() as the second argument.
-
-
When execution::set_error(r,e) is called, it calls execution::set_error(out_r,e).
-
-
When execution::set_done(r) is called, it calls execution::set_done(out_r).
-
-
-
Calls execution::schedule(sch), which results in s3. It then calls execution::connect(s3,r), resulting in op_state3, and then it calls execution::start(op_state3). If any of these throws an exception, it catches it and calls execution::set_error(out_r,current_exception()).
-
-
-
-
Any receiver r created by an implementation of on and lazy_on shall implement the get_scheduler receiver query. The scheduler returned from the query for all such receivers should be equivalent to the sch argument passed into the on or lazy_on call.
execution::transfer and execution::lazy_transfer are used to adapt a sender into a sender with a different associated set_value completion scheduler. [Note: it results in a transition between different execution contexts when executed. --end note]
-
-
The name execution::transfer denotes a customization point object. For some subexpressions sch and s, let Sch be decltype((sch)) and S be decltype((s)). If Sch does not satisfy execution::scheduler, or S does not satisfy execution::sender, execution::transfer is ill-formed. Otherwise, the expression execution::transfer(s,sch) is expression-equivalent to:
-
-
-
tag_invoke(execution::transfer,get_completion_scheduler<set_value_t>(s),s,sch), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::transfer,s,sch), if that expression is valid and its type satisfies 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 execution::schedule_from(sch,s2), where s2 is a sender which sends equivalent to those sent by s, the program is ill-formed with no diagnostic required.
-
-
The name execution::lazy_transfer denotes a customization point object. For some subexpressions sch and s, let Sch be decltype((sch)) and S be decltype((s)). If Sch does not satisfy execution::scheduler, or S does not satisfy execution::sender, execution::lazy_transfer is ill-formed. Otherwise, the expression execution::lazy_transfer(s,sch) is expression-equivalent to:
-
-
-
tag_invoke(execution::lazy_transfer,get_completion_scheduler<set_value_t>(s),s,sch), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::lazy_transfer,s,sch), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, lazy_schedule_from(sch,s).
-
-
If the function selected above does not return a sender which is a result of a call to execution::lazy_schedule_from(sch,s2), where s2 is a sender which sends equivalent to those sent by s, the program is ill-formed with no diagnostic required.
-
-
Senders returned from execution::transfer and execution::lazy_transfer shall not propagate the sender queries get_completion_scheduler<CPO> to an input sender. They shall return a sender equivalent to the sch argument from those queries.
execution::schedule_from and execution::lazy_schedule_from are used to schedule work dependent on the completion of a sender onto a scheduler’s associated execution context. [Note: schedule_from and lazy_schedule_from are not meant to be used in
-user code; they are used in the implementation of transfer and lazy_transfer. -end note]
-
-
The name execution::schedule_from denotes a customization point object. For some subexpressions sch and s, let Sch be decltype((sch)) and S be decltype((s)). If Sch does not satisfy execution::scheduler, or S does not satisfy execution::typed_sender, execution::schedule_from is ill-formed. Otherwise, the expression execution::schedule_from(sch,s) is expression-equivalent to:
-
-
-
tag_invoke(execution::schedule_from,sch,s), if that expression is valid and its type satisfies execution::sender. If the function selected by tag_invoke does not return a sender which completes on an execution agent belonging to the associated
-execution context of sch and sends signals equivalent to those sent by s, the program is ill-formed with no diagnostic required.
-
-
Otherwise, lazy_schedule_from(sch,s).
-
-
-
The name execution::lazy_schedule_from denotes a customization point object. For some subexpressions sch and s, let Sch be decltype((sch)) and S be decltype((s)). If Sch does not satisfy execution::scheduler, or S does not satisfy execution::typed_sender, execution::lazy_schedule_from is ill-formed. Otherwise, the expression execution::lazy_schedule_from(sch,s) is expression-equivalent to:
-
-
-
tag_invoke(execution::lazy_schedule_from,sch,s), if that expression is valid and its type satisfies execution::sender. If the function selected by tag_invoke does not return a sender which completes on an execution agent belonging to the associated
-execution context of sch and sends signals equivalent to those sent by s, the program is ill-formed with no diagnostic required.
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r.
-
-
Calls execution::connect(s,r), which results in an operation state op_state2. If any of these throws an exception, calls execution::set_error on out_r, passing current_exception() as the second argument.
-
-
When a receiver completion-signal Signal(r,args...) is called, it constructs a receiver r2:
-
-
-
When execution::set_value(r2) is called, it calls Signal(out_r,args...).
-
-
When execution::set_error(r2,e) is called, it calls execution::set_error(out_r,e).
-
-
When execution::done(r2) is called, it calls execution::set_done(out_r).
-
-
It then calls execution::schedule(sch), resulting in a sender s3. It then calls execution::connect(s3,r2), resulting in an operation state op_state3. It then calls execution::start(op_state3). If any of these throws an exception,
-it catches it and calls execution::set_error(out_r,current_exception()).
-
-
Returns an operation state op_state that contains op_state2. When execution::start(op_state) is called, calls execution::start(op_state2).
-
-
-
-
Senders returned from execution::transfer and execution::lazy_transfer shall not propagate the sender queries get_completion_scheduler<CPO> to an input sender. They shall return a scheduler equivalent to the sch argument from those queries.
execution::then and execution::lazy_then are used to attach invocables as continuation for successful completion of the input sender.
-
-
The name execution::then denotes a customization point object. For some subexpressions s and f, let S be decltype((s)). If S does not satisfy execution::sender, execution::then is ill-formed. Otherwise, the expression execution::then(s,f) is
-expression-equivalent to:
-
-
-
tag_invoke(execution::then,get_completion_scheduler<set_value_t>(s),s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::then,s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, lazy_then(s,f).
-
-
If the function selected above does not return a sender which invokes f with the result of the set_value signal of s, passing the return value as the value to any connected receivers, and propagates the other completion-signals sent by s, the program is
- ill-formed with no diagnostic required.
-
-
The name execution::lazy_then denotes a customization point object. For some subexpressions s and f, let S be decltype((s)). If S does not satisfy execution::sender, execution::lazy_then is ill-formed. Otherwise, the expression execution::lazy_then(s,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::lazy_then,get_completion_scheduler<set_value_t>(s),s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::lazy_then,s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r:
-
-
-
When execution::set_value(r,args...) is called, calls invoke(f,args...) and passes the result v to execution::set_value(out_r,v). If any of these throws an exception, it catches it and calls execution::set_error(out_r,current_exception()).
-
-
When execution::set_error(r,e) is called, calls execution::set_error(out_r,e).
-
-
When execution::set_done(r) is called, calls execution::set_done(out_r).
-
-
-
Calls execution::connect(s,r), which results in an operation state op_state2.
-
-
Returns an operation state op_state that contains op_state2. When execution::start(op_state) is called, calls execution::start(op_state2).
-
-
-
If the function selected above does not return a sender which invokes f with the result of the set_value signal of s, passing the return value as the value to any connected receivers, and propagates the other completion-signals sent by s, the program is
- ill-formed with no diagnostic required.
execution::upon_error and execution::lazy_upon_error are used to attach invocables as continuation for successful completion of the input sender.
-
-
The name execution::upon_error denotes a customization point object. For some subexpressions s and f, let S be decltype((s)). If S does not satisfy execution::sender, execution::upon_error is ill-formed. Otherwise, the expression execution::upon_error(s,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::upon_error,get_completion_scheduler<set_error_t>(s),s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::upon_error,s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, lazy_upon_error(s,f).
-
-
If the function selected above does not return a sender which invokes f with the result of the set_error signal of s, passing the return value as the value to any connected receivers, and propagates the other completion-signals sent by s, the program is
- ill-formed with no diagnostic required.
-
-
The name execution::lazy_upon_error denotes a customization point object. For some subexpressions s and f, let S be decltype((s)). If S does not satisfy execution::sender, execution::lazy_upon_error is ill-formed. Otherwise, the expression execution::lazy_upon_error(s,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::lazy_upon_error,get_completion_scheduler<set_error_t>(s),s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::lazy_upon_error,s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r:
-
-
-
When execution::set_value(r,args...) is called, calls execution::set_value(out_r,args...).
-
-
When execution::set_error(r,e) is called, calls invoke(f,e) and passes the result v to execution::set_value(out_r,v). If any of these throws an exception, it catches it and calls execution::set_error(out_r,current_exception()).
-
-
When execution::set_done(r) is called, calls execution::set_done(out_r).
-
-
-
Calls execution::connect(s,r), which results in an operation state op_state2.
-
-
Returns an operation state op_state that contains op_state2. When execution::start(op_state) is called, calls execution::start(op_state2).
-
-
-
If the function selected above does not return a sender which invokes f with the result of the set_error signal of s, passing the return value as the value to any connected receivers, and propagates the other completion-signals sent by s, the program is
- ill-formed with no diagnostic required.
execution::upon_done and execution::lazy_upon_done are used to attach invocables as continuation for successful completion of the input sender.
-
-
The name execution::upon_done denotes a customization point object. For some subexpressions s and f, let S be decltype((s)). If S does not satisfy execution::sender, execution::upon_done is ill-formed. Otherwise, the expression execution::upon_done(s,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::upon_done,get_completion_scheduler<set_done_t>(s),s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::upon_done,s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, lazy_upon_done(s,f).
-
-
If the function selected above does not return a sender which invokes f when the set_done signal of s is called, passing the return value as the value to any connected receivers, and propagates the other completion-signals sent by s, the program is
- ill-formed with no diagnostic required.
-
-
The name execution::lazy_upon_done denotes a customization point object. For some subexpressions s and f, let S be decltype((s)). If S does not satisfy execution::sender, execution::lazy_upon_done is ill-formed. Otherwise, the expression execution::lazy_upon_done(s,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::lazy_upon_done,get_completion_scheduler<set_done_t>(s),s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::lazy_upon_done,s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r:
-
-
-
When execution::set_value(r,args...) is called, calls execution::set_value(out_r,args...).
-
-
When execution::set_error(r,e) is called, calls execution::set_error(out_r,e).
-
-
When execution::set_done(r) is called, calls invoke(f) and passes the result v to execution::set_value(out_r,v). If any of these throws an exception, it catches it and calls execution::set_error(out_r,current_exception()).
-
-
-
Calls execution::connect(s,r), which results in an operation state op_state2.
-
-
Returns an operation state op_state that contains op_state2. When execution::start(op_state) is called, calls execution::start(op_state2).
-
-
-
If the function selected above does not return a sender which invokes f when the set_done signal of s is called, passing the return value as the value to any connected receivers, and propagates the other completion-signals sent by s, the program is
- ill-formed with no diagnostic required.
execution::let_value and execution::lazy_let_value are used to insert continuations creating more work dependent on the results of their input senders into a sender chain.
-
-
The name execution::let_value denotes a customization point object. For some subexpressions s and f, let S be decltype((s)). If S does not satisfy execution::sender, execution::let_value is ill-formed. Otherwise, the expression execution::let_value(s,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::let_value,get_completion_scheduler<set_value_t>(s),s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::let_value,s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, lazy_let_value(s,f).
-
-
If the function selected above does not return a sender which invokes f when set_value is called, and making its completion dependent on the completion of a sender returned by f, and propagates the other completion-signals sent by s, the program is
- ill-formed with no diagnostic required.
-
-
The name execution::lazy_let_value denotes a customization point object. For some subexpressions s and f, let S be decltype((s)). If S does not satisfy execution::sender, execution::lazy_let_value is ill-formed. Otherwise, the expression execution::lazy_let_value(s,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::lazy_let_value,get_completion_scheduler<set_value_t>(s),s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::lazy_let_value,s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r.
-
-
-
When execution::set_value(r,args...) is called, copies args... into op_state2 as args2..., then calls invoke(f,args2...), resulting in a sender s3. It then calls execution::connect(s3,out_r), resulting in an operation state op_state3. op_state3 is saved as a part of op_state2. It then calls execution::start(op_state3). If any of these throws an exception, it catches it and calls execution::set_error(out_r,current_exception()).
-
-
When execution::set_error(r,e) is called, calls execution::set_error(out_r,e).
-
-
When execution::set_done(r,e) is called, calls execution::set_done(out_r).
-
-
-
Calls execution::connect(s,r), which results in an operation state op_state2.
-
-
Returns an operation state op_state that contains op_state2. When execution::start(op_state) is called, calls execution::start(op_state2).
-
-
-
If the function selected above does not return a sender which invokes f when set_value is called, and making its completion dependent on the completion of a sender returned by f, and propagates the other completion-signals sent by s, the program is
- ill-formed with no diagnostic required.
execution::let_error and execution::lazy_let_error are used to insert continuations creating more work dependent on the results of their input senders into a sender chain.
-
-
The name execution::let_error denotes a customization point object. For some subexpressions s and f, let S be decltype((s)). If S does not satisfy execution::sender, execution::let_error is ill-formed. Otherwise, the expression execution::let_error(s,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::let_error,get_completion_scheduler<set_error_t>(s),s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::let_error,s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, lazy_let_error(s,f).
-
-
If the function selected above does not return a sender which invokes f when set_error is called, and making its completion dependent on the completion of a sender returned by f, and propagates the other completion-signals sent by s, the program is
- ill-formed with no diagnostic required.
-
-
The name execution::lazy_let_error denotes a customization point object. For some subexpressions s and f, let S be decltype((s)). If S does not satisfy execution::sender, execution::lazy_let_error is ill-formed. Otherwise, the expression execution::lazy_let_error(s,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::lazy_let_error,get_completion_scheduler<set_error_t>(s),s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::lazy_let_error,s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r.
-
-
-
When execution::set_value(r,args...) is called, calls execution::set_value(out_r,args...).
-
-
When execution::set_error(r,e) is called, copies e into op_state2 as e, then calls invoke(f,e), resulting in a sender s3. It then calls execution::connect(s3,out_r), resulting in an operation state op_state3. op_state3 is saved
-as a part of op_state2. It then calls execution::start(op_state3). If any of these throws an exception, it catches it and calls execution::set_error(out_r,current_exception()).
-
-
When execution::set_done(r,e) is called, calls execution::set_done(out_r).
-
-
-
Calls execution::connect(s,r), which results in an operation state op_state2.
-
-
Returns an operation state op_state that contains op_state2. When execution::start(op_state) is called, calls execution::start(op_state2).
-
-
-
If the function selected above does not return a sender which invokes f when set_error is called, and making its completion dependent on the completion of a sender returned by f, and propagates the other completion-signals sent by s, the program is
- ill-formed with no diagnostic required.
execution::let_done and execution::lazy_let_done are used to insert continuations creating more work dependent on the results of their input senders into a sender chain.
-
-
The name execution::let_done denotes a customization point object. For some subexpressions s and f, let S be decltype((s)). If S does not satisfy execution::sender, execution::let_done is ill-formed. Otherwise, the expression execution::let_done(s,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::let_done,get_completion_scheduler<set_done_t>(s),s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::let_done,s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, lazy_let_done(s,f).
-
-
If the function selected above does not return a sender which invokes f when set_done is called, and making its completion dependent on the completion of a sender returned by f, and propagates the other completion-signals sent by s, the program is
- ill-formed with no diagnostic required.
-
-
The name execution::lazy_let_done denotes a customization point object. For some subexpressions s and f, let S be decltype((s)). If S does not satisfy execution::sender, execution::lazy_let_done is ill-formed. Otherwise, the expression execution::lazy_let_done(s,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::lazy_let_done,get_completion_scheduler<set_done_t>(s),s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::lazy_let_done,s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r.
-
-
-
When execution::set_value(r,args...) is called, calls execution::set_value(out_r,args...).
-
-
When execution::set_error(r,e) is called, calls execution::set_error(out_r,e).
-
-
When execution::set_done(r) is called, calls invoke(f), resulting in a sender s3. It then calls execution::connect(s3,out_r), resulting in an operation state op_state3. op_state3 is saved as a part of op_state2.
-It then calls execution::start(op_state3). If any of these throws an exception, it catches it and calls execution::set_error(out_r,current_exception()).
-
-
-
Calls execution::connect(s,r). which results in an operation state op_state2.
-
-
Returns an operation state op_state that contains op_state2. When execution::start(op_state) is called, calls execution::start(op_state2).
-
-
-
If the function selected above does not return a sender which invokes f when set_done is called, and making its completion dependent on the completion of a sender returned by f, and propagates the other completion-signals sent by s, the program is
- ill-formed with no diagnostic required.
execution::bulk and execution::lazy_bulk are used to run a task repeatedly for every index in an index space.
-
-
The name execution::bulk denotes a customization point object. For some subexpressions s, shape, and f, let S be decltype((s)), Shape be decltype((shape)), and F be decltype((f)). If S does not satisfy execution::sender or Shape does not
-satisfy integral, execution::bulk is ill-formed. Otherwise, the expression execution::bulk(s,shape,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::bulk,get_completion_scheduler<set_value_t>(s),s,shape,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::bulk,s,shape,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, lazy_bulk(s,shape,f).
-
-
-
The name execution::lazy_bulk denotes a customization point object. For some subexpressions s, shape, and f, let S be decltype((s)), Shape be decltype((shape)), and F be decltype((f)). If S does not satisfy execution::sender or Shape does not satisfy integral, execution::bulk is ill-formed. Otherwise, the expression execution::bulk(s,shape,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::bulk,get_completion_scheduler<set_value_t>(s),s,shape,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::bulk,s,shape,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r:
-
-
-
When execution::set_value(r,args...) is called, calls f(i,args...) for each i of type Shape from 0 to shape, then calls execution::set_value(out_r,args...). If any of these throws an exception, it catches it and calls execution::set_error(out_r,current_exception()).
-
-
When execution::set_error(r,e) is called, calls execution::set_error(out_r,e).
-
-
When execution::set_done(r,e) is called, calls execution::set_done(out_r,e).
-
-
-
Calls execution::connect(s,r), which results in an operation state op_state2.
-
-
Returns an operation state op_state that contains op_state2. When execution::start(op_state) is called, calls execution::start(op_state2).
-
-
-
If the function selected above does not return a sender which invokes f(i,args...) for each i of type Shape from 0 to shape when the input sender sends values args..., 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.
execution::split and execution::lazy_split are used to adapt an arbitrary sender into a sender that can be connected multiple times.
-
-
The name execution::split denotes a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::typed_sender, execution::split is ill-formed. Otherwise, the expression execution::split(s) is
-expression-equivalent to:
-
-
-
tag_invoke(execution::split,get_completion_scheduler<set_value_t>(s),s), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::split,s), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, lazy_split(s).
-
-
If the function selected above does not return a sender which sends references to values sent by s, propagating the other channels, the program is ill-formed with no diagnostic required.
-
-
The name execution::lazy_split denotes a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::typed_sender, execution::lazy_split is ill-formed. Otherwise, the expression execution::lazy_split(s) is expression-equivalent to:
-
-
-
tag_invoke(execution::lazy_split,get_completion_scheduler<set_value_t>(s),s), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::lazy_split,s), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, constructs a sender s2, which:
-
-
-
Creates an object sh_state. The lifetime of sh_state shall last for at least as long as the lifetime of the last operation state object returned from execution::connect(s,some_r) for some receiver some_r.
-
-
Constructs a receiver r:
-
-
-
When execution::set_value(r,args...) is called, saves the expressions args... as subobjects of sh_state.
-
-
When execution::set_error(r,e) is called, saves the expression e as a subobject of sh_state.
-
-
When execution::set_done(r) is called, saves this fact in sh_state.
-
-
-
Calls execution::connect(s,r), resulting in an operation state op_state2. op_state2 is saved as a subobject of sh_state.
-
-
When s2 is connected with a receiver out_r, it returns an operation state object op_state. When execution::start(op_state) is called, it calls execution::start(op_state2), if this is the first time this expression would be evaluated. When both execution::start(op_state) and Signal(r,args...) have been called, calls Signal(out_r,args2...), where args2... is a pack of lvalues referencing the subobjects of sh_state that have been saved by the
-original call to Signal(r,args...).
-
-
-
If the function selected above does not return a sender which sends references to values sent by s, propagating the other channels, the program is ill-formed with no diagnostic required.
execution::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_with_variant 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.
-
-
The name execution::when_all denotes a customization point object. For some subexpressions s..., let S be decltype((s)). If any type Si in S... does not satisfy execution::typed_sender, or the number of the arguments sender_traits<Si>::value_types passes into the Variant template parameter is not 1, execution::when_all is ill-formed. Otherwise, the expression execution::when_all(s...) is expression-equivalent to:
-
-
-
tag_invoke(execution::when_all,s...), if that expression is valid and its type satisfies execution::sender. If the function selected by tag_invoke does not return a sender which sends a concatenation of values sent by s... when they all complete with set_value, the program is ill-formed with no diagnostic required.
-
-
Otherwise, constructs a sender s. When s is connected with some receiver out_r, it:
-
-
-
For each sender si in s..., constructs a receiver ri:
-
-
-
If execution::set_value(ri,ti...) is called for every ri, execution::set_value(out_r,t0...,t1...,...,tn...) is called, where n is sizeof...(s)-1.
-
-
Otherwise, if execution::set_error(ri,e) is called for any ri, execution::set_error(out_r,e) is called.
-
-
Otherwise, if execution::set_done(ri) is called for any ri, execution::set_done(out_r) is called.
-
-
-
For each sender si in s..., calls execution::connect(si,ri), resulting in operation states op_statei.
-
-
Returns an operation state op_state that contains each operation state op_statei. When execution::start(op_state) is called, calls execution::start(op_statei) for each op_statei.
-
-
-
-
The name execution::when_all_with_variant denotes a customization point object. For some subexpressions s..., let S be decltype((s)). If any type Si in S... does not satisfy execution::typed_sender, execution::when_all_with_variant is ill-formed. Otherwise, the expression execution::when_all_with_variant(s...) is expression-equivalent to:
-
-
-
tag_invoke(execution::when_all_with_variant,s...), if that expression is valid and its type satisfies execution::sender. If the function selected by tag_invoke does not return a sender which sends the types into-variant-type<S>... when they all complete with set_value, the program is ill-formed with no diagnostic required.
Adaptors defined in this subclause are strictly lazy.
-
-
Senders returned from adaptors defined in this subclause shall not expose the sender queries get_completion_scheduler<CPO>.
-
-
tag_invoke expressions used in the definitions of the sender adaptors in this subclause shall not consider member functions of their first non-tag arguments.
execution::transfer_when_all and execution::lazy_transfer_when_all are 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_with_variant and execution::lazy_transfer_when_all_with_variant are 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 adaptor. --end note]
-
-
The name execution::transfer_when_all denotes a customization point object. For some subexpressions sch and s..., let Sch be decltype(sch) and S be decltype((s)). If Sch does not satisfy scheduler, or any type Si in S... does not satisfy execution::typed_sender, or the number of the arguments sender_traits<Si>::value_types passes into the Variant template parameter is not 1 execution::transfer_when_all is ill-formed.
-Otherwise, the expression execution::transfer_when_all(sch,s...) is expression-equivalent to:
-
-
-
tag_invoke(execution::transfer_when_all,sch,s...), if that expression is valid and its type satisfies execution::sender. If the function selected by tag_invoke does not return a sender which sends a concatenation of values sent by s... when they
-all complete with set_value, 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 of sch, the program is ill-formed with no diagnostic required.
-
-
Otherwise, transfer(when_all(s...),sch).
-
-
-
The name execution::lazy_transfer_when_all denotes a customization point object. For some subexpressions sch and s..., let Sch be decltype(sch) and S be decltype((s)). If Sch does not satisfy scheduler, or any type Si in S... does not satisfy execution::typed_sender, or the number of the arguments sender_traits<Si>::value_types passes into the Variant template parameter is not 1, execution::lazy_transfer_when_all is ill-formed. Otherwise, the expression execution::lazy_transfer_when_all(sch,s...) is expression-equivalent to:
-
-
-
tag_invoke(execution::lazy_transfer_when_all,sch,s...), if that expression is valid and its type satisfies execution::sender. If the function selected by tag_invoke does not return a sender which sends a concatenation of values sent by s... when
-they all complete with set_value, 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 of sch, the program is ill-formed with no diagnostic
-required.
-
-
Otherwise, lazy_transfer(when_all(s...),sch).
-
-
-
The name execution::transfer_when_all_with_variant denotes a customization point object. For some subexpressions s..., let S be decltype((s)). If any type Si in S... does not satisfy execution::typed_sender, execution::transfer_when_all_with_variant is ill-formed. Otherwise, the expression execution::transfer_when_all_with_variant(s...) is expression-equivalent to:
-
-
-
tag_invoke(execution::transfer_when_all_with_variant,s...), if that expression is valid and its type satisfies execution::sender. If the function selected by tag_invoke does not return a sender which sends the types into-variant-type<S>... when they all complete with set_value, the program is ill-formed with no diagnostic required.
The name execution::lazy_transfer_when_all_with_variant denotes a customization point object. For some subexpressions s..., let S be decltype((s)). If any type Si in S... does not satisfy execution::typed_sender, execution::lazy_transfer_when_all_with_variant is ill-formed. Otherwise, the expression execution::lazy_transfer_when_all_with_variant(s...) is expression-equivalent to:
-
-
-
tag_invoke(execution::lazy_transfer_when_all_with_variant,s...), if that expression is valid and its type satisfies execution::sender. If the function selected by tag_invoke does not return a sender which sends the types into-variant-type<S>... when they all complete with set_value, the program is ill-formed with no diagnostic required.
Senders returned from execution::transfer_when_all and execution::lazy_transfer_when_all shall not propagate the sender queries get_completion_scheduler<CPO> to input senders. They shall return a scheduler equivalent to the sch argument from
-those queries.
execution::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.
-
-
The template into-variant-type is used to compute the type sent by a sender returned from execution::into_variant.
execution::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.
-
-
The name execution::ensure_started denotes a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::typed_sender, execution::ensure_started is ill-formed. Otherwise, the expression execution::ensure_started(s) is expression-equivalent to:
-
-
-
tag_invoke(execution::ensure_started,get_completion_scheduler<set_value_t>(s),s), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::ensure_started,s), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise:
-
-
-
Constructs a receiver r.
-
-
Calls execution::connect(s,r), resulting in operation state op_state, and then calls execution::start(op_state). If any of these throws an exception, it catches it and calls execution::set_error(r,current_exception()).
-
-
Constructs a sender s2. When s2 is connected with some receiver out_r, it results in an operation state op_state2. Once both execution::start(op_state2) and one of the receiver completion-signals has been called on r:
-
-
-
If execution::set_value(r,ts...) has been called, calls execution::set_value(out_r,ts...).
-
-
If execution::set_error(r,e) has been called, calls execution::set_error(out_r,e).
-
-
If execution::set_done(r) has been called, calls execution::set_done(out_r).
-
-
-
-
If the function selected above does not eagerly start the sender s and return a sender which propagates the signals sent by s once started, the program is ill-formed with no diagnostic required.
execution::start_detached is used to eagerly start a sender without the caller needing to manage the lifetimes of any objects.
-
-
The name execution::start_detached denotes a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::sender, execution::start_detached is ill-formed. Otherwise, the expression execution::start_detached(s) is expression-equivalent to:
-
-
-
tag_invoke(execution::start_detached,execution::get_completion_scheduler<execution::set_value_t>(s),s), if that expression is valid and its type is void.
-
-
Otherwise, tag_invoke(execution::start_detached,s), if that expression is valid and its type is void.
-
-
Otherwise:
-
-
-
Constructs a receiver r:
-
-
-
When set_value(r,ts...) is called, it does nothing.
-
-
When set_error(r,e) is called, it calls std::terminate.
-
-
When set_done(r) is called, it does nothing.
-
-
-
Calls execution::connect(s,r), resulting in an operation state op_state, then calls execution::start(op_state).
-
-
-
If the function selected above does not eagerly start the sender s after connecting it with a receiver which ignores the set_value and set_done signals and calls std::terminate on the set_error signal, the program is ill-formed with no diagnostic
- required.
this_thread::sync_wait and this_thread::sync_wait_with_variant 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.
-
-
The templates sync-wait-type and sync-wait-with-variant-type are used to determine the return types of this_thread::sync_wait and this_thread::sync_wait_with_variant.
The name this_thread::sync_wait denotes a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::typed_sender, or the number of the arguments sender_traits<S>::value_types passes into the Variant template parameter is not 1, this_thread::sync_wait is ill-formed. Otherwise, this_thread::sync_wait is expression-equivalent to:
-
-
-
tag_invoke(this_thread::sync_wait,execution::get_completion_scheduler<execution::set_value_t>(s),s), if this expression is valid and its type is sync-wait-type<S>.
-
-
Otherwise, tag_invoke(this_thread::sync_wait,s), if this expression is valid and its type is sync-wait-type<S>.
-
-
Otherwise:
-
-
-
Constructs a receiver r.
-
-
Calls execution::connect(s,r), resulting in an operation state op_state, then calls execution::start(op_state).
-
-
Blocks the current thread until a receiver completion-signal of r is called. When it is:
-
-
-
If execution::set_value(r,ts...) has been called, returns sync-wait-type<S>(make_tuple(ts...))>.
-
-
If execution::set_error(r,e...) has been called, if remove_cvref_t(decltype(e)) is exception_ptr, calls std::rethrow_exception(e). Otherwise, throws e.
-
-
If execution::set_done(r) has been called, returns sync-wait-type<S(nullopt)>.
-
-
-
-
-
The name this_thread::sync_wait_with_variant denotes a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::typed_sender, this_thread::sync_wait_with_variant is ill-formed. Otherwise, this_thread::sync_wait_with_variant is expression-equivalent to:
-
-
-
tag_invoke(this_thread::sync_wait_with_variant,execution::get_completion_scheduler<execution::set_value_t>(s),s), if this expression is valid and its type is sync-wait-with-variant-type<S>.
-
-
Otherwise, tag_invoke(this_thread::sync_wait_with_variant,s), if this expression is valid and its type is sync-wait-with-variant-type<S>.
Any receiver r created by an implementation of sync_wait and sync_wait_with_variant shall implement the get_scheduler 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.
-
-
9.7. execution::execute[execution.execute]
-
-
-
execution::execute is used to create fire-and-forget tasks on a specified scheduler.
-
-
The name execution::execute denotes a customization point object. For some subexpressions sch and f, let Sch be decltype((sch)) and F be decltype((f)). If Sch does not satisfy execution::scheduler or F does not satisfy invocable<>, execution::execute is ill-formed. Otherwise, execution::execute is expression-equivalent to:
-
-
-
tag_invoke(execution::execute,sch,f), if that expression is valid and its type is void. If the function selected by tag_invoke does not invoke the function f on an execution agent belonging to the associated execution context of sch, or if it
-does not call std::terminate if an error occurs after control is returned to the caller, the program is ill-formed with no diagnostic required.
ISO/IEC JTC1/SC22/WG21 14882: Programming Language — C++
-
Audience:
-
SG1, LEWG
-
-
-
-
-
-
-
-
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 (std::atomic, std::mutex, std::counting_semaphore, etc) and lower level building blocks (std::thread, etc), we lack a Standard vocabulary and framework for asynchrony and parallelism that C++ programmers desperately need. std::async/std::future/std::promise, 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.
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.19.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.20.2 execution::then is a sender adaptor that takes an input sender and a std::invocable, and calls the std::invocable on the signal sent by the input sender. The sender returned by then sends the result of that invocation. In this case, the input sender came from schedule, so its void, meaning it won’t send us a value, so our std::invocable takes no parameters. But we return an int, which will be sent to the next recipient.
-
-
Now, we add another operation to the chain, again using § 4.20.2 execution::then. This time, we get sent a value - the int from the previous step. We add 42 to it, and then return the result.
-
-
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.21.2 this_thread::sync_wait, which will either return a std::optional<std::tuple<...>> with the value sent by the last sender, or an empty std::optional if the last sender sent a done signal, or it throws an exception if the last sender sent an error.
This example builds an asynchronous computation of an inclusive scan:
-
-
-
It scans a sequence of doubles (represented as the std::span<constdouble>input) and stores the result in another sequence of doubles (represented as std::span<double>output).
-
-
It takes a scheduler, which specifies what execution context the scan should be launched on.
-
-
It also takes a tile_count parameter that controls the number of execution agents that will be spawned.
-
-
First we need to allocate temporary storage needed for the algorithm, which we’ll do with a std::vector, partials. We need one double of temporary storage for each execution agent we create.
-
-
Next we’ll create our initial sender with § 4.19.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 sch, which means the next item in the chain will use sch.
-
-
Senders and sender adaptors support composition via operator|, similar to C++ ranges. We’ll use operator| to attach the next piece of work, which will spawn tile_count execution agents using § 4.20.9 execution::bulk (see § 4.12 Most sender adaptors are pipeable for details).
-
-
Each agent will call a std::invocable, passing it two arguments. The first is the agent’s index (i) in the § 4.20.9 execution::bulk operation, in this case a unique integer in [0,tile_count). The second argument is what the input sender sent - the temporary storage.
-
-
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 std::inclusive_scan over our elements. We store the scan result for our last element, which is the sum of all of our elements, in our temporary storage partials.
-
-
After all computation in that initial § 4.20.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 partials. We’ll do that with a single execution agent which will execute after the § 4.20.9 execution::bulk completes. We create that execution agent with § 4.20.2 execution::then.
-
-
§ 4.20.2 execution::then takes an input sender and an std::invocable and calls the std::invocable with the value sent by the input sender. Inside our std::invocable, we call std::inclusive_scan on partials, which the input senders will send to us.
-
-
Then we return partials, which the next phase will need.
-
-
Finally we do another § 4.20.9 execution::bulk of the same shape as before. In this § 4.20.9 execution::bulk, we will use the scanned values in partials to integrate the sums from other tiles into our elements, completing the inclusive scan.
-
-
async_inclusive_scan returns a sender that sends the output std::span<double>. A consumer of the algorithm can chain additional work that uses the scan result. At the point at which async_inclusive_scan returns, the computation may not have completed. In fact, it may not have even started.
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:
-
-
-
async_read 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 a std::span<std::byte>, and a handle to an I/O context. It will asynchronously read into the input buffer, up to the size of the std::span. It returns a sender which will send the number of bytes read once the read completes.
-
-
async_read_array 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 a dynamic_buffer object that owns the data that was sent.
-
-
dynamic_buffer is an aggregate struct that contains a std::unique_ptr<std::byte[]> and a size.
-
-
The first thing we do inside of async_read_array is create a sender that will send a new, empty dynamic_array object using § 4.19.2 execution::just. We can attach more work to the pipeline using operator| composition (see § 4.12 Most sender adaptors are pipeable for details).
-
-
We need the lifetime of this dynamic_array object to last for the entire pipeline. So, we use let_value, which takes an input sender and a std::invocable that must return a sender itself (see § 4.20.4 execution::let_* for details). let_value sends the value from the input sender to the std::invocable. Critically, the lifetime of the sent object will last until the sender returned by the std::invocable completes.
-
-
Inside of the let_valuestd::invocable, we have the rest of our logic. First, we want to initiate an async_read of the buffer size. To do that, we need to send a std::span pointing to buf.size. We can do that with § 4.19.2 execution::just.
Next, we pipe a std::invocable that will be invoked after the async_read completes using § 4.20.2 execution::then.
-
-
That std::invocable gets sent the number of bytes read.
-
-
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 std::span<std::byte> to the storage for the data from the std::invocable. This will be sent to the next recipient in the pipeline.
-
-
And that recipient will be another async_read, which will read the data.
-
-
Once the data has been read, in another § 4.20.2 execution::then, we confirm that we read the right number of bytes.
-
-
Finally, we move out of and return our dynamic_buffer object. It will get sent by the sender returned by async_read_array. We can attach more things to that sender to use the data in the buffer.
-
-
1.3.4. More end-user examples
-
1.3.4.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.3.4.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.
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.
In this code, NN::async_read_some and NN::async_write_some are asynchronous socket-based networking APIs that return senders. EX::repeat_effect_until, EX::let_value, and EX::then and 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 repeat_effect_until expression and all its child operations -- is stored altogether in a single object.
-
1.4. Examples: Algorithms
-
In this section we show a few simple sender/receiver-based algorithm implementations.
-
1.4.1. then
-
// For emulating "deducing this"
-template<classA,classB>
- concept__this=same_as<remove_cvref_t<A>,B>;
-
-template<receiverR,classF>
-struct_then_receiver{
- Rr_;
- Ff_;
-
- // Customize set_value by invoking the callable and passing the result to the inner receiver
- template<class...As>
- requiresreceiver_of<R,invoke_result_t<F,As...>>
- friendvoidtag_invoke(std::execution::set_value_t,_then_receiver&&self,As&&...as){
- std::execution::set_value((R&&)self.r_,invoke((F&&)self.f_,(As&&)as...));
- }
-
- // Forward all other tag_invoke-based CPOs (copy_cvref_t from P1450):
- template<__this<_then_receiver>Self,class...As,invocable<copy_cvref_t<Self,R>,As...>Tag>
- friendautotag_invoke(Tagtag,Self&&self,As&&...as)
- noexcept(is_nothrow_invocable_v<Tag,copy_cvref_t<Self,R>,As...>)
- ->invoke_result_t<Tag,copy_cvref_t<Self,R>,As...>{
- return((Tag&&)tag)(((Self&&)self).r_,(As&&)as...);
- }
-};
-
-template<senderS,classF>
-struct_then_sender:std::execution::sender_base{
- Ss_;
- Ff_;
-
- template<receiverR>
- requiressender_to<S,_then_receiver<R,F>>
- friendautotag_invoke(std::experimental::connect_t,_then_sender&&self,Rr)
- ->std::execution::connect_result_t<S,_then_receiver<R,F>>{
- returnstd::execution::connect((S&&)s_,_then_receiver<R,F>{(R&&)r,(F&&)f_});
- }
-};
-
-template<senderS,classF>
-senderautothen(Ss,Ff){
- return_then_sender{{},(S&&)s,(F&&)f};
-}
-
-
This code builds a then 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 (set_error and set_done), as well as all receiver queries,
-are passed through unchanged.
-
In detail, it does the following:
-
-
-
Defines a receiver that aggregates another receiver and an invocable which:
-
-
-
Defines a constrained tag_invoke overload for transforming the value channel.
-
-
Defines another constrained overload of tag_invoke that passes all other customizations through unchanged.
-
-
-
Defines a sender that aggregates another sender and the invocable, which defines a tag_invoke customization for std::execution::connect that wraps the incoming receiver in the receiver from (1) and passes it and the incoming sender to std::execution::connect, returning the result.
-
-
1.4.2. retry
-
// _conv needed so we can emplace construct non-movable types into
-// a std::optional.
-template<invocableF>
- requiresstd::is_nothrow_move_constructible_v<F>
-struct_conv{
- Ff_;
- explicit_conv(Ff)noexcept:f_((F&&)f){}
- operatorinvoke_result_t<F>()&&{
- return((F&&)f_)();
- }
-};
-
-// pass through all customizations except set_error, which retries the operation.
-template<classO,classR>
-struct_retry_receiver{
- O*o_;
- explicit_retry_receiver(O*o):o_(o){}
- friendvoidtag_invoke(std::execution::set_error_t,_retry_receiver&&self,auto&&)noexcept{
- self.o_->_retry();// This causes the op to be retried
- }
- // Forward all other tag_invoke-based CPOs (copy_cvref_t from P1450):
- template<__this<_retry_receiver>Self,class...As,invocable<copy_cvref_t<Self,R>,As...>Tag>
- friendautotag_invoke(Tagtag,Self&&self,As&&...as)
- noexcept(is_nothrow_invocable_v<Tag,copy_cvref_t<Self,R>,As...>)
- ->invoke_result_t<Tag,copy_cvref_t<Self,R>,As...>{
- return((Tag&&)tag)(((copy_cvref_t<Self,R>&&)self.o_->r_,(As&&)as...);
- }
-};
-
-template<senderS>
-struct_retry_sender:std::execution::sender_base{
- Ss_;
- explicit_retry_sender(Ss):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<receiverR>
- struct_op{
- Ss_;
- Rr_;
- std::optional<state_t<S&,_retry_receiver<_op,R>>>o_;
-
- _op(Ss,Rr):s_((S&&)s),r_((R&&)r),o_{_connect()}{}
- _op(_op&&)=delete;
-
- auto_connect()noexcept{
- return_conv{[this]{
- returnstd::execution::connect(s_,_retry_receiver<_op,R>{this});
- }};
- }
- void_retry()noexcepttry{
- o_.emplace(_connect());// potentially throwing
- std::execution::start(*o_);
- }catch(...){
- std::execution::set_error((R&&)r_,std::current_exception());
- }
- friendvoidtag_invoke(std::execution::start_t,_op&o)noexcept{
- std::execution::start(*o.o_);
- }
- };
-
- template<receiverR>
- requiressender_to<S&,_retry_receiver<_op<R>,R>>
- friend_op<R>tag_invoke(std::execution::connect_t,_retry_sender&&self,Rr){
- return_op<R>{(S&&)self.s_,(R&&)r};
- }
-};
-
-template<senderS>
-senderautoretry(Ss){
- return_retry_sender{(S&&)s};
-}
-
-
The retry 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 optional, which allows us
-to reinitialize it multiple times.
-
This example does the following:
-
-
-
Defines a _conv utility that takes advantage of C++17’s guaranteed copy elision to
-emplace a non-movable type in a std::optional.
-
-
Defines a _retry_receiver 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 set_error, which causes a _retry() function to be called instead.
-
-
Defines an operation state that aggregates the input sender and receiver, and declares
-storage for the nested operation state in a std::optional. Constructing the operation
-state constructs a _retry_receiver with a pointer to the (under construction) operation
-state and uses it to connect to the aggregated sender.
-
-
Starting the operation state dispatches to start on the inner operation state.
-
-
The _retry() 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.
-
-
After reinitializing the inner operation state, _retry() calls start on it, causing
-the failed operation to be rescheduled.
-
-
Defines a _retry_sender that implements the connect customization point to return
-an operation state constructed from the passed-in sender and receiver.
-
-
1.5. Examples: Schedulers
-
In this section we look at some schedulers of varying complexity.
The inline scheduler is a trivial scheduler that completes immediately and synchronously on
-the thread that calls std::execution::start on the operation state produced by its sender.
-In other words, start(connect(schedule(inline-scheduler),receiver)) is
-just a fancy way of saying set_value(receiver), with the exception of the fact that start wants to be passed an lvalue.
-
Although not a particularly useful scheduler, it serves to illustrate the basics of
-implementing one. The inline_scheduler:
-
-
-
Customizes execution::schedule to return an instance of the sender type _sender.
-
-
The _sender type models the typed_sender concept and provides the metadata needed
-to describe it as a sender of no values (see value_types) that can send an exception_ptr as an error (see error_types), and that never calls set_done (see sends_done).
-
-
The _sender type customizes execution::connect to accept a receiver of no values.
-It returns an instance of type _op that holds the receiver by value.
-
-
The operation state customizes std::execution::start to call std::execution::set_value on the receiver, passing any exceptions to std::execution::set_error as an exception_ptr.
-
-
1.5.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 manual_event_loop.
The single_thread_context 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 manual_event_loop 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 state 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 this_thread::sync_wait, 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.6. 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.7. Design changes from P0443
-
-
-
The executor concept has been removed and all of its proposed functionality is now based on schedulers and senders, as per SG1 direction.
-
-
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.
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.8. 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.8.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.8.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.8.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.9. Field experience
-
This proposal draws heavily from our field experience with libunifex. 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.
-
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:
Before this proposal is approved, we will present a new implementation of this proposal written from the specification and intended as a contribution to libc++. This implementation will demonstrate the viability of the design across the use cases and execution contexts that the committee has identified as essential.
-
2. Revision history
-
2.1. R2
-
The changes since R1 are as follows:
-
-
-
Remove the eagerly executing sender algorithms.
-
-
Extend the execution::connect customization point and the sender_traits<> template to recognize awaitables as typed_senders.
-
-
Add utilities as_awaitable() and with_awaitable_senders<> so a coroutine type can trivially make senders awaitable with a coroutine.
-
-
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 bulk algorithm.
-
-
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 execution::just_error and execution::just_done.
-
-
Add sender adaptors execution::done_as_optional and execution::done_as_error.
-
-
Document more production uses of sender/receiver at scale.
-
-
Various fixes of typos and bugs.
-
-
2.2. 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 unschedule adaptor.
-
-
Various fixes of typos and bugs.
-
-
2.3. 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]: std::execution; however, for brevity, the std:: part of this name is omitted. When you see execution::foo, treat that as std::execution::foo.
-
-
Universal references and explicit calls to std::move/std::forward are omitted in code samples and signatures for simplicity; assume universal references and perfect forwarding unless stated otherwise.
-
-
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 std::invocable 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 std::invocable 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, schedule, 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::schedulerautosch=get_thread_pool().scheduler();
-execution::senderautosnd=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 execution::schedule API.
-
Future papers will propose additional scheduler concepts that extend scheduler to add other capabilities. For example:
-
-
-
A time_scheduler concept that extends scheduler to support time-based scheduling.
-Such a concept might provide access to schedule_after(sched,duration), schedule_at(sched,time_point) and now(sched) APIs.
-
-
Concepts that extend scheduler to support opening, reading and writing files asynchronously.
-
-
Concepts that extend scheduler to support connecting, sending data and receiving data over the network asynchronously.
-
-
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.
execution::schedulerautosch=get_thread_pool().scheduler();
-execution::senderautosnd=execution::schedule(sch);
-execution::senderautocont=execution::then(snd,[]{
- std::fstreamfile{"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 execution::connect) senders and return a sender.
-
-
sender consumers, algorithms that take (and potentially execution::connect) senders and do not return a sender.
-
-
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 std::thread (which they aren’t).
-
-
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 on 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
-
get_completion_scheduler is a query that retrieves the completion scheduler for a specific completion signal from a sender.
-Calling get_completion_scheduler 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::schedulerautocpu_sched=new_thread_scheduler{};
-execution::schedulerautogpu_sched=cuda::scheduler();
-
-execution::senderautosnd0=execution::schedule(cpu_sched);
-execution::schedulerautocompletion_sch0=
- execution::get_completion_scheduler<execution::set_value_t>(snd0);
-// completion_sch0 is equivalent to cpu_sched
-
-execution::senderautosnd1=execution::then(snd0,[]{
- std::cout<<"I am running on cpu_sched!\n";
-});
-execution::schedulerautocompletion_sch1=
- execution::get_completion_scheduler<execution::set_value_t>(snd1);
-// completion_sch1 is equivalent to cpu_sched
-
-execution::senderautosnd2=execution::transfer(snd1,gpu_sched);
-execution::senderautosnd3=execution::then(snd2,[]{
- std::cout<<"I am running on gpu_sched!\n";
-});
-execution::schedulerautocompletion_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 execution::schedule, 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 execution::transfer sender adaptor performs a transition from one execution context to another:
-
execution::schedulerautosch1=...;
-execution::schedulerautosch2=...;
-
-execution::senderautosnd1=execution::schedule(sch1);
-execution::senderautothen1=execution::then(snd1,[]{
- std::cout<<"I am running on sch1!\n";
-});
-
-execution::senderautosnd2=execution::transfer(then1,sch2);
-execution::senderautothen2=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 std::unique_ptr that it will be transferring ownership of to the
-operation-state returned by a call to execution::connect so that the operation has access to
-this resource. In such a sender, calling execution::connect 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 execution::connect only has overloads for an rvalue-qualified sender. Callers must pass the sender
-as an rvalue to the call to execution::connect, 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 execution::connect to accept an lvalue reference to the
-sender. Callers can indicate that they want the sender to remain valid after the call to execution::connect by passing an lvalue reference to the sender to call these overloads. Multi-shot senders should also define
-overloads of execution::connect 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 execution::connect.
-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 execution::connect. 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 execution::connect on the input sender, such as in this_thread::sync_wait or execution::start_detached.
-
Some multi-use sender algorithms may require that an input sender be copy-constructible but will only call execution::connect 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 execution::connect 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 split sender adaptor facilitates connecting to a sender multiple times, regardless of whether it is single-shot or multi-shot:
-
autosome_algorithm(execution::senderauto&&input){
- execution::senderautomulti_shot=split(input);
- // "multi_shot" is guaranteed to be multi-shot,
- // regardless of whether "input" was multi-shot or not
-
- returnwhen_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.
-
when_all 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. when_all returns a sender that also does not have an associated scheduler.
-
transfer_when_all 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 when_all. You can think of it as a composition of transfer(when_all(inputs...),scheduler), 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 execution::connect() before calling execution::start(), 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 when_all(ops...) algorithm should cancel other operations as soon as one operation fails
-
-
a first_successful(ops...) algorithm should cancel the other operations as soon as one operation completes successfuly
-
-
a generic timeout(src,duration) algorithm needs to be able to cancel the src operation after the timeout duration has elapsed.
-
-
a stop_when(src,trigger) algorithm should cancel src if trigger completes first and cancel trigger if src completes first
-
-
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:
In this example, if we take the operation returned by query_server_b(query), this operation will
-receive a stop-request when any of the following happens:
-
-
-
first_successful algorithm will send a stop-request if query_server_a(query) completes successfully
-
-
when_all algorithm will send a stop-request if the load_file("some_file.jpg") operation completes with an error or done result.
-
-
timeout algorithm will send a stop-request if the operation does not complete within 5 seconds.
-
-
stop_when algorithm will send a stop-request if the user clicks on the "Cancel" button in the user-interface.
-
-
The parent operation consuming the composed_cancellation_example() sends a stop-request
-
-
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 std::stop_token-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 std::stoppable_token and std::stoppable_token_for concepts that generalise the interface of std::stop_token type to allow other types with different implementation strategies.
-
-
Add std::unstoppable_token concept for detecting whether a stoppable_token can never receive a stop-request.
-
-
Add std::in_place_stop_token, std::in_place_stop_source and std::in_place_stop_callback<CB> types that provide a more efficient implementation of a stop-token for use in structured concurrency situations.
-
-
Add std::never_stop_token for use in places where you never want to issue a stop-request
-
-
Add std::execution::get_stop_token() CPO for querying the stop-token to use for an operation from its receiver.
-
-
Add std::execution::stop_token_type_t<T> for querying the type of a stop-token returned from 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 execution::get_stop_token() CPO.
-This CPO takes a single parameter, which is the receiver passed to execution::connect, and returns a std::stoppable_token that the operation should use to check for stop-requests for that operation.
-
As the caller of execution::connect typically has control over the receiver type it passes, it is able to customise
-the execution::get_stop_token() 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 execution::get_stop_token() CPO then invoking the CPO on that receiver will
-invoke the default implementation which returns std::never_stop_token. This is a special stoppable_token type that
-is statically known to always return false from the stop_possible() 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 execution::get_stop_token(), 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 execution::start() 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 execution::start() 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 execution::start() implementation.
-
An implementation of execution::start() 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 execution::start 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 execution::start() call (which is still executing).
-
For an example of an implementation strategy for solving these data-races see the async_recv() example in Appendix A.
-
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. 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, get_forward_progress_guarantee, which returns one of the enumerators of a new enum type, forward_progress_guarantee. Each enumerator of forward_progress_guarantee corresponds to one of the aforementioned
-guarantees.
-
4.12. Most sender adaptors are pipeable
-
To facilitate an intuitive syntax for composition, most sender adaptors are pipeable; they can be composed (piped) together with operator|.
-This mechanism is similar to the operator| 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.
-
a|b will pass the sender a as the first argument to the pipeable sender adaptor b. Pipeable sender adaptors support partial application of the parameters after the first. For example, all of the following are equivalent:
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:
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.
-
-
-
execution::when_all and execution::when_all_with_variant: 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::on: 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 to 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.13. 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 R that is a range of senders, consider the following in a coroutine that returns an async generator type:
This transforms each element of the asynchronous sequence R with the function fn on demand, as the data arrives. The result is a new asynchronous sequence of the transformed values.
-
Now imagine that R is the simple expression views::iota(0)|views::transform(execution::just). 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 [fn(0), fn(1), fn(2), ...].
-
Far more interesting would be if R 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.14. 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:
- returnread_socket_async(socket,span{buff})
- // For success (partial and full), specify the next action:
- |execution::let_value([](error_codeerr,size_tbytes_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(autosnd:read_socket_async(socket,span{buff})){
- try{
- if(optional<size_t>bytes_read=
- co_awaitexecution::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.15. 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 typed_sender 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 await_transform trickery from a promise type to make them awaitable.)
-
For an example, imagine a coroutine type called task<T> that knows nothing about senders. It doesn’t implement any of the sender customization points. Despite that fact, and despite the fact that the this_thread::sync_wait algorithm is constrained with the typed_sender concept, the following would compile and do what the user wants:
-
task<int>doSomeAsyncWork();
-
-intmain(){
- // OK, awaitable types satisfy the requirements for typed senders:
- autoo=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.16. 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 execution::with_awaitable_senders 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 retry algorithm:
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 with_awaitable_senders 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 into_variant sender algorithm -- before co_await-ing that sender.
-
4.17. Cancellation of a sender can unwind a stack of coroutines
-
When looking at the sender-based retry 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 execution::set_done?
-
When your task type’s promise inherits from with_awaitable_senders, what happens is this: the coroutine behaves as if an uncatchable exception had been thrown from the co_await 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 with_awaitable_senders, or more generally implement a member function called unhandled_done, the exception unwinds the chain of coroutines as if an exception were thrown except that it bypasses catch(...) 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 execution::let_done, execution::upon_done, execution::done_as_optional, or execution::done_as_error sender adaptors. For instance, we can use execution::done_as_optional to "catch" the done signal and map it into an empty optional as shown below:
-
if(autoopt=co_awaitexecution::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 connect-ing an awaitable and a receiver, the adaptation layer awaits the awaitable within a coroutine that implements unhandled_done in its promise type. The effect of this is that an "uncatchable" done exception propagates seamlessly out of awaitables, causing execution::set_done to be called on the receiver.
-
Obviously, unhandled_done is a library extension of the coroutine promise interface. Many promise types will not implement unhandled_done. When an uncatchable done exception tries to propagate through such a coroutine, it is treated as an unhandled exception and terminate 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 unhandled_done. The author of [P1056R1], which proposes a standard coroutine task type, is in agreement.
-
4.18. 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 execution::par, 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.
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 policy while using scheduler to create any work to be run. The expression may be ill-formed if scheduler 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 executing_on 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 void 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.19. User-facing sender factories
-
A sender factory is an algorithm that takes no senders as parameters and returns a sender.
execution::schedulerautosch1=get_system_thread_pool().scheduler();
-
-execution::senderautosnd1=execution::schedule(sch1);
-// snd1 describes the creation of a new task on the system thread pool
-
Returns a sender with no completion schedulers, which sends the provided values. If a provided value is an lvalue reference, a copy is made inside the returned sender and a non-const lvalue reference to the copy is sent. If the provided value is an rvalue reference, it is moved into the returned sender and an rvalue reference to it is sent.
Returns a sender whose value completion scheduler is the provided scheduler, which sends the provided values in the same manner as just.
-
execution::senderautovals=execution::transfer_just(
- get_system_thread_pool().scheduler(),
- 1,2,3
-);
-execution::senderautosnd=execution::then(vals,[](auto...args){
- std::print(args...);
-});
-// when snd is executed, it will print "123"
-
-
This adaptor is included as it greatly simplifies lifting values into senders.
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 set_error. 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 set_error.
-
4.19.5. execution::just_done
-
execution::senderautojust_done();
-
-
Returns a sender with no completion schedulers, which completes immediately by calling the receiver’s set_done.
-
4.20. User-facing sender adaptors
-
A sender adaptor is an algorithm that takes one or more senders, which it may execution::connect, as parameters, and returns a sender, whose completion is related to the sender arguments it has received.
execution::schedulerautocpu_sched=get_system_thread_pool().scheduler();
-execution::schedulerautogpu_sched=cuda::scheduler();
-
-execution::senderautocpu_task=execution::schedule(cpu_sched);
-// cpu_task describes the creation of a new task on the system thread pool
-
-execution::senderautogpu_task=execution::transfer(cpu_task,gpu_sched);
-// gpu_task describes the transition of the task graph described by cpu_task to the gpu
-
then 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.
-
then is guaranteed to not begin executing function until the returned sender is started.
-
execution::senderautoinput=get_input();
-execution::senderautosnd=execution::then(input,[](auto...args){
- std::print(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.
upon_error and upon_done are similar to then, but where then works with values sent by the input sender, upon_error works with errors, and upon_done is invoked when the "done" signal is sent.
let_value is very similar to then: 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 then sends exactly what that function ends up returning - let_value requires that the function return a sender, and the sender returned by let_value 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.
-
let_value is guaranteed to not begin executing function until the returned sender is started.
-
let_error and let_done are similar to let_value, but where let_value works with values sent by the input sender, let_error works with errors, and let_done is invoked when the "done" signal is sent.
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.
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.
Returns a sender that maps the value channel from a T to an optional<decay_t<T>>, and maps the done channel to a value of an empty optional<decay_t<T>>.
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 function will begin executing until the returned sender is started. Each invocation of function 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 bulk execute with the same guarantee. This allows, for instance, a scheduler to execute all invocations of the function in parallel.
-
The bulk operation is intended to be used at the point where the number of agents to be created is known and provided to bulk via its shape 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, bulk can be combined with additional operations such as let_value to deliver dynamic shape information to the bulk 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.
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.
when_all returns a sender which completes once all of the input senders have completed. The values send by this sender are the values sent by each of the input, in order of the arguments passed to when_all.
-
when_all_with_variant does the same, but it adapts all the input senders using into_variant.
execution::schedulerautosched=get_thread_pool().scheduler();
-
-execution::senderautosends_1=...;
-execution::senderautosends_abc=...;
-
-execution::senderautoboth=execution::when_all(sched,
- sends_1,
- sends_abc
-);
-
-execution::senderautofinal=execution::then(both,[](auto...args){
- std::cout<<std::format("the two args: {}, {}",args...);
-});
-// when final executes, it will print "the two args: 1, abc"
-
Once ensure_started returns, it is known that the provided sender has been connected and start 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 execution::connect() is called, or if execution::connect() is called but the
-returned operation-state is destroyed before execution::start() 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 std::shared_ptr 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.21. User-facing sender consumers
-
A sender consumer is an algorithm that takes one or more senders, which it may execution::connect, as parameters, and does not return a sender.
this_thread::sync_wait is a sender consumer that submits the work described by the provided sender for execution, similarly to ensure_started, except that it blocks the current std::thread or thread of main 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.19.1 execution::schedule and § 4.19.3 execution::transfer_just are meant to enter the domain of senders, sync_wait 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, sync_wait throws that error as an exception, or rethrows the original exception if the error is of type std::exception_ptr.
-
If the provided sender sends the "done" signal instead of values, sync_wait returns an empty optional.
-
For an explanation of the requires clause, see § 5.8 Most senders are typed. That clause also explains another sender consumer, built on top of sync_wait: sync_wait_with_variant.
-
Note: This function is specified inside std::this_thread, and not inside execution. This is because sync_wait 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 std::this_thread, 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 std::this_fiber::sync_wait would be provided. We also expect that runtimes with execution agents that use different synchronization mechanisms than std::thread's will provide their own flavors of sync_wait as well (assuming their execution agents have the means
-to block in a non-deadlock manner).
-
4.22. 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.
A receiver is a callback that supports more than one channel. In fact, it supports three of them:
-
-
-
set_value, which is the moral equivalent of an operator() or a function call, which signals successful completion of the operation its execution depends on;
-
-
set_error, 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; and
-
-
set_done, which signals that the operation completed without succeeding (set_value) and without failing (set_error). 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.
-
-
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 set_value failed with an exception, either set_error or set_done 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 std::promise, which provides the first two signals as set_value and set_error, 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.
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: start, 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 execution::ensure_started and this_thread::sync_wait, 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.
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 connect will be fulfilled.
-
execution::senderautosnd=someinputsender;
-execution::receiverautorcv=somereceiver;
-execution::operation_stateautostate=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 then, 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 execution::then(sender,invocable) to be equivalent to:
-
-
-
sender.then(invocable), if that expression is well formed; otherwise
-
-
then(sender,invocable), performed in a context where this call always performs ADL, if that expression is well formed; otherwise
-
-
a default implementation of then, which returns a sender adaptor, and then define the exact semantics of said adaptor.
-
-
However, this definition is problematic. Imagine another sender adaptor, bulk, 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 bulk to have specialized behavior, which invokes a kernel of more than one thread (with its size defined by the call to bulk); therefore, we would like to customize bulk for CUDA senders to achieve this. However, there’s no reason for CUDA kernels to
-necessarily customize the then sender adaptor, as the generic implementation is perfectly sufficient. This creates a problem, though; consider the following snippet:
-
execution::schedulerautocuda_sch=cuda_scheduler{};
-
-execution::senderautoinitial=execution::schedule(cuda_sch);
-// the type of initial is a type defined by the cuda_scheduler
-// let’s call it cuda::schedule_sender<>
-
-execution::senderautonext=execution::then(cuda_sch,[]{return1;});
-// 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::senderautokernel_sender=execution::bulk(next,shape,[](inti){...});
-
-
How can we specialize the bulk sender adaptor for our wrapped schedule_sender? 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):
However, if the input sender is not just a then_sender_adaptor like in the example above, but another sender that overrides bulk 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 execution::<sender-algorithm>(sender,args...), for any given sender algorithm that accepts a sender as its first argument, should be
-equivalent to:
-
-
-
tag_invoke(<sender-algorithm>,get_completion_scheduler<Signal>(sender),sender,args...), if that expression is well-formed; otherwise
-
-
tag_invoke(<sender-algorithm>,sender,args...), if that expression is well-formed; otherwise
-
-
a default implementation, if there exists a default implementation of the given sender algorithm.
-
-
where Signal is one of set_value, set_error, or set_done; for most sender algorithms, the completion scheduler for set_value would be used, but for some (like upon_error or let_done), one of the others would be used.
-
For sender algorithms which accept concepts other than sender as their first argument, we propose that the customization scheme remains as it has been in [P0443R14] so far, except it should also use tag_invoke.
-
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.20.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 execution::start 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.20.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.20.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 execution::transfer 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 transfer 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 transfer.
-
To allow for such customization from both ends, we propose the inclusion of a secondary transitioning sender adaptor, called schedule_from. This adaptor is a form of schedule, 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 transfer, to ensure that both schedulers have a say in how the transitions are made. Any scheduler that specializes transfer(snd,sch) shall ensure that the
-return value of their customization is equivalent to schedule_from(sch,snd2), where snd2 is a successor of snd that sends values equivalent to those sent by snd.
-
The default implementation of transfer(snd,sched) is schedule_from(sched,snd).
-
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 execution::then is
-hard.
-
The mechanism for this advertisement is the same as in [P0443R14]; the way to query the types is through sender_traits::value_types<tuple_like,variant_like>.
-
sender_traits::value_types 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 when_all). 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.21.2 this_thread::sync_wait: it returns a tuple of values sent by the sender passed to it, wrapped in std::optional to handle the set_done signal. However, this assumes that those values can be represented as a
-tuple, like here:
-
execution::senderautosends_1=...;
-execution::senderautosends_2=...;
-execution::senderautosends_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 value_types of a sender which sends Types... to be as follows:
If senders could only ever send one specific set of values, this would probably need to be the required form of value_types 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) set_value 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 value_types, one that represents a variant-like type. If we permit such senders, we would almost certainly need to require that the canonical form of value_types 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 Types1..., Types2..., ..., TypesN... to be as follows:
This, however, introduces a couple of complications:
-
-
-
A just(1) sender would also need to follow this structure, so the correct type for storing the value sent by it would be std::variant<std::tuple<int>> 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 where value_types 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.
-
-
As a consequence of (1): because sync_wait needs to store the above type, it can no longer return just a std::tuple<int> for just(1); it has to return std::variant<std::tuple<int>>. 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 of sync_wait must be the same across all sender types.
-
-
One possible solution to (2) above is to place a requirement on sync_wait that it can only accept senders which send only a single set of values, therefore removing the need for std::variant to appear in its API; because of this, we propose to expose both sync_wait, which is a simple, user-friendly version of the sender consumer, but requires that value_types have only one possible variant, and sync_wait_with_variant, which accepts any sender, but returns an optional whose value type is the variant of all the
-possible tuples sent by the input sender:
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: sync_wait. We imagine that if, in the future, C++ was to
-gain fibers support, we would want to also have std::this_fiber::sync_wait, in addition to std::this_thread::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.
-
-
This paper proposes to instead use the mechanism described in [P1895R0]: tag_invoke; the wording for tag_invoke has been incorporated into the proposed specification in this paper.
-
In short, instead of using globally reserved names, tag_invoke uses the type of the customization point object itself as the mechanism to find customizations. It globally reserves only a single name - tag_invoke - which itself is used the same way that
-ranges-style customization points are used. All other customization points are defined in terms of tag_invoke. For example, the customization for std::this_thread::sync_wait(s) will call tag_invoke(std::this_thread::sync_wait,s), instead of attempting
-to invoke s.sync_wait(), and then sync_wait(s) if the member call is not valid.
-
Using tag_invoke 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<typenameTag,typename...Args>
-friendautotag_invoke(Tag&&tag,wrapper&self,Args&&...args){
- returnstd::forward<Tag>(tag)(self.subobject,std::forward<Args>(args)...);
-}
-
-// but override one of them with a specific value
-friendautotag_invoke(specific_customization_point_t,wrapper&self){
- returnself.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 tag_invoke, dispatching to manually constructed vtables containing pointers to specialized implementations for the wrapped objects. For an example of such a polymorphic wrapper, see unifex::any_unique (example).
-
-
6. Specification
-
Much of this wording follows the wording of [P0443R14].
Insert this section as a new subclause, between Searchers [func.search] and Class template hash[unord.hash].
-
-
-
-
-
The name std::tag_invoke denotes a customization point object. For some subexpressions tag and args..., tag_invoke(tag,args...) is expression-equivalent to an unqualified call to tag_invoke(decay-copy(tag),args...) with overload
-resolution performed in a context that includes the declaration:
-
voidtag_invoke();
-
-
and that does not include the the std::tag_invoke name.
-
-
-
-
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 <stop_token>. 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.
Insert this section as a new subclause between Header <stop_token> synopsis [thread.stoptoken.syn] and Class stop_token[stoptoken].
-
-
-
-
-
The stoppable_token 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, T::callback_type<CB>, 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. The stoppable_token_for concept checks for a stop token type compatible with a given
-callback type. The unstoppable_token concept checks for a stop token type that does not allow stopping.
Let t and u be distinct object of type T. The type T models stoppable_token only if:
-
-
-
All copies of a stoppable_token reference the same logical shared stop state and shall report values consistent with each other.
-
-
If t.stop_possible() evaluates to false then, if u, references the same logical shared stop state, u.stop_possible() shall also subsequently evaluate to false and u.stop_requested() shall also subsequently evaluate to false.
-
-
If t.stop_requested() evaluates to true then, if u, references the same logical shared stop state, u.stop_requested() shall also subsequently evaluate to true and u.stop_possible() shall also subsequently evaluate to true.
-
-
Given a callback-type, CB, and a callback-initializer argument, init, of type Initializer then constructing an instance, cb, of type T::callback_type<CB>, passing t as the first argument and init as the second argument to the constructor, shall,
-if t.stop_possible() is true, construct an instance, callback, of type CB, direct-initialized with init, and register callback with t’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.
-
-
-
If t.stop_requested() is true at the time callback is registered then callback may be invoked immediately inline inside the call to cb’s constructor.
-
-
If callback is invoked then, if u references the same shared stop state as t, an evaluation of u.stop_requested() will be true if the beginning of the invocation of callback strongly-happens-before the evaluation of u.stop_requested().
-
-
If t.stop_possible() evaluates to false then the construction of cb is not required to construct and initialize callback.
-
-
-
Construction of a T::callback_type<CB> instance shall only throw exceptions thrown by the initialization of the CB instance from the value of type Initializer.
-
-
Destruction of the T::callback_type<CB> object, cb, removes callback from the shared stop state such that callback will not be invoked after the destructor returns.
-
-
-
If callback is currently being invoked on another thread then the destructor of cb will block until the invocation of callback returns such that the return from the invocation of callback strongly-happens-before the destruction of callback.
-
-
Destruction of a callback cb shall not block on the completion of the invocation of some other callback registered with the same shared stop state.
-
-
-
-
-
-
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.
-
-
-
Table 1: Execution control library summary [tab:execution.summary]
None of a scheduler’s copy constructor, destructor, equality comparison, or swap member functions shall exit via an exception.
-
-
None of these member functions, nor a scheduler type’s schedule function, shall introduce data races as a result of concurrent invocations of those functions from different
-threads.
-
-
For any two (possibly const) values s1 and s2 of some scheduler type S, s1==s2 shall return true only if both s1 and s2 are handles to the same associated execution context.
-
-
A scheduler type’s destructor shall not block pending completion of any receivers connected to the sender objects returned from schedule. [Note: The ability to wait for completion of submitted function objects may be provided by the associated execution
-context of the scheduler. —end note]
execution::get_forward_progress_guarantee is used to ask a scheduler about the forward progress guarantees of execution agents created by that scheduler.
-
-
The name execution::get_forward_progress_guarantee denotes a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::scheduler, execution::get_forward_progress_guarantee is ill-formed.
-Otherwise, execution::get_forward_progress_guarantee(s) is expression equivalent to:
-
-
-
tag_invoke(execution::get_forward_progress_guarantee,as_const(s)), if this expression is well formed and its type is execution::forward_progress_guarantee, and is noexcept.
If execution::get_forward_progress_guarantee(s) for some scheduler s returns execution::forward_progress_guarantee::concurrent, all execution agents created by that scheduler shall provide the concurrent forward progress guarantee. If it returns execution::forward_progress_guarantee::parallel, all execution agents created by that scheduler shall provide at least the parallel forward progress guarantee.
this_thread::execute_may_block_caller is used to ask a scheduler s whether a call execution::execute(s,f) with any invocable f may block the thread where such a call occurs.
-
-
The name this_thread::execute_may_block_caller denotes a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::scheduler, this_thread::execute_may_block_caller is ill-formed. Otherwise, this_thread::execute_may_block_caller(s) is expression equivalent to:
-
-
-
tag_invoke(this_thread::execute_may_block_caller,as_const(s)), if this expression is well formed and its type is bool, and is noexcept.
-
-
Otherwise, true.
-
-
-
If this_thread::execute_may_block_caller(s) for some scheduler s returns false, no execution::execute(s,f) call with some invocable f shall block the calling thread.
-
-
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, set_error, and set_done. These are collectively known as a receiver’s completion-signal operations.
-
-
The receiver concept defines the requirements for a receiver type with an unknown set of value types. The receiver_of concept defines the requirements for a receiver type with a known set of value types, whose error type is std::exception_ptr.
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 execution::start has been called on the operation state object that was returned by execution::connect to connect that receiver to a sender.
-
-
Once execution::start 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.
-
-
If execution::set_value exits with an exception, it is still valid to call execution::set_error or execution::set_done on the receiver, but it is no longer valid to call execution::set_value on the receiver.
-
-
-
Once one of a receiver’s completion-signal operations has completed non-exceptionally, the receiver contract has been satisfied.
execution::set_value is used to send a value completion signal to a receiver.
-
-
The name execution::set_value denotes a customization point object. The expression execution::set_value(R,Vs...) for some subexpressions R and Vs... is expression-equivalent to:
-
-
-
tag_invoke(execution::set_value,R,Vs...), if that expression is valid. If the function selected by tag_invoke does not send the value(s) Vs... to the receiver R’s value channel, the program is ill-formed with no diagnostic required.
-
-
Otherwise, execution::set_value(R,Vs...) is ill-formed.
execution::set_error is used to send a error signal to a receiver.
-
-
The name execution::set_error denotes a customization point object. The expression execution::set_error(R,E) for some subexpressions R and E is expression-equivalent to:
-
-
-
tag_invoke(execution::set_error,R,E), if that expression is valid. If the function selected by tag_invoke does not send the error E to the receiver R’s error channel, the program is ill-formed with no diagnostic required.
-
-
Otherwise, execution::set_error(R,E) is ill-formed.
execution::set_done is used to send a done signal to a receiver.
-
-
The name execution::set_done denotes a customization point object. The expression execution::set_done(R) for some subexpression R is expression-equivalent to:
-
-
-
tag_invoke(execution::set_done,R), if that expression is valid. If the function selected by tag_invoke does not signal the receiver R’s done channel, the program is ill-formed with no diagnostic required.
execution::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]
-
-
The name execution::get_scheduler denotes a customization point object. For some subexpression r, let R be decltype((r)). If R does not satisfy execution::receiver, execution::get_scheduler is ill-formed. Otherwise, execution::get_scheduler(r) is
-expression equivalent to:
-
-
-
tag_invoke(execution::get_scheduler,as_const(r)), if this expression is well formed and satisfies execution::scheduler, and is noexcept.
-
-
Otherwise, execution::get_scheduler(r) is ill-formed.
execution::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]
-
-
The name execution::get_allocator denotes a customization point object. For some subexpression r, let R be decltype((r)). If R does not satisfy execution::receiver, execution::get_allocator is ill-formed. Otherwise, execution::get_allocator(r) is
-expression equivalent to:
-
-
-
tag_invoke(execution::get_allocator,as_const(r)), if this expression is well formed and models Allocator, and is noexcept.
-
-
Otherwise, execution::get_allocator(r) is ill-formed.
execution::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]
-
-
The name execution::get_stop_token denotes a customization point object. For some subexpression r, let R be decltype((r)). If R does not satisfy execution::receiver, execution::get_stop_token is ill-formed. Otherwise, execution::get_stop_token(r) is expression equivalent to:
-
-
-
tag_invoke(execution::get_stop_token,as_const(r)), if this expression is well formed and satisfies stoppable_token, and is noexcept.
-
-
Otherwise, never_stop_token{}.
-
-
-
Let r be a receiver, s be a sender, and op_state be an operation state resulting from an execution::connect(s,r) call. Let token be a stop token resulting from an execution::get_stop_token(r) call. token must remain valid at least until a call to
-a receiver completion-signal function of r returns successfully. [Note: this means that, unless it knows about further guarantees provided by the receiver r, the implementation of op_state should not use token after it makes a call to a receiver
-completion-signal function of r. This also implies that stop callbacks registered on token by the implementation of op_state or s must be destroyed before such a call to a receiver completion-signal function of r. --end note]
-
-
9.5. Operation states [execution.op_state]
-
-
-
The operation_state concept defines the requirements for an operation state type, which allows for starting the execution of work.
execution::start is used to start work represented by an operation state object.
-
-
The name execution::start denotes a customization point object. The expression execution::start(O) for some lvalue subexpression O is expression-equivalent to:
-
-
-
tag_invoke(execution::start,O), if that expression is valid. If the function selected by tag_invoke does not start the work represented by the operation state O, the program is ill-formed with no diagnostic required.
-
-
Otherwise, execution::start(O) is ill-formed.
-
-
-
The caller of execution::start(O) must guarantee that the lifetime of the operation state object O extends at least until one of the receiver completion-signal functions of a receiver R passed into the execution::connect call that produced O 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]
-
-
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 sender concept defines the requirements for a sender type. The sender_to concept defines the requirements for a sender type capable of being connected with a specific receiver type.
The class sender_base is used as a base class to tag sender types which do not expose member templates value_types, error_types, and a static member constant expression sends_done.
-
-
The class template sender_traits is used to query a sender type for facts associated with the signal it sends.
-
-
The primary class template sender_traits<S> also recognizes awaitables as typed senders. For this clause ([execution]):
-
-
-
An awaitable is an expression that would be well-formed as the operand of a co_await expression within a coroutine that does not define an await_transform member in its promise type.
-
-
For any type T, is-awaitable<T> is true if an expression of that type is an awaitable as described above; otherwise, false.
-
-
For an awaitable a such that decltype((a)) is type A, await-result-type<A> is an alias for decltype(e), where e is a's await-resume expression ([expr.await]).
-
-
-
The primary class template sender_traits<S> is defined as if inheriting from an implementation-defined class template sender-traits-base<S> defined as follows:
-
-
-
If has-sender-types<S> is true, then sender-traits-base<S> is equivalent to:
template<classS>
- structsender-traits-base{
- using__unspecialized=void;// exposition only
- };
-
-
-
-
If sender_traits<S>::value_types<Tuple,Variant> for some sender type S is well formed, it shall be a type Variant<Tuple<Args0...>,Tuple<Args1...>,...,Tuple<ArgsN...>>>, where the type packs Args0 through ArgsN are the packs of types the sender S passes as
-arguments to execution::set_value after a receiver object. If such sender S invokes execution::set_value(r,args...) for some receiver r, where decltype(args) is not one of the type packs Args0 through ArgsN, the program is ill-formed with no
-diagnostic required.
-
-
If sender_traits<S>::error_types<Variant> for some sender type S is well formed, it shall be a type Variant<E0,E1,...,EN>, where the types E0 through EN are the types the sender S passes as arguments to execution::set_error after a receiver
-object. If such sender S invokes execution::set_error(r,e) for some receiver r, where decltype(e) is not one of the types E0 through EN, the program is ill-formed with no diagnostic required.
-
-
If sender_traits<S>::sends_done is well formed and false, and such sender S invokes execution::set_done(r) for some receiver r, the program is ill-formed with no diagnostic required.
-
-
Users may specialize sender_traits on program-defined types.
execution::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.
-
-
The name execution::connect denotes a customization point object. For some subexpressions s and r, let S be decltype((s)) and R be decltype((r)). If R does not satisfy execution::receiver or S does not satisfy execution::sender, execution::connect(s,r) is ill-formed. Otherwise, the expression execution::connect(s,r) is expression-equivalent to:
-
-
-
tag_invoke(execution::connect,s,r), if that expression is valid and its type satisfies execution::operation_state. If the function selected by tag_invoke does not return an operation state for which execution::start starts work described by s, the program
-is ill-formed with no diagnostic required.
-
-
Otherwise, connect-awaitable(s,r) if is-awaitable<S> is true and that expression is valid, where connect-awaitable is a coroutine equivalent to the following:
where connect-awaitable suspends at the initial suspends point ([dcl.fct.def.coroutine]), and:
-
-
-
set-value-expr first evaluates co_await(S&&)s, then suspends the coroutine and evaluates execution::set_value((R&&)r) if await-result-type<S> is cvvoid; otherwise, it evaluates auto&&res=co_await(S&&)s, then suspends the coroutine and evaluates execution::set_value((R&&)r,(decltype(res))res).
-
If the call to execution::set_value exits with an exception, the coroutine is resumed and the exception is immediately propagated in the context of the coroutine.
-
[Note: If the call to execution::set_value exits normally, then the connect-awaitable coroutine is never resumed. --end note]
-
-
set-error-expr first suspends the coroutine and then executes execution::set_error((R&&)r,std::move(e)).
-
[Note: The connect-awaitable coroutine is never resumed after the call to execution::set_error. --end note]
-
-
operation-state-task is a type that models operation_state. Its execution::start resumes the connect-awaitable coroutine, advancing it past the initial suspend point.
-
-
Let p be an lvalue reference to the promise of the connect-awaitable coroutine, let b be a const lvalue reference to the receiver r, and let c be any customization point object excluding those of type set_value_t, set_error_t and set_done_t. Then std::tag_invoke(c,p,as...) is expression-equivalent to c(b,as...) for any set of arguments as....
-
-
The expression p.unhandled_done() is expression-equivalent to (execution::set_done((R&&)r),noop_coroutine()).
-
-
The operand of the requires-clause of connect-awaitable is equivalent to receiver_of<R> if await-result-type<S> is cvvoid; otherwise, it is receiver_of<R,await-result-type<S>>.
-
-
Otherwise, execution::connect(s,r) is ill-formed.
-
-
-
Standard sender types shall always expose an rvalue-qualified overload of a customization of execution::connect. Standard sender types shall only expose an lvalue-qualified overload of a customization of execution::connect if they are copyable.
execution::get_completion_scheduler is used to ask a sender object for the completion scheduler for one of its signals.
-
-
The name execution::get_completion_scheduler denotes a customization point object template. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::sender, execution::get_completion_scheduler<CPO>(s) is ill-formed for all template arguments CPO. If the template
-argument CPO in execution::get_completion_scheduler<CPO> is not one of execution::set_value_t, execution::set_error_t, or execution::set_done_t, execution::get_completion_scheduler<CPO> is ill-formed. Otherwise, execution::get_completion_scheduler<CPO>(s) is expression-equivalent to:
-
-
-
tag_invoke(execution::get_completion_scheduler<CPO>,as_const(s)), if this expression is well formed and satisfies execution::scheduler, and is noexcept.
-
-
Otherwise, execution::get_completion_scheduler<CPO>(s) is ill-formed.
-
-
-
If, for some sender s and customization point object CPO, execution::get_completion_scheduler<decltype(CPO)>(s) is well-formed and results in a scheduler sch, and the sender s invokes CPO(r,args...), for some receiver r which has been connected to s, with additional arguments args..., on an execution agent which does not belong to the associated execution context of sch, the behavior is undefined.
execution::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.
-
-
The name execution::schedule denotes a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::scheduler, execution::schedule is ill-formed. Otherwise, the expression execution::schedule(s) is expression-equivalent to:
-
-
-
tag_invoke(execution::schedule,s), if that expression is valid and its type satisfies execution::sender. If the function selected by tag_invoke does not return a sender whose set_value completion scheduler is equivalent to s, the program is ill-formed with no
-diagnostic required.
-
-
Otherwise, execution::schedule(s) is ill-formed.
-
-
-
9.6.4.3. execution::just[execution.senders.just]
-
-
-
execution::just is used to create a sender that propagates a set of values to a connected receiver.
execution::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.
-
-
The name execution::transfer_just denotes a customization point object. For some subexpressions s and vs..., let S be decltype((s)) and Vs... be decltype((vs)). If S does not satisfy execution::scheduler, or any type V in Vs does not
-satisfy moveable-value, execution::transfer_just(s,vs...) is ill-formed. Otherwise, execution::transfer_just(s,vs...) is expression-equivalent to:
-
-
-
tag_invoke(execution::transfer_just,s,vs...), if that expression is valid and its type satisfies execution::typed_sender. If the function selected by tag_invoke does not return a sender whose set_value completion scheduler is equivalent to s and sends
-values equivalent to vs... to a receiver connected to it, the program is ill-formed with no diagnostic required.
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 execution::connect, and execution::start is called on the resulting operation state. This requirement applies to any function that is selected by the implementation of the sender adaptor.
-
-
Unless otherwise specified, all sender adaptors which accept a single sender 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.
-
-
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 execution::connect. This requirements
-applies to any sender returned from a function that is selected by the implementation of such sender adaptor.
A pipeable sender adaptor closure object is a function object that accepts one or more sender arguments and returns a sender. For a sender adaptor closure object C and an expression S such that decltype((S)) models sender, the following
-expressions are equivalent and yield a sender:
-
C(S)
-S|C
-
-
Given an additional pipeable sender adaptor closure object D, the expression C|D produces another pipeable sender adaptor closure object E:
-
E is a perfect forwarding call wrapper ([func.require]) with the following properties:
-
-
-
Its target object is an object d of type decay_t<decltype((D))> direct-non-list-initialized with D.
-
-
It has one bound argument entity, an object c of type decay_t<decltype((C))> direct-non-list-initialized with C.
-
-
Its call pattern is d(c(arg)), where arg is the argument used in a function call expression of E.
-
-
The expression C|D is well-formed if and only if the initializations of the state entities of E are all well-formed.
-
-
An object t of type T is a pipeable sender adaptor closure object if T models derived_from<sender_adaptor_closure<T>>, T has no other base
-classes of type range_adaptor_closure<U> for any other type U, and T does not model sender.
-
-
The template parameter D for sender_adaptor_closure may be an incomplete type. Before any expression of type cvD appears as
-an operand to the | operator, D shall be complete and model derived_from<range_adaptor_closure<D>>. The behavior of an expression involving an
-object of type cvD as an operand to the | operator is undefined if overload resolution selects a program-defined operator| function.
-
-
A pipeable sender adaptor object is a customization point object that accepts a sender as its first argument and returns a 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 adaptor accepts more than one argument, then let s be an expression such that decltype((s)) models sender,
-let args... be arguments such that adaptor(s,args...) is a well-formed expression as specified in the rest of this subclause
-([execution.senders.adaptor.objects]), and let BoundArgs be a pack that denotes decay_t<decltype((args))>.... The expression adaptor(args...) produces a pipeable sender adaptor closure object f that is a perfect forwarding call wrapper with the following properties:
-
-
-
Its target object is a copy of adaptor.
-
-
Its bound argument entities bound_args consist of objects of types BoundArgs... direct-non-list-initialized with std::forward<decltype((args))>(args)..., respectively.
-
-
Its call pattern is adaptor(r,bound_args...), where r is the argument used in a function call expression of f.
-
-
-
The expression adaptor(args...) is well-formed if and only if the initializations of the bound argument entities of the result, as specified above,
-are all well-formed.
execution::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.
-
-
The name execution::on denotes a customization point object. For some subexpressions sch and s, let Sch be decltype((sch)) and S be decltype((s)). If Sch does not satisfy execution::scheduler, or S does not satisfy execution::sender, execution::on is ill-formed. Otherwise, the expression execution::on(sch,s) is expression-equivalent to:
-
-
-
tag_invoke(execution::on,sch,s), if that expression is valid and its type satisfies execution::sender. If the function selected above does not return a sender which starts s on an execution agent of the associated execution context of sch when
-started, the program is ill-formed with no diagnostic required.
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it results in an operation state op_state. When execution::start is called on op_state, it:
-
-
-
Constructs a receiver r:
-
-
-
When execution::set_value(r) is called, it calls execution::connect(s,out_r), which results in op_state2. It calls execution::start(op_state2). If any of these throws an exception, it calls execution::set_error on out_r,
-passing current_exception() as the second argument.
-
-
When execution::set_error(r,e) is called, it calls execution::set_error(out_r,e).
-
-
When execution::set_done(r) is called, it calls execution::set_done(out_r).
-
-
-
Calls execution::schedule(sch), which results in s3. It then calls execution::connect(s3,r), resulting in op_state3, and then it calls execution::start(op_state3).
-
-
The lifetimes of op_state2 and op_state3, once constructed, last until op_state is destroyed.
-
-
-
-
Any receiver r created by an implementation of on shall implement the get_scheduler receiver query. The scheduler returned from the query for all such receivers should be equivalent to the sch argument passed into the on call.
execution::transfer is used to adapt a sender into a sender with a different associated set_value completion scheduler. [Note: it results in a transition between different execution contexts when executed. --end note]
-
-
The name execution::transfer denotes a customization point object. For some subexpressions sch and s, let Sch be decltype((sch)) and S be decltype((s)). If Sch does not satisfy execution::scheduler, or S does not satisfy execution::sender, execution::transfer is ill-formed. Otherwise, the expression execution::transfer(s,sch) is expression-equivalent to:
-
-
-
tag_invoke(execution::transfer,get_completion_scheduler<set_value_t>(s),s,sch), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::transfer,s,sch), if that expression is valid and its type satisfies 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 execution::schedule_from(sch,s2), where s2 is a sender which sends equivalent to those sent by s, the program is ill-formed with no diagnostic required.
-
-
Senders returned from execution::transfer shall not propagate the sender queries get_completion_scheduler<CPO> to an input sender. They shall return a scheduler equivalent to the sch argument from those queries.
execution::schedule_from is used to schedule work dependent on the completion of a sender onto a scheduler’s associated execution context. [Note: schedule_from is not meant to be used in user code; they are used in the implementation of transfer. -end note]
-
-
The name execution::schedule_from denotes a customization point object. For some subexpressions sch and s, let Sch be decltype((sch)) and S be decltype((s)). If Sch does not satisfy execution::scheduler, or S does not satisfy execution::typed_sender, execution::schedule_from is ill-formed. Otherwise, the expression execution::schedule_from(sch,s) is expression-equivalent to:
-
-
-
tag_invoke(execution::schedule_from,sch,s), if that expression is valid and its type satisfies execution::sender. If the function selected by tag_invoke does not return a sender which completes on an execution agent belonging to the associated
-execution context of sch and sends signals equivalent to those sent by s, the program is ill-formed with no diagnostic required.
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r.
-
-
Calls execution::connect(s,r), which results in an operation state op_state2.
-
-
When a receiver completion-signal Signal(r,args...) is called, it constructs a receiver r2:
-
-
-
When execution::set_value(r2) is called, it calls Signal(out_r,args...).
-
-
When execution::set_error(r2,e) is called, it calls execution::set_error(out_r,e).
-
-
When execution::done(r2) is called, it calls execution::set_done(out_r).
-
-
It then calls execution::schedule(sch), resulting in a sender s3. It then calls execution::connect(s3,r2), resulting in an operation state op_state3. It then calls execution::start(op_state3). If any of these throws an exception,
-it catches it and calls execution::set_error(out_r,current_exception()).
-
-
Returns an operation state op_state that contains op_state2. When execution::start(op_state) is called, calls execution::start(op_state2). The lifetime of op_state3 ends when op_state is destroyed.
-
-
-
-
Senders returned from execution::transfer shall not propagate the sender queries get_completion_scheduler<CPO> to an input sender. They shall return a scheduler equivalent to the sch argument from those queries.
execution::then is used to attach invocables as continuation for successful completion of the input sender.
-
-
The name execution::then denotes a customization point object. For some subexpressions s and f, let S be decltype((s)). If S does not satisfy execution::sender, execution::then is ill-formed. Otherwise, the expression execution::then(s,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::then,get_completion_scheduler<set_value_t>(s),s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::then,s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r:
-
-
-
When execution::set_value(r,args...) is called, calls invoke(f,args...) and passes the result v to execution::set_value(out_r,v). If any of these throws an exception, it catches it and calls execution::set_error(out_r,current_exception()).
-
-
When execution::set_error(r,e) is called, calls execution::set_error(out_r,e).
-
-
When execution::set_done(r) is called, calls execution::set_done(out_r).
-
-
-
Calls execution::connect(s,r), which results in an operation state op_state2.
-
-
Returns an operation state op_state that contains op_state2. When execution::start(op_state) is called, calls execution::start(op_state2).
-
-
-
If the function selected above does not return a sender which invokes f with the result of the set_value signal of s, passing the return value as the value to any connected receivers, and propagates the other completion-signals sent by s, the program is
- ill-formed with no diagnostic required.
execution::upon_error is used to attach invocables as continuation for unsuccessul completion of the input sender.
-
-
The name execution::upon_error denotes a customization point object. For some subexpressions s and f, let S be decltype((s)). If S does not satisfy execution::sender, execution::upon_error is ill-formed. Otherwise, the expression execution::upon_error(s,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::upon_error,get_completion_scheduler<set_error_t>(s),s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::upon_error,s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r:
-
-
-
When execution::set_value(r,args...) is called, calls execution::set_value(out_r,args...).
-
-
When execution::set_error(r,e) is called, calls invoke(f,e) and passes the result v to execution::set_value(out_r,v). If any of these throws an exception, it catches it and calls execution::set_error(out_r,current_exception()).
-
-
When execution::set_done(r) is called, calls execution::set_done(out_r).
-
-
-
Calls execution::connect(s,r), which results in an operation state op_state2.
-
-
Returns an operation state op_state that contains op_state2. When execution::start(op_state) is called, calls execution::start(op_state2).
-
-
-
If the function selected above does not return a sender which invokes f with the result of the set_error signal of s, passing the return value as the value to any connected receivers, and propagates the other completion-signals sent by s, the program is
- ill-formed with no diagnostic required.
execution::upon_done is used to attach invocables as continuation for the completion of the input sender using the "done" channel.
-
-
The name execution::upon_done denotes a customization point object. For some subexpressions s and f, let S be decltype((s)). If S does not satisfy execution::sender, execution::upon_done is ill-formed. Otherwise, the expression execution::upon_done(s,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::upon_done,get_completion_scheduler<set_done_t>(s),s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::upon_done,s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r:
-
-
-
When execution::set_value(r,args...) is called, calls execution::set_value(out_r,args...).
-
-
When execution::set_error(r,e) is called, calls execution::set_error(out_r,e).
-
-
When execution::set_done(r) is called, calls invoke(f) and passes the result v to execution::set_value(out_r,v). If any of these throws an exception, it catches it and calls execution::set_error(out_r,current_exception()).
-
-
-
Calls execution::connect(s,r), which results in an operation state op_state2.
-
-
Returns an operation state op_state that contains op_state2. When execution::start(op_state) is called, calls execution::start(op_state2).
-
-
-
If the function selected above does not return a sender which invokes f when the set_done signal of s is called, passing the return value as the value to any connected receivers, and propagates the other completion-signals sent by s, the program is
- ill-formed with no diagnostic required.
execution::let_value is used to insert continuations creating more work dependent on the results of their input senders into a sender chain.
-
-
The name execution::let_value denotes a customization point object. For some subexpressions s and f, let S be decltype((s)). If S does not satisfy execution::sender, execution::let_value is ill-formed. Otherwise, the expression execution::let_value(s,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::let_value,get_completion_scheduler<set_value_t>(s),s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::let_value,s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r.
-
-
-
When execution::set_value(r,args...) is called, copies args... into op_state2 as args2..., then calls invoke(f,args2...), resulting in a sender s3. It then calls execution::connect(s3,out_r), resulting in an operation state op_state3. op_state3 is saved as a part of op_state2. It then calls execution::start(op_state3). If any of these throws an exception, it catches it and calls execution::set_error(out_r,current_exception()).
-
-
When execution::set_error(r,e) is called, calls execution::set_error(out_r,e).
-
-
When execution::set_done(r,e) is called, calls execution::set_done(out_r).
-
-
-
Calls execution::connect(s,r), which results in an operation state op_state2.
-
-
Returns an operation state op_state that contains op_state2. When execution::start(op_state) is called, calls execution::start(op_state2).
-
-
-
If the function selected above does not return a sender which invokes f when set_value is called, and making its completion dependent on the completion of a sender returned by f, and propagates the other completion-signals sent by s, the program is
- ill-formed with no diagnostic required.
execution::let_error is used to insert continuations creating more work dependent on the results of their input senders into a sender chain.
-
-
The name execution::let_error denotes a customization point object. For some subexpressions s and f, let S be decltype((s)). If S does not satisfy execution::sender, execution::let_error is ill-formed. Otherwise, the expression execution::let_error(s,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::let_error,get_completion_scheduler<set_error_t>(s),s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::let_error,s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r.
-
-
-
When execution::set_value(r,args...) is called, calls execution::set_value(out_r,args...).
-
-
When execution::set_error(r,e) is called, copies e into op_state2 as e, then calls invoke(f,e), resulting in a sender s3. It then calls execution::connect(s3,out_r), resulting in an operation state op_state3. op_state3 is saved
-as a part of op_state2. It then calls execution::start(op_state3). If any of these throws an exception, it catches it and calls execution::set_error(out_r,current_exception()).
-
-
When execution::set_done(r,e) is called, calls execution::set_done(out_r).
-
-
-
Calls execution::connect(s,r), which results in an operation state op_state2.
-
-
Returns an operation state op_state that contains op_state2. When execution::start(op_state) is called, calls execution::start(op_state2).
-
-
-
If the function selected above does not return a sender which invokes f when set_error is called, and making its completion dependent on the completion of a sender returned by f, and propagates the other completion-signals sent by s, the program is
- ill-formed with no diagnostic required.
execution::let_done is used to insert continuations creating more work dependent on the results of their input senders into a sender chain.
-
-
The name execution::let_done denotes a customization point object. For some subexpressions s and f, let S be decltype((s)). If S does not satisfy execution::sender, execution::let_done is ill-formed. Otherwise, the expression execution::let_done(s,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::let_done,get_completion_scheduler<set_done_t>(s),s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::let_done,s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r.
-
-
-
When execution::set_value(r,args...) is called, calls execution::set_value(out_r,args...).
-
-
When execution::set_error(r,e) is called, calls execution::set_error(out_r,e).
-
-
When execution::set_done(r) is called, calls invoke(f), resulting in a sender s3. It then calls execution::connect(s3,out_r), resulting in an operation state op_state3. op_state3 is saved as a part of op_state2.
-It then calls execution::start(op_state3). If any of these throws an exception, it catches it and calls execution::set_error(out_r,current_exception()).
-
-
-
Calls execution::connect(s,r). which results in an operation state op_state2.
-
-
Returns an operation state op_state that contains op_state2. When execution::start(op_state) is called, calls execution::start(op_state2).
-
-
-
If the function selected above does not return a sender which invokes f when set_done is called, and making its completion dependent on the completion of a sender returned by f, and propagates the other completion-signals sent by s, the program is
- ill-formed with no diagnostic required.
execution::bulk is used to run a task repeatedly for every index in an index space.
-
-
The name execution::bulk denotes a customization point object. For some subexpressions s, shape, and f, let S be decltype((s)), Shape be decltype((shape)), and F be decltype((f)). If S does not satisfy execution::sender or Shape does not satisfy integral, execution::bulk is ill-formed. Otherwise, the expression execution::bulk(s,shape,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::bulk,get_completion_scheduler<set_value_t>(s),s,shape,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::bulk,s,shape,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r:
-
-
-
When execution::set_value(r,args...) is called, calls f(i,args...) for each i of type Shape from 0 to shape, then calls execution::set_value(out_r,args...). If any of these throws an exception, it catches it and calls execution::set_error(out_r,current_exception()).
-
-
When execution::set_error(r,e) is called, calls execution::set_error(out_r,e).
-
-
When execution::set_done(r,e) is called, calls execution::set_done(out_r,e).
-
-
-
Calls execution::connect(s,r), which results in an operation state op_state2.
-
-
Returns an operation state op_state that contains op_state2. When execution::start(op_state) is called, calls execution::start(op_state2).
-
-
-
If the function selected above does not return a sender which invokes f(i,args...) for each i of type Shape from 0 to shape when the input sender sends values args..., 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.
execution::split is used to adapt an arbitrary sender into a sender that can be connected multiple times.
-
-
The name execution::split denotes a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::typed_sender, execution::split is ill-formed. Otherwise, the expression execution::split(s) is expression-equivalent to:
-
-
-
tag_invoke(execution::split,get_completion_scheduler<set_value_t>(s),s), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::split,s), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, constructs a sender s2, which:
-
-
-
Creates an object sh_state. The lifetime of sh_state shall last for at least as long as the lifetime of the last operation state object returned from execution::connect(s,some_r) for some receiver some_r.
-
-
Constructs a receiver r:
-
-
-
When execution::set_value(r,args...) is called, saves the expressions args... as subobjects of sh_state.
-
-
When execution::set_error(r,e) is called, saves the expression e as a subobject of sh_state.
-
-
When execution::set_done(r) is called, saves this fact in sh_state.
-
-
-
Calls execution::connect(s,r), resulting in an operation state op_state2. op_state2 is saved as a subobject of sh_state.
-
-
When s2 is connected with a receiver out_r, it returns an operation state object op_state. When execution::start(op_state) is called, it calls execution::start(op_state2), if this is the first time this expression would be evaluated. When both execution::start(op_state) and Signal(r,args...) have been called, calls Signal(out_r,args2...), where args2... is a pack of lvalues referencing the subobjects of sh_state that have been saved by the
-original call to Signal(r,args...).
-
-
-
If the function selected above does not return a sender which sends references to values sent by s, propagating the other channels, the program is ill-formed with no diagnostic required.
execution::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_with_variant 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.
-
-
The name execution::when_all denotes a customization point object. For some subexpressions s..., let S be decltype((s)). If any type Si in S... does not satisfy execution::typed_sender, or the number of the arguments sender_traits<Si>::value_types passes into the Variant template parameter is not 1, execution::when_all is ill-formed. Otherwise, the expression execution::when_all(s...) is expression-equivalent to:
-
-
-
tag_invoke(execution::when_all,s...), if that expression is valid and its type satisfies execution::sender. If the function selected by tag_invoke does not return a sender which sends a concatenation of values sent by s... when they all complete with set_value, the program is ill-formed with no diagnostic required.
-
-
Otherwise, constructs a sender s. When s is connected with some receiver out_r, it:
-
-
-
For each sender si in s..., constructs a receiver ri:
-
-
-
If execution::set_value(ri,ti...) is called for every ri, execution::set_value(out_r,t0...,t1...,...,tn...) is called, where n is sizeof...(s)-1.
-
-
Otherwise, if execution::set_error(ri,e) is called for any ri, execution::set_error(out_r,e) is called.
-
-
Otherwise, if execution::set_done(ri) is called for any ri, execution::set_done(out_r) is called.
-
-
-
For each sender si in s..., calls execution::connect(si,ri), resulting in operation states op_statei.
-
-
Returns an operation state op_state that contains each operation state op_statei. When execution::start(op_state) is called, calls execution::start(op_statei) for each op_statei.
-
-
-
-
The name execution::when_all_with_variant denotes a customization point object. For some subexpressions s..., let S be decltype((s)). If any type Si in S... does not satisfy execution::typed_sender, execution::when_all_with_variant is ill-formed. Otherwise, the expression execution::when_all_with_variant(s...) is expression-equivalent to:
-
-
-
tag_invoke(execution::when_all_with_variant,s...), if that expression is valid and its type satisfies execution::sender. If the function selected by tag_invoke does not return a sender which sends the types into-variant-type<S>... when they all complete with set_value, the program is ill-formed with no diagnostic required.
Senders returned from adaptors defined in this subclause shall not expose the sender queries get_completion_scheduler<CPO>.
-
-
tag_invoke expressions used in the definitions of the sender adaptors in this subclause shall not consider member functions of their first non-tag arguments.
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 that only send a single set of values each, while also making sure
-that they complete on the specified scheduler. execution::transfer_when_all_with_variant 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]
-
-
The name execution::transfer_when_all denotes a customization point object. For some subexpressions sch and s..., let Sch be decltype(sch) and S be decltype((s)). If Sch does not satisfy scheduler, or any type Si in S... does not satisfy execution::typed_sender, or the number of the arguments sender_traits<Si>::value_types passes into the Variant template parameter is not 1, execution::transfer_when_all is ill-formed. Otherwise, the expression execution::transfer_when_all(sch,s...) is expression-equivalent to:
-
-
-
tag_invoke(execution::transfer_when_all,sch,s...), if that expression is valid and its type satisfies execution::sender. If the function selected by tag_invoke does not return a sender which sends a concatenation of values sent by s... when
-they all complete with set_value, 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 of sch, the program is ill-formed with no diagnostic
-required.
-
-
Otherwise, transfer(when_all(s...),sch).
-
-
-
The name execution::transfer_when_all_with_variant denotes a customization point object. For some subexpressions s..., let S be decltype((s)). If any type Si in S... does not satisfy execution::typed_sender, execution::transfer_when_all_with_variant is ill-formed. Otherwise, the expression execution::transfer_when_all_with_variant(s...) is expression-equivalent to:
-
-
-
tag_invoke(execution::transfer_when_all_with_variant,s...), if that expression is valid and its type satisfies execution::sender. If the function selected by tag_invoke does not return a sender which sends the types into-variant-type<S>... when they all complete with set_value, the program is ill-formed with no diagnostic required.
Senders returned from execution::transfer_when_all shall not propagate the sender queries get_completion_scheduler<CPO> to input senders. They shall return a scheduler equivalent to the sch argument from those queries.
execution::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.
-
-
The template into-variant-type is used to compute the type sent by a sender returned from execution::into_variant.
execution::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.
-
-
The name execution::done_as_optional denotes a customization point object. For some subexpression s., let S be decltype((s)). If the type S does not satisfy single-typed-sender, execution::done_as_optional(s) is ill-formed. Otherwise, the expression execution::done_as_optional(s) is expression-equivalent to:
execution::done_as_error is used to handle a done signal by mapping it into the error channel as an exception_ptr that refers to a custom exception type. The result is a sender that never completes with done, reporting cancellation by completing with an error.
-
-
The template into-variant-type is used to compute the type sent by a sender returned from execution::into_variant.
execution::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.
-
-
The name execution::ensure_started denotes a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::typed_sender, execution::ensure_started is ill-formed. Otherwise, the expression execution::ensure_started(s) is expression-equivalent to:
-
-
-
tag_invoke(execution::ensure_started,get_completion_scheduler<set_value_t>(s),s), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::ensure_started,s), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise:
-
-
-
Constructs a receiver r.
-
-
Calls execution::connect(s,r), resulting in operation state op_state, and then calls execution::start(op_state).
-
-
Constructs a sender s2. When s2 is connected with some receiver out_r, it results in an operation state op_state2. Once both execution::start(op_state2) and one of the receiver completion-signals has been called on r:
-
-
-
If execution::set_value(r,ts...) has been called, calls execution::set_value(out_r,ts...).
-
-
If execution::set_error(r,e) has been called, calls execution::set_error(out_r,e).
-
-
If execution::set_done(r) has been called, calls execution::set_done(out_r).
-
-
The lifetime of op_state lasts until all three of the following have occured:
-
-
-
the lifetime of op_state2 has ended,
-
-
the lifetime of s2 has ended, and
-
-
a receiver completion-signal has been called on r.
-
-
-
-
If the function selected above does not eagerly start the sender s and return a sender which propagates the signals sent by s once started, the program is ill-formed with no diagnostic required.
-
-
Note: The wording for execution::ensure_started 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.
execution::start_detached is used to eagerly start a sender without the caller needing to manage the lifetimes of any objects.
-
-
The name execution::start_detached denotes a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::sender, execution::start_detached is ill-formed. Otherwise, the expression execution::start_detached(s) is expression-equivalent to:
-
-
-
tag_invoke(execution::start_detached,execution::get_completion_scheduler<execution::set_value_t>(s),s), if that expression is valid and its type is void.
-
-
Otherwise, tag_invoke(execution::start_detached,s), if that expression is valid and its type is void.
-
-
Otherwise:
-
-
-
Constructs a receiver r:
-
-
-
When set_value(r,ts...) is called, it does nothing.
-
-
When set_error(r,e) is called, it calls std::terminate.
-
-
When set_done(r) is called, it does nothing.
-
-
-
Calls execution::connect(s,r), resulting in an operation state op_state, then calls execution::start(op_state). The lifetime of op_state lasts until one of the receiver completion-signals of r is called.
-
-
-
If the function selected above does not eagerly start the sender s after connecting it with a receiver which ignores the set_value and set_done signals and calls std::terminate on the set_error signal, the program is ill-formed with no diagnostic required.
this_thread::sync_wait and this_thread::sync_wait_with_variant 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.
-
-
The templates sync-wait-type and sync-wait-with-variant-type are used to determine the return types of this_thread::sync_wait and this_thread::sync_wait_with_variant.
The name this_thread::sync_wait denotes a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::typed_sender, or the number of the arguments sender_traits<S>::value_types passes into the Variant template parameter is not 1, this_thread::sync_wait is ill-formed. Otherwise, this_thread::sync_wait is expression-equivalent to:
-
-
-
tag_invoke(this_thread::sync_wait,execution::get_completion_scheduler<execution::set_value_t>(s),s), if this expression is valid and its type is sync-wait-type<S>.
-
-
Otherwise, tag_invoke(this_thread::sync_wait,s), if this expression is valid and its type is sync-wait-type<S>.
-
-
Otherwise:
-
-
-
Constructs a receiver r.
-
-
Calls execution::connect(s,r), resulting in an operation state op_state, then calls execution::start(op_state).
-
-
Blocks the current thread until a receiver completion-signal of r is called. When it is:
-
-
-
If execution::set_value(r,ts...) has been called, returns sync-wait-type<S>(make_tuple(ts...))>.
-
-
If execution::set_error(r,e...) has been called, if remove_cvref_t(decltype(e)) is exception_ptr, calls std::rethrow_exception(e). Otherwise, throws e.
-
-
If execution::set_done(r) has been called, returns sync-wait-type<S(nullopt)>.
-
-
-
-
-
The name this_thread::sync_wait_with_variant denotes a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::typed_sender, this_thread::sync_wait_with_variant is ill-formed. Otherwise, this_thread::sync_wait_with_variant is expression-equivalent to:
-
-
-
tag_invoke(this_thread::sync_wait_with_variant,execution::get_completion_scheduler<execution::set_value_t>(s),s), if this expression is valid and its type is sync-wait-with-variant-type<S>.
-
-
Otherwise, tag_invoke(this_thread::sync_wait_with_variant,s), if this expression is valid and its type is sync-wait-with-variant-type<S>.
Any receiver r created by an implementation of sync_wait and sync_wait_with_variant shall implement the get_scheduler 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.
-
-
9.7. execution::execute[execution.execute]
-
-
-
execution::execute is used to create fire-and-forget tasks on a specified scheduler.
-
-
The name execution::execute denotes a customization point object. For some subexpressions sch and f, let Sch be decltype((sch)) and F be decltype((f)). If Sch does not satisfy execution::scheduler or F does not satisfy invocable<>, execution::execute is ill-formed. Otherwise, execution::execute is expression-equivalent to:
-
-
-
tag_invoke(execution::execute,sch,f), if that expression is valid and its type is void. If the function selected by tag_invoke does not invoke the function f on an execution agent belonging to the associated execution context of sch, or if it
-does not call std::terminate if an error occurs after control is returned to the caller, the program is ill-formed with no diagnostic required.
as_awaitable is used to wrap a sender into an object that is awaitable within a particular coroutine. A sender s of type S can be made awaitable if and only if:
-
-
-
S models the concept typed_sender, and
-
-
It calls at most one overload of execution::set_value with zero or one argument, and
-
-
It calls at most one overload of exception::set_error, and
-
-
Given an lvalue p to the promise type of an awaiting coroutine, p.unhandled_done() is well-formed, noexcept, and returns a type convertible to coroutine_handle<>.
Alias template single-sender-value-type is defined as follows:
-
-
-
If sender_traits<remove_cvref_t<S>>::value_types<Tuple,Variant> would have the form Variant<Tuple<T>>, then single-sender-value-type<S> is an alias for type T.
-
-
Otherwise, if sender_traits<remove_cvref_t<S>>::value_types<Tuple,Variant> would have the form Variant<Tuple<>> or Variant<>, then single-sender-value-type<S> is an alias for type void.
-
-
Otherwise, single-sender-value-type<S is ill-formed.
-
-
-
For some expressions s and p such that decltype((s)) is S and decltype((p)) is P&, as_awaitable(s,p) returns sender-awaitable{s,p}, where sender-awaitable is an unspecified class type equivalant to the following:
-
classsender-awaitable{
- structunit{};// exposition only
- usingvalue_t=single-sender-value-type<S>;
- usingresult_t=conditional_t<is_void_v<value_t>,unit,value_t>;
- structawaitable-receiver;// exposition only
-
- variant<monostate,result_t,exception_ptr>result_{};// exposition only
- connect_result_t<S,awaitable-receiver>state_;// exposition only
-
- public:
- sender-awaitable(S&&s,P&p);
- boolawait_ready()constnoexcept{return false;}
- voidawait_suspend(coro::coroutine_handle<>)noexcept{start(state_);}
- value_tawait_resume();
-};
-
-
-
-
awaitable-receiver is a receiver type equivalent to the following:
-
structawaitable-receiver{// exposition only
- variant<monostate,result_t,exception_ptr>*result_ptr_;
- coroutine_handle<P>continuation_;
- // ... see below
-};
-
-
Let r be an rvalue expression of type awaitable-receiver, let v be an expression of type result_t, let e be an arbitrary expression of type E, let p be an lvalue reference to the coroutine promise type, let c be a customization point object, and let as... be a pack of arguments. Then:
-
-
-
If value_t is void, then execution::set_value(r) is expression-equivalent to (result_ptr_->emplace<1>(),continuation_.resume()); otherwise, execution::set_value(r,v) is expression-equivalent to (result_ptr_->emplace<1>(v),continuation_.resume()).
-
-
execution::set_error(r,e) is expression-equivalent to (result_ptr_->emplace<2>(AS_EXCEPT_PTR(e)),continuation_.resume()), where AS_EXCEPT_PTR(e) is:
-
-
-
e if decay_t<E> names the same type as exception_ptr,
-
-
Otherwise, make_exception_ptr(system_error(e)) if decay_t<E> names the same type as error_code,
-
-
Otherwise, make_exception_ptr(e).
-
-
-
execution::set_done(r) is expression-equivalent to continuation_.promise().unhandled_done().resume().
-
-
std::tag_invoke(c,r,as...) is expression-equivalent to c(p,as...) when the type of c is not one of execution::set_value_t, execution::set_error_t, or execution::set_done_t.
-
-
-
sender-awaitable::sender-awaitable(S&&s,P&p)
-
-
-
Effects: initializes state_ with connect((S&&)s,awaitable-receiver{&result_,coroutine_handle<P>::from_promise(p)}).
with_awaitable_senders, when used as the base class of a coroutine promise type, makes senders awaitable in that coroutine type.
-
In addition, it provides a default implementation of unhandled_done() such that if a sender completes by calling execution::set_done, it is treated as if an uncatchable "done" exception were thrown from the await-expression. In practice, the coroutine is never resumed, and the unhandled_done of the coroutine caller’s promise type is called.
To get a better feel for how this interface might be used by low-level operations see this example implementation
-of a cancellable async_recv() operation for a Windows Socket.
-
structoperation_base:WSAOVERALAPPED{
- usingcompletion_fn=void(operation_base*op,DWORDbytesTransferred,interrorCode)noexcept;
-
- // Assume IOCP event loop will call this when this OVERLAPPED structure is dequeued.
- completion_fn*completed;
-};
-
-template<typenameReceiver>
-structrecv_op:operation_base{
- recv_op(SOCKETs,void*data,size_tlen,Receiverr)
- :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);
- }
-
- friendvoidtag_invoke(std::tag_t<std::execution::start>,recv_op&self)noexcept{
- // Avoid even calling WSARecv() if operation already cancelled
- autost=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
- constboolstopPossible=st.stop_possible();
- if(!stopPossible){
- self.ready.store(true,std::memory_order_relaxed);
- }
-
- // Launch the operation
- DWORDbytesTransferred=0;
- DWORDflags=0;
- intresult=WSARecv(self.sock,&self.buffer,1,&bytesTransferred,&flags,
- static_cast<WSAOVERLAPPED*>(&self), NULL);
- if(result==SOCKET_ERROR){
- interrorCode=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();
-
- BOOLok=WSAGetOverlappedResult(self.sock,(WSAOVERLAPPED*)&self,&bytesTransferred,FALSE,&flags);
- if(ok){
- std::execution::set_value(std::move(self.receiver),bytesTransferred);
- }else{
- interrorCode=WSAGetLastError();
- std::execution::set_error(std::move(self.receiver),
- std::error_code(errorCode,std::system_category()));
- }
- }
- }
- }
-
- structcancel_cb{
- recv_op&op;
-
- voidoperator()()noexcept{
- CancelIoEx((HANDLE)op.sock,(OVERLAPPED*)(WSAOVERLAPPED*)&op);
- }
- };
-
- staticvoidon_complete(operation_base*op,DWORDbytesTransferred,interrorCode)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()));
- }
- }
- }
-
- Receiverreceiver;
- SOCKETsock;
- WSABUFbuffer;
- std::optional<typenamestop_callback_type_t<Receiver>::templatecallback_type<cancel_cb>>stopCallback;
- std::atomic<bool>ready{false};
-};
-
-structrecv_sender{
- SOCKETsock;
- void*data;
- size_tlen;
-
- template<typenameReceiver>
- friendrecv_op<Receiver>tag_invoke(std::tag_t<std::execution::connect>
- constrecv_sender&s,
- Receiverr){
- returnrecv_op<Receiver>{s.sock,s.data,s.len,std::move(r)};
- }
-};
-
-recv_senderasync_recv(SOCKETs,void*data,size_tlen){
- returnrecv_sender{s,data,len};
-}
-
ISO/IEC JTC1/SC22/WG21 14882: Programming Language — C++
-
Audience:
-
SG1, LEWG
-
-
-
-
-
-
-
-
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 (std::atomic, std::mutex, std::counting_semaphore, etc) and lower level building blocks (std::thread, etc), we lack a Standard vocabulary and framework for asynchrony and parallelism that C++ programmers desperately need. std::async/std::future/std::promise, 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.
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 std::invocable, and calls the std::invocable on the signal sent by the input sender. The sender returned by then sends the result of that invocation. In this case, the input sender came from schedule, so its void, meaning it won’t send us a value, so our std::invocable takes no parameters. But we return an int, which will be sent to the next recipient.
-
-
Now, we add another operation to the chain, again using § 4.21.2 execution::then. This time, we get sent a value - the int from the previous step. We add 42 to it, and then return the result.
-
-
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 std::optional<std::tuple<...>> with the value sent by the last sender, or an empty std::optional if the last sender sent a done signal, or it throws an exception if the last sender sent an error.
This example builds an asynchronous computation of an inclusive scan:
-
-
-
It scans a sequence of doubles (represented as the std::span<constdouble>input) and stores the result in another sequence of doubles (represented as std::span<double>output).
-
-
It takes a scheduler, which specifies what execution context the scan should be launched on.
-
-
It also takes a tile_count parameter that controls the number of execution agents that will be spawned.
-
-
First we need to allocate temporary storage needed for the algorithm, which we’ll do with a std::vector, partials. We need one double of temporary storage for each execution agent we create.
-
-
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 sch, which means the next item in the chain will use sch.
-
-
Senders and sender adaptors support composition via operator|, similar to C++ ranges. We’ll use operator| to attach the next piece of work, which will spawn tile_count execution agents using § 4.21.9 execution::bulk (see § 4.13 Most sender adaptors are pipeable for details).
-
-
Each agent will call a std::invocable, passing it two arguments. The first is the agent’s index (i) in the § 4.21.9 execution::bulk operation, in this case a unique integer in [0,tile_count). The second argument is what the input sender sent - the temporary storage.
-
-
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 std::inclusive_scan over our elements. We store the scan result for our last element, which is the sum of all of our elements, in our temporary storage 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 partials. 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.
-
-
§ 4.21.2 execution::then takes an input sender and an std::invocable and calls the std::invocable with the value sent by the input sender. Inside our std::invocable, we call std::inclusive_scan on partials, which the input senders will send to us.
-
-
Then we return partials, which the next phase will need.
-
-
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 partials to integrate the sums from other tiles into our elements, completing the inclusive scan.
-
-
async_inclusive_scan returns a sender that sends the output std::span<double>. A consumer of the algorithm can chain additional work that uses the scan result. At the point at which async_inclusive_scan returns, the computation may not have completed. In fact, it may not have even started.
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:
-
-
-
async_read 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 a std::span<std::byte>, and a handle to an I/O context. It will asynchronously read into the input buffer, up to the size of the std::span. It returns a sender which will send the number of bytes read once the read completes.
-
-
async_read_array 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 a dynamic_buffer object that owns the data that was sent.
-
-
dynamic_buffer is an aggregate struct that contains a std::unique_ptr<std::byte[]> and a size.
-
-
The first thing we do inside of async_read_array is create a sender that will send a new, empty dynamic_array object using § 4.20.2 execution::just. We can attach more work to the pipeline using operator| composition (see § 4.13 Most sender adaptors are pipeable for details).
-
-
We need the lifetime of this dynamic_array object to last for the entire pipeline. So, we use let_value, which takes an input sender and a std::invocable that must return a sender itself (see § 4.21.4 execution::let_* for details). let_value sends the value from the input sender to the std::invocable. Critically, the lifetime of the sent object will last until the sender returned by the std::invocable completes.
-
-
Inside of the let_valuestd::invocable, we have the rest of our logic. First, we want to initiate an async_read of the buffer size. To do that, we need to send a std::span pointing to buf.size. We can do that with § 4.20.2 execution::just.
Next, we pipe a std::invocable that will be invoked after the async_read completes using § 4.21.2 execution::then.
-
-
That std::invocable gets sent the number of bytes read.
-
-
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 std::span<std::byte> to the storage for the data from the std::invocable. This will be sent to the next recipient in the pipeline.
-
-
And that recipient will be another async_read, which will read the data.
-
-
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 dynamic_buffer object. It will get sent by the sender returned by async_read_array. We can attach more things to that sender to use the data in the buffer.
-
-
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 async_recv() operation for a Windows Socket.
-
structoperation_base:WSAOVERALAPPED{
- usingcompletion_fn=void(operation_base*op,DWORDbytesTransferred,interrorCode)noexcept;
-
- // Assume IOCP event loop will call this when this OVERLAPPED structure is dequeued.
- completion_fn*completed;
-};
-
-template<typenameReceiver>
-structrecv_op:operation_base{
- recv_op(SOCKETs,void*data,size_tlen,Receiverr)
- :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);
- }
-
- friendvoidtag_invoke(std::tag_t<std::execution::start>,recv_op&self)noexcept{
- // Avoid even calling WSARecv() if operation already cancelled
- autost=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
- constboolstopPossible=st.stop_possible();
- if(!stopPossible){
- self.ready.store(true,std::memory_order_relaxed);
- }
-
- // Launch the operation
- DWORDbytesTransferred=0;
- DWORDflags=0;
- intresult=WSARecv(self.sock,&self.buffer,1,&bytesTransferred,&flags,
- static_cast<WSAOVERLAPPED*>(&self), NULL);
- if(result==SOCKET_ERROR){
- interrorCode=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();
-
- BOOLok=WSAGetOverlappedResult(self.sock,(WSAOVERLAPPED*)&self,&bytesTransferred,FALSE,&flags);
- if(ok){
- std::execution::set_value(std::move(self.receiver),bytesTransferred);
- }else{
- interrorCode=WSAGetLastError();
- std::execution::set_error(std::move(self.receiver),
- std::error_code(errorCode,std::system_category()));
- }
- }
- }
- }
-
- structcancel_cb{
- recv_op&op;
-
- voidoperator()()noexcept{
- CancelIoEx((HANDLE)op.sock,(OVERLAPPED*)(WSAOVERLAPPED*)&op);
- }
- };
-
- staticvoidon_complete(operation_base*op,DWORDbytesTransferred,interrorCode)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()));
- }
- }
- }
-
- Receiverreceiver;
- SOCKETsock;
- WSABUFbuffer;
- std::optional<typenamestop_callback_type_t<Receiver>::templatecallback_type<cancel_cb>>stopCallback;
- std::atomic<bool>ready{false};
-};
-
-structrecv_sender{
- SOCKETsock;
- void*data;
- size_tlen;
-
- template<typenameReceiver>
- friendrecv_op<Receiver>tag_invoke(std::tag_t<std::execution::connect>
- constrecv_sender&s,
- Receiverr){
- returnrecv_op<Receiver>{s.sock,s.data,s.len,std::move(r)};
- }
-};
-
-recv_senderasync_recv(SOCKETs,void*data,size_tlen){
- returnrecv_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.
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.
In this code, NN::async_read_some and NN::async_write_some are asynchronous socket-based networking APIs that return senders. EX::repeat_effect_until, EX::let_value, and EX::then 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 repeat_effect_until 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.
This code builds a then 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 (set_error and set_done), as well as all receiver queries,
-are passed through unchanged.
-
In detail, it does the following:
-
-
-
Defines a receiver in terms of execution::receiver_adaptor that aggregates
-another receiver and an invocable that:
-
-
-
Defines a constrained tag_invoke overload for transforming the value
-channel.
-
-
Defines another constrained overload of tag_invoke that passes all other
-customizations through unchanged.
-
-
The tag_invoke overloads are actually implemented by execution::receiver_adaptor; they dispatch either to named members, as
-shown above with _then_receiver::set_value, or to the adapted receiver.
-
-
Defines a sender that aggregates another sender and the invocable, which defines a tag_invoke customization for std::execution::connect that wraps the incoming receiver in the receiver from (1) and passes it and the incoming sender to std::execution::connect, returning the result.
-
-
1.5.2. retry
-
template<classFrom,classTo>
-using_decays_to=same_as<decay_t<From>,To>;
-
-// _conv needed so we can emplace construct non-movable types into
-// a std::optional.
-template<invocableF>
- requiresis_nothrow_move_constructible_v<F>
-struct_conv{
- Ff_;
- explicit_conv(Ff)noexcept:f_((F&&)f){}
- operatorinvoke_result_t<F>()&&{
- return((F&&)f_)();
- }
-};
-
-// pass through all customizations except set_error, which retries the operation.
-template<classO,classR>
-struct_retry_receiver
- :std::execution::receiver_adaptor<_retry_receiver<O,R>>{
- O*o_;
-
- R&&base()&&noexcept{return(R&&)o_->r_;}
- constR&base()const&noexcept{returno_->r_;}
-
- explicit_retry_receiver(O*o):o_(o){}
-
- voidset_error(auto&&)&&noexcept{
- o_->_retry();// This causes the op to be retried
- }
-};
-
-template<senderS>
-struct_retry_sender:std::execution::sender_base{
- Ss_;
- explicit_retry_sender(Ss):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<receiverR>
- struct_op{
- Ss_;
- Rr_;
- std::optional<
- std::execution::connect_result_t<S&,_retry_receiver<_op,R>>>o_;
-
- _op(Ss,Rr):s_((S&&)s),r_((R&&)r),o_{_connect()}{}
- _op(_op&&)=delete;
-
- auto_connect()noexcept{
- return_conv{[this]{
- returnstd::execution::connect(s_,_retry_receiver<_op,R>{this});
- }};
- }
- void_retry()noexcepttry{
- o_.emplace(_connect());// potentially throwing
- std::execution::start(*o_);
- }catch(...){
- std::execution::set_error((R&&)r_,std::current_exception());
- }
- friendvoidtag_invoke(std::execution::start_t,_op&o)noexcept{
- std::execution::start(*o.o_);
- }
- };
-
- template<receiverR>
- requiressender_to<S&,R>
- friend_op<R>tag_invoke(std::execution::connect_t,_retry_sender&&self,Rr){
- return{(S&&)self.s_,(R&&)r};
- }
-};
-
-namespacestd::execution{
- template<typed_senderS>
- structsender_traits<_retry_sender<S>>:sender_traits<S>{};
-}
-
-std::execution::senderautoretry(std::execution::senderautos){
- return_retry_sender{std::move(s)};
-}
-
-
The retry 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 optional, which allows us
-to reinitialize it multiple times.
-
This example does the following:
-
-
-
Defines a _conv utility that takes advantage of C++17’s guaranteed copy elision to
-emplace a non-movable type in a std::optional.
-
-
Defines a _retry_receiver 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 set_error, which causes a _retry() function to be called instead.
-
-
Defines an operation state that aggregates the input sender and receiver, and declares
-storage for the nested operation state in a std::optional. Constructing the operation
-state constructs a _retry_receiver with a pointer to the (under construction) operation
-state and uses it to connect to the aggregated sender.
-
-
Starting the operation state dispatches to start on the inner operation state.
-
-
The _retry() 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.
-
-
After reinitializing the inner operation state, _retry() calls start on it, causing
-the failed operation to be rescheduled.
-
-
Defines a _retry_sender that implements the connect customization point to return
-an operation state constructed from the passed-in sender and receiver.
-
-
1.6. Examples: Schedulers
-
In this section we look at some schedulers of varying complexity.
The inline scheduler is a trivial scheduler that completes immediately and synchronously on
-the thread that calls std::execution::start on the operation state produced by its sender.
-In other words, start(connect(schedule(inline-scheduler),receiver)) is
-just a fancy way of saying set_value(receiver), with the exception of the fact that start wants to be passed an lvalue.
-
Although not a particularly useful scheduler, it serves to illustrate the basics of
-implementing one. The inline_scheduler:
-
-
-
Customizes execution::schedule to return an instance of the sender type _sender.
-
-
The _sender type models the typed_sender concept and provides the metadata needed
-to describe it as a sender of no values (see value_types) that can send an exception_ptr as an error (see error_types), and that never calls set_done (see sends_done).
-
-
The _sender type customizes execution::connect to accept a receiver of no values.
-It returns an instance of type _op that holds the receiver by value.
-
-
The operation state customizes std::execution::start to call std::execution::set_value on the receiver, passing any exceptions to std::execution::set_error as an 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 std::execution::run_loop.
The single_thread_context 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 execution::run_loop 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 this_thread::sync_wait, 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 executor concept has been removed and all of its proposed functionality is now based on schedulers and senders, as per SG1 direction.
-
-
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.
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 run_loop suggested here (where it is called manual_event_loop). 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 let_value are really nice additions.
-
-
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 f2 in the continuation will always be ready here f1.then([](future<T>f2){...}) is enough of a reason to separate the channels. All the other obvious reasons apply as well of course.
-
-
For futures we have a thing called hpx::dataflow which is an optimized version of when_all(...).then(...) which avoids intermediate allocations. With the sender/receiver when_all(...)|then(...) we get that "for free".
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 execution::start_detached, but otherwise implements the concepts, customization points, traits, queries, coroutine integration, sender factories, pipe support, execution::receiver_adaptor, and execution::run_loop.
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:
Fix specification of the on algorithm to clarify lifetimes of intermediate operation states and properly scope the get_scheduler query.
-
-
Fix a memory safety bug in the implementation of connect-awaitable.
-
-
Fix recursive definition of the scheduler concept.
-
-
Enhancements:
-
-
-
Add run_loop execution context.
-
-
Add receiver_adaptor utility to simplify writing receivers.
-
-
Require a scheduler’s sender to model sender_of and provide a completion scheduler.
-
-
Specify the cancellation scope of the when_all algorithm.
-
-
Make as_awaitable a customization point.
-
-
Change connect's handling of awaitables to consider those types that are awaitable owing to customization of as_awaitable.
-
-
Add value_types_of_t and error_types_of_t alias templates; rename stop_token_type_t to 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 execution::connect customization point and the sender_traits<> template to recognize awaitables as typed_senders.
-
-
Add utilities as_awaitable() and with_awaitable_senders<> so a coroutine type can trivially make senders awaitable with a coroutine.
-
-
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 bulk algorithm.
-
-
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 execution::just_error and execution::just_done.
-
-
Add sender adaptors execution::done_as_optional and 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 unschedule adaptor.
-
-
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]: std::execution; however, for brevity, the std:: part of this name is omitted. When you see execution::foo, treat that as std::execution::foo.
-
-
Universal references and explicit calls to std::move/std::forward are omitted in code samples and signatures for simplicity; assume universal references and perfect forwarding unless stated otherwise.
-
-
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 std::invocable 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 std::invocable 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, schedule, 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::schedulerautosch=thread_pool.scheduler();
-execution::senderautosnd=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 execution::schedule API.
-
Future papers will propose additional scheduler concepts that extend scheduler to add other capabilities. For example:
-
-
-
A time_scheduler concept that extends scheduler to support time-based scheduling.
-Such a concept might provide access to schedule_after(sched,duration), schedule_at(sched,time_point) and now(sched) APIs.
-
-
Concepts that extend scheduler to support opening, reading and writing files asynchronously.
-
-
Concepts that extend scheduler to support connecting, sending data and receiving data over the network asynchronously.
-
-
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.
execution::schedulerautosch=thread_pool.scheduler();
-execution::senderautosnd=execution::schedule(sch);
-execution::senderautocont=execution::then(snd,[]{
- std::fstreamfile{"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 execution::connect) senders and return a sender.
-
-
sender consumers, algorithms that take (and potentially execution::connect) senders and do not return a sender.
-
-
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 std::thread (which they aren’t).
-
-
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 on 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
-
get_completion_scheduler is a query that retrieves the completion scheduler for a specific completion signal from a sender.
-Calling get_completion_scheduler 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::schedulerautocpu_sched=new_thread_scheduler{};
-execution::schedulerautogpu_sched=cuda::scheduler();
-
-execution::senderautosnd0=execution::schedule(cpu_sched);
-execution::schedulerautocompletion_sch0=
- execution::get_completion_scheduler<execution::set_value_t>(snd0);
-// completion_sch0 is equivalent to cpu_sched
-
-execution::senderautosnd1=execution::then(snd0,[]{
- std::cout<<"I am running on cpu_sched!\n";
-});
-execution::schedulerautocompletion_sch1=
- execution::get_completion_scheduler<execution::set_value_t>(snd1);
-// completion_sch1 is equivalent to cpu_sched
-
-execution::senderautosnd2=execution::transfer(snd1,gpu_sched);
-execution::senderautosnd3=execution::then(snd2,[]{
- std::cout<<"I am running on gpu_sched!\n";
-});
-execution::schedulerautocompletion_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 execution::schedule, 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 execution::transfer sender adaptor performs a transition from one execution context to another:
-
execution::schedulerautosch1=...;
-execution::schedulerautosch2=...;
-
-execution::senderautosnd1=execution::schedule(sch1);
-execution::senderautothen1=execution::then(snd1,[]{
- std::cout<<"I am running on sch1!\n";
-});
-
-execution::senderautosnd2=execution::transfer(then1,sch2);
-execution::senderautothen2=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 std::unique_ptr that it will be transferring ownership of to the
-operation-state returned by a call to execution::connect so that the operation has access to
-this resource. In such a sender, calling execution::connect 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 execution::connect only has overloads for an rvalue-qualified sender. Callers must pass the sender
-as an rvalue to the call to execution::connect, 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 execution::connect to accept an lvalue reference to the
-sender. Callers can indicate that they want the sender to remain valid after the call to execution::connect by passing an lvalue reference to the sender to call these overloads. Multi-shot senders should also define
-overloads of execution::connect 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 execution::connect.
-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 execution::connect. 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 execution::connect on the input sender, such as in this_thread::sync_wait or execution::start_detached.
-
Some multi-use sender algorithms may require that an input sender be copy-constructible but will only call execution::connect 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 execution::connect 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 split sender adaptor facilitates connecting to a sender multiple times, regardless of whether it is single-shot or multi-shot:
-
autosome_algorithm(execution::senderauto&&input){
- execution::senderautomulti_shot=split(input);
- // "multi_shot" is guaranteed to be multi-shot,
- // regardless of whether "input" was multi-shot or not
-
- returnwhen_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.
-
when_all 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. when_all returns a sender that also does not have an associated scheduler.
-
transfer_when_all 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 when_all. You can think of it as a composition of transfer(when_all(inputs...),scheduler), 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 execution::connect() before calling execution::start(), 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 when_all(ops...) algorithm should cancel other operations as soon as one operation fails
-
-
a first_successful(ops...) algorithm should cancel the other operations as soon as one operation completes successfuly
-
-
a generic timeout(src,duration) algorithm needs to be able to cancel the src operation after the timeout duration has elapsed.
-
-
a stop_when(src,trigger) algorithm should cancel src if trigger completes first and cancel trigger if src completes first
-
-
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:
In this example, if we take the operation returned by query_server_b(query), this operation will
-receive a stop-request when any of the following happens:
-
-
-
first_successful algorithm will send a stop-request if query_server_a(query) completes successfully
-
-
when_all algorithm will send a stop-request if the load_file("some_file.jpg") operation completes with an error or done result.
-
-
timeout algorithm will send a stop-request if the operation does not complete within 5 seconds.
-
-
stop_when algorithm will send a stop-request if the user clicks on the "Cancel" button in the user-interface.
-
-
The parent operation consuming the composed_cancellation_example() sends a stop-request
-
-
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 std::stop_token-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 std::stoppable_token and std::stoppable_token_for concepts that generalise the interface of std::stop_token type to allow other types with different implementation strategies.
-
-
Add std::unstoppable_token concept for detecting whether a stoppable_token can never receive a stop-request.
-
-
Add std::in_place_stop_token, std::in_place_stop_source and std::in_place_stop_callback<CB> types that provide a more efficient implementation of a stop-token for use in structured concurrency situations.
-
-
Add std::never_stop_token for use in places where you never want to issue a stop-request
-
-
Add std::execution::get_stop_token() CPO for querying the stop-token to use for an operation from its receiver.
-
-
Add std::execution::stop_token_of_t<T> for querying the type of a stop-token returned from 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 execution::get_stop_token() CPO.
-This CPO takes a single parameter, which is the receiver passed to execution::connect, and returns a std::stoppable_token that the operation should use to check for stop-requests for that operation.
-
As the caller of execution::connect typically has control over the receiver type it passes, it is able to customise
-the execution::get_stop_token() 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 execution::get_stop_token() CPO then invoking the CPO on that receiver will
-invoke the default implementation which returns std::never_stop_token. This is a special stoppable_token type that
-is statically known to always return false from the stop_possible() 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 execution::get_stop_token(), 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 execution::start() 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 execution::start() 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 execution::start() implementation.
-
An implementation of execution::start() 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 execution::start 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 execution::start() call (which is still executing).
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 ensure_started 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 execution::connect?" or similarly, "What happens if a call
-to execution::connect is made, but the returned operation state is destroyed
-before execution::start 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 when_all(eager_op_1(),eager_op_2()). Imagine eager_op_1() starts an
-asynchronous operation successfully, but then eager_op_2() throws. For
-lazy senders, that failure happens in the context of the when_all 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 before when_all has been called.
-
It then becomes the responsibility, not of the algorithm, but of the end
-user to handle the exception and ensure that eager_op_1() is joined before
-allowing the exception to propagate. If they fail to do that, they incur
-undefined behavior.
-
-
Detach from the computation - let the operation continue in the background -
-like an implicit call to std::thread::detach(). 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::shared_ptr, to ensure resources are kept alive until the operations
-complete, and can make clean shutdown nigh impossible.
-
-
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 std::terminate if an eager sender is destructed
-without joining it. This is the approach that std::thread destructor
-takes.
-
-
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 std::jthread uses for its
-destructor. It reduces the risk of deadlock but does not eliminate it.
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 execution::start 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 sequence that puts async operations in sequence by starting an operation only
-after the preceding one has completed. In an expression like sequence(a(),then(src,[]{b();}),c()), one my reasonably assume that a(), b() and c() 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.
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 execution::get_stop_token is an std::unstoppable_token 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, get_forward_progress_guarantee, which returns one of the enumerators of a new enum type, forward_progress_guarantee. Each enumerator of forward_progress_guarantee 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 operator|.
-This mechanism is similar to the operator| 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.
-
a|b will pass the sender a as the first argument to the pipeable sender adaptor b. Pipeable sender adaptors support partial application of the parameters after the first. For example, all of the following are equivalent:
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:
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.
-
-
-
execution::when_all and execution::when_all_with_variant: 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::on: 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 to 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 R that is a range of senders, consider the following in a coroutine that returns an async generator type:
This transforms each element of the asynchronous sequence R with the function fn on demand, as the data arrives. The result is a new asynchronous sequence of the transformed values.
-
Now imagine that R is the simple expression views::iota(0)|views::transform(execution::just). 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 [fn(0), fn(1), fn(2), ...].
-
Far more interesting would be if R 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:
- returnread_socket_async(socket,span{buff})
- // For success (partial and full), specify the next action:
- |execution::let_value([](error_codeerr,size_tbytes_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(autosnd:read_socket_async(socket,span{buff})){
- try{
- if(optional<size_t>bytes_read=
- co_awaitexecution::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 typed_sender 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 await_transform trickery from a promise type to make them awaitable.)
-
For an example, imagine a coroutine type called task<T> that knows nothing about senders. It doesn’t implement any of the sender customization points. Despite that fact, and despite the fact that the this_thread::sync_wait algorithm is constrained with the typed_sender concept, the following would compile and do what the user wants:
-
task<int>doSomeAsyncWork();
-
-intmain(){
- // OK, awaitable types satisfy the requirements for typed senders:
- autoo=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 execution::with_awaitable_senders 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 retry algorithm:
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 with_awaitable_senders 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 into_variant sender algorithm -- before co_await-ing that sender.
-
4.18. Cancellation of a sender can unwind a stack of coroutines
-
When looking at the sender-based retry 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 execution::set_done?
-
When your task type’s promise inherits from with_awaitable_senders, what happens is this: the coroutine behaves as if an uncatchable exception had been thrown from the co_await 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 with_awaitable_senders, or more generally implement a member function called unhandled_done, the exception unwinds the chain of coroutines as if an exception were thrown except that it bypasses catch(...) 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 execution::let_done, execution::upon_done, execution::done_as_optional, or execution::done_as_error sender adaptors. For instance, we can use execution::done_as_optional to "catch" the done signal and map it into an empty optional as shown below:
-
if(autoopt=co_awaitexecution::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 connect-ing an awaitable and a receiver, the adaptation layer awaits the awaitable within a coroutine that implements unhandled_done in its promise type. The effect of this is that an "uncatchable" done exception propagates seamlessly out of awaitables, causing execution::set_done to be called on the receiver.
-
Obviously, unhandled_done is a library extension of the coroutine promise interface. Many promise types will not implement unhandled_done. When an uncatchable done exception tries to propagate through such a coroutine, it is treated as an unhandled exception and terminate 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 unhandled_done. 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 execution::par, 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.
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 policy while using scheduler to create any work to be run. The expression may be ill-formed if scheduler 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 executing_on 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 void 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.
execution::schedulerautosch1=get_system_thread_pool().scheduler();
-
-execution::senderautosnd1=execution::schedule(sch1);
-// snd1 describes the creation of a new task on the system thread pool
-
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 set_value.
Returns a sender whose value completion scheduler is the provided scheduler, which sends the provided values in the same manner as just.
-
execution::senderautovals=execution::transfer_just(
- get_system_thread_pool().scheduler(),
- 1,2,3
-);
-execution::senderautosnd=execution::then(vals,[](auto...args){
- std::print(args...);
-});
-// when snd is executed, it will print "123"
-
-
This adaptor is included as it greatly simplifies lifting values into senders.
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 set_error. 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 set_error.
-
4.20.5. execution::just_done
-
execution::senderautojust_done();
-
-
Returns a sender with no completion schedulers, which completes immediately by calling the receiver’s set_done.
-
4.21. User-facing sender adaptors
-
A sender adaptor is an algorithm that takes one or more senders, which it may execution::connect, as parameters, and returns a sender, whose completion is related to the sender arguments it has received.
execution::schedulerautocpu_sched=get_system_thread_pool().scheduler();
-execution::schedulerautogpu_sched=cuda::scheduler();
-
-execution::senderautocpu_task=execution::schedule(cpu_sched);
-// cpu_task describes the creation of a new task on the system thread pool
-
-execution::senderautogpu_task=execution::transfer(cpu_task,gpu_sched);
-// gpu_task describes the transition of the task graph described by cpu_task to the gpu
-
then 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.
-
then is guaranteed to not begin executing function until the returned sender is started.
-
execution::senderautoinput=get_input();
-execution::senderautosnd=execution::then(input,[](auto...args){
- std::print(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.
upon_error and upon_done are similar to then, but where then works with values sent by the input sender, upon_error works with errors, and upon_done is invoked when the "done" signal is sent.
let_value is very similar to then: 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 then sends exactly what that function ends up returning - let_value requires that the function return a sender, and the sender returned by let_value 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.
-
let_value is guaranteed to not begin executing function until the returned sender is started.
-
let_error and let_done are similar to let_value, but where let_value works with values sent by the input sender, let_error works with errors, and let_done is invoked when the "done" signal is sent.
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.
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.
Returns a sender that maps the value channel from a T to an optional<decay_t<T>>, and maps the done channel to a value of an empty optional<decay_t<T>>.
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 function will begin executing until the returned sender is started. Each invocation of function 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 bulk execute with the same guarantee. This allows, for instance, a scheduler to execute all invocations of the function in parallel.
-
The bulk operation is intended to be used at the point where the number of agents to be created is known and provided to bulk via its shape 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, bulk can be combined with additional operations such as let_value to deliver dynamic shape information to the bulk 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.
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.
when_all 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 set_value 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 when_all. It completes inline on the execution context on which the last input sender completes, unless stop is requested before when_all is started, in which case it completes inline within the call to start.
-
when_all_with_variant does the same, but it adapts all the input senders using into_variant, and so it does not constrain the input arguments as when_all does.
execution::schedulerautosched=thread_pool.scheduler();
-
-execution::senderautosends_1=...;
-execution::senderautosends_abc=...;
-
-execution::senderautoboth=execution::when_all(sched,
- sends_1,
- sends_abc
-);
-
-execution::senderautofinal=execution::then(both,[](auto...args){
- std::cout<<std::format("the two args: {}, {}",args...);
-});
-// when final executes, it will print "the two args: 1, abc"
-
Once ensure_started returns, it is known that the provided sender has been connected and start 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 execution::connect() is called, or if execution::connect() is called but the
-returned operation-state is destroyed before execution::start() 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 std::shared_ptr 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 execution::connect, as parameters, and does not return a sender.
this_thread::sync_wait is a sender consumer that submits the work described by the provided sender for execution, similarly to ensure_started, except that it blocks the current std::thread or thread of main 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, sync_wait 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, sync_wait throws that error as an exception, or rethrows the original exception if the error is of type std::exception_ptr.
-
If the provided sender sends the "done" signal instead of values, sync_wait returns an empty optional.
-
For an explanation of the requires clause, see § 5.8 Most senders are typed. That clause also explains another sender consumer, built on top of sync_wait: sync_wait_with_variant.
-
Note: This function is specified inside std::this_thread, and not inside execution. This is because sync_wait 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 std::this_thread, 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 std::this_fiber::sync_wait would be provided. We also expect that runtimes with execution agents that use different synchronization mechanisms than std::thread's will provide their own flavors of sync_wait 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.
A receiver is a callback that supports more than one channel. In fact, it supports three of them:
-
-
-
set_value, which is the moral equivalent of an operator() or a function call, which signals successful completion of the operation its execution depends on;
-
-
set_error, 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; and
-
-
set_done, which signals that the operation completed without succeeding (set_value) and without failing (set_error). 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.
-
-
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 set_value failed with an exception, either set_error or set_done 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 std::promise, which provides the first two signals as set_value and set_error, 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.
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: start, 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 execution::ensure_started and this_thread::sync_wait, 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.
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 connect will be fulfilled.
-
execution::senderautosnd=someinputsender;
-execution::receiverautorcv=somereceiver;
-execution::operation_stateautostate=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 then, 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 execution::then(sender,invocable) to be equivalent to:
-
-
-
sender.then(invocable), if that expression is well formed; otherwise
-
-
then(sender,invocable), performed in a context where this call always performs ADL, if that expression is well formed; otherwise
-
-
a default implementation of then, which returns a sender adaptor, and then define the exact semantics of said adaptor.
-
-
However, this definition is problematic. Imagine another sender adaptor, bulk, 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 bulk to have specialized behavior, which invokes a kernel of more than one thread (with its size defined by the call to bulk); therefore, we would like to customize bulk for CUDA senders to achieve this. However, there’s no reason for CUDA kernels to
-necessarily customize the then sender adaptor, as the generic implementation is perfectly sufficient. This creates a problem, though; consider the following snippet:
-
execution::schedulerautocuda_sch=cuda_scheduler{};
-
-execution::senderautoinitial=execution::schedule(cuda_sch);
-// the type of initial is a type defined by the cuda_scheduler
-// let’s call it cuda::schedule_sender<>
-
-execution::senderautonext=execution::then(cuda_sch,[]{return1;});
-// 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::senderautokernel_sender=execution::bulk(next,shape,[](inti){...});
-
-
How can we specialize the bulk sender adaptor for our wrapped schedule_sender? 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):
However, if the input sender is not just a then_sender_adaptor like in the example above, but another sender that overrides bulk 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 execution::<sender-algorithm>(sender,args...), for any given sender algorithm that accepts a sender as its first argument, should be
-equivalent to:
-
-
-
tag_invoke(<sender-algorithm>,get_completion_scheduler<Signal>(sender),sender,args...), if that expression is well-formed; otherwise
-
-
tag_invoke(<sender-algorithm>,sender,args...), if that expression is well-formed; otherwise
-
-
a default implementation, if there exists a default implementation of the given sender algorithm.
-
-
where Signal is one of set_value, set_error, or set_done; for most sender algorithms, the completion scheduler for set_value would be used, but for some (like upon_error or let_done), one of the others would be used.
-
For sender algorithms which accept concepts other than sender as their first argument, we propose that the customization scheme remains as it has been in [P0443R14] so far, except it should also use tag_invoke.
-
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 execution::start 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 execution::transfer 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 transfer 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 transfer.
-
To allow for such customization from both ends, we propose the inclusion of a secondary transitioning sender adaptor, called schedule_from. This adaptor is a form of schedule, 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 transfer, to ensure that both schedulers have a say in how the transitions are made. Any scheduler that specializes transfer(snd,sch) shall ensure that the
-return value of their customization is equivalent to schedule_from(sch,snd2), where snd2 is a successor of snd that sends values equivalent to those sent by snd.
-
The default implementation of transfer(snd,sched) is schedule_from(sched,snd).
-
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 execution::then is
-hard.
-
The mechanism for this advertisement is the same as in [P0443R14]; the way to query the types is through sender_traits::value_types<tuple_like,variant_like>.
-
sender_traits::value_types 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 when_all). 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 std::optional to handle the set_done signal. However, this assumes that those values can be represented as a
-tuple, like here:
-
execution::senderautosends_1=...;
-execution::senderautosends_2=...;
-execution::senderautosends_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 value_types of a sender which sends Types... to be as follows:
If senders could only ever send one specific set of values, this would probably need to be the required form of value_types 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) set_value 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 value_types, one that represents a variant-like type. If we permit such senders, we would almost certainly need to require that the canonical form of value_types 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 Types1..., Types2..., ..., TypesN... to be as follows:
This, however, introduces a couple of complications:
-
-
-
A just(1) sender would also need to follow this structure, so the correct type for storing the value sent by it would be std::variant<std::tuple<int>> 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 where value_types 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.
-
-
As a consequence of (1): because sync_wait needs to store the above type, it can no longer return just a std::tuple<int> for just(1); it has to return std::variant<std::tuple<int>>. 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 of sync_wait must be the same across all sender types.
-
-
One possible solution to (2) above is to place a requirement on sync_wait that it can only accept senders which send only a single set of values, therefore removing the need for std::variant to appear in its API; because of this, we propose to expose both sync_wait, which is a simple, user-friendly version of the sender consumer, but requires that value_types have only one possible variant, and sync_wait_with_variant, which accepts any sender, but returns an optional whose value type is the variant of all the
-possible tuples sent by the input sender:
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: sync_wait. We imagine that if, in the future, C++ was to
-gain fibers support, we would want to also have std::this_fiber::sync_wait, in addition to std::this_thread::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.
-
-
This paper proposes to instead use the mechanism described in [P1895R0]: tag_invoke; the wording for tag_invoke has been incorporated into the proposed specification in this paper.
-
In short, instead of using globally reserved names, tag_invoke uses the type of the customization point object itself as the mechanism to find customizations. It globally reserves only a single name - tag_invoke - which itself is used the same way that
-ranges-style customization points are used. All other customization points are defined in terms of tag_invoke. For example, the customization for std::this_thread::sync_wait(s) will call tag_invoke(std::this_thread::sync_wait,s), instead of attempting
-to invoke s.sync_wait(), and then sync_wait(s) if the member call is not valid.
-
Using tag_invoke 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<typenameTag,typename...Args>
-friendautotag_invoke(Tag&&tag,wrapper&self,Args&&...args){
- returnstd::forward<Tag>(tag)(self.subobject,std::forward<Args>(args)...);
-}
-
-// but override one of them with a specific value
-friendautotag_invoke(specific_customization_point_t,wrapper&self){
- returnself.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 tag_invoke, dispatching to manually constructed vtables containing pointers to specialized implementations for the wrapped objects. For an example of such a polymorphic wrapper, see unifex::any_unique (example).
-
-
6. Specification
-
Much of this wording follows the wording of [P0443R14].
Insert this section as a new subclause, between Searchers [func.search] and Class template hash[unord.hash].
-
-
-
-
-
The name std::tag_invoke denotes a customization point object. For some subexpressions tag and args..., tag_invoke(tag,args...) is expression-equivalent to an unqualified call to tag_invoke(decay-copy(tag),args...) with overload
-resolution performed in a context that includes the declaration:
-
voidtag_invoke();
-
-
and that does not include the the std::tag_invoke name.
-
-
-
-
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 <stop_token>. 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.
Insert this section as a new subclause between Header <stop_token> synopsis [thread.stoptoken.syn] and Class stop_token[stoptoken].
-
-
-
-
-
The stoppable_token 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, T::callback_type<CB>, 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. The stoppable_token_for concept checks for a stop token type compatible with a given
-callback type. The unstoppable_token concept checks for a stop token type that does not allow stopping.
Let t and u be distinct object of type T. The type T models stoppable_token only if:
-
-
-
All copies of a stoppable_token reference the same logical shared stop state and shall report values consistent with each other.
-
-
If t.stop_possible() evaluates to false then, if u, references the same logical shared stop state, u.stop_possible() shall also subsequently evaluate to false and u.stop_requested() shall also subsequently evaluate to false.
-
-
If t.stop_requested() evaluates to true then, if u, references the same logical shared stop state, u.stop_requested() shall also subsequently evaluate to true and u.stop_possible() shall also subsequently evaluate to true.
-
-
Given a callback-type, CB, and a callback-initializer argument, init, of type Initializer then constructing an instance, cb, of type T::callback_type<CB>, passing t as the first argument and init as the second argument to the constructor, shall,
-if t.stop_possible() is true, construct an instance, callback, of type CB, direct-initialized with init, and register callback with t’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.
-
-
-
If t.stop_requested() is true at the time callback is registered then callback may be invoked immediately inline inside the call to cb’s constructor.
-
-
If callback is invoked then, if u references the same shared stop state as t, an evaluation of u.stop_requested() will be true if the beginning of the invocation of callback strongly-happens-before the evaluation of u.stop_requested().
-
-
If t.stop_possible() evaluates to false then the construction of cb is not required to construct and initialize callback.
-
-
-
Construction of a T::callback_type<CB> instance shall only throw exceptions thrown by the initialization of the CB instance from the value of type Initializer.
-
-
Destruction of the T::callback_type<CB> object, cb, removes callback from the shared stop state such that callback will not be invoked after the destructor returns.
-
-
-
If callback is currently being invoked on another thread then the destructor of cb will block until the invocation of callback returns such that the return from the invocation of callback strongly-happens-before the destruction of callback.
-
-
Destruction of a callback cb shall not block on the completion of the invocation of some other callback registered with the same shared stop state.
-
-
-
-
-
-
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.
-
-
-
Table 1: Execution control library summary [tab:execution.summary]
None of a scheduler’s copy constructor, destructor, equality comparison, or swap member functions shall exit via an exception.
-
-
None of these member functions, nor a scheduler type’s schedule function, shall introduce data races as a result of concurrent invocations of those functions from different threads.
-
-
For any two (possibly const) values s1 and s2 of some scheduler type S, s1==s2 shall return true only if both s1 and s2 are handles to the same associated execution context.
-
-
For a given scheduler expression s, the expression execution::get_completion_scheduler<set_value_t>(execution::schedule(s)) shall compare equal to s.
-
-
A scheduler type’s destructor shall not block pending completion of any receivers connected to the sender objects returned from schedule. [Note: The ability to wait for completion of submitted function objects may be provided by the associated execution
-context of the scheduler. —end note]
execution::get_forward_progress_guarantee is used to ask a scheduler about the forward progress guarantees of execution agents created by that scheduler.
-
-
The name execution::get_forward_progress_guarantee denotes a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::scheduler, execution::get_forward_progress_guarantee is ill-formed.
-Otherwise, execution::get_forward_progress_guarantee(s) is expression equivalent to:
-
-
-
tag_invoke(execution::get_forward_progress_guarantee,as_const(s)), if this expression is well formed and its type is execution::forward_progress_guarantee, and is noexcept.
If execution::get_forward_progress_guarantee(s) for some scheduler s returns execution::forward_progress_guarantee::concurrent, all execution agents created by that scheduler shall provide the concurrent forward progress guarantee. If it returns execution::forward_progress_guarantee::parallel, all execution agents created by that scheduler shall provide at least the parallel forward progress guarantee.
this_thread::execute_may_block_caller is used to ask a scheduler s whether a call execution::execute(s,f) with any invocable f may block the thread where such a call occurs.
-
-
The name this_thread::execute_may_block_caller denotes a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::scheduler, this_thread::execute_may_block_caller is ill-formed. Otherwise, this_thread::execute_may_block_caller(s) is expression equivalent to:
-
-
-
tag_invoke(this_thread::execute_may_block_caller,as_const(s)), if this expression is well formed and its type is bool, and is noexcept.
-
-
Otherwise, true.
-
-
-
If this_thread::execute_may_block_caller(s) for some scheduler s returns false, no execution::execute(s,f) call with some invocable f shall block the calling thread.
-
-
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, set_error, and set_done. These are collectively known as a receiver’s completion-signal operations.
-
-
The receiver concept defines the requirements for a receiver type with an unknown set of value types. The receiver_of concept defines the requirements for a receiver type with a known set of value types, whose error type is std::exception_ptr.
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 execution::start has been called on the operation state object that was returned by execution::connect to connect that receiver to a sender.
-
-
Once execution::start 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.
-
-
If execution::set_value exits with an exception, it is still valid to call execution::set_error or execution::set_done on the receiver, but it is no longer valid to call execution::set_value on the receiver.
-
-
-
Once one of a receiver’s completion-signal operations has completed non-exceptionally, the receiver contract has been satisfied.
execution::set_value is used to send a value completion signal to a receiver.
-
-
The name execution::set_value denotes a customization point object. The expression execution::set_value(R,Vs...) for some subexpressions R and Vs... is expression-equivalent to:
-
-
-
tag_invoke(execution::set_value,R,Vs...), if that expression is valid. If the function selected by tag_invoke does not send the value(s) Vs... to the receiver R’s value channel, the program is ill-formed with no diagnostic required.
-
-
Otherwise, execution::set_value(R,Vs...) is ill-formed.
execution::set_error is used to send a error signal to a receiver.
-
-
The name execution::set_error denotes a customization point object. The expression execution::set_error(R,E) for some subexpressions R and E is expression-equivalent to:
-
-
-
tag_invoke(execution::set_error,R,E), if that expression is valid. If the function selected by tag_invoke does not send the error E to the receiver R’s error channel, the program is ill-formed with no diagnostic required.
-
-
Otherwise, execution::set_error(R,E) is ill-formed.
execution::set_done is used to send a done signal to a receiver.
-
-
The name execution::set_done denotes a customization point object. The expression execution::set_done(R) for some subexpression R is expression-equivalent to:
-
-
-
tag_invoke(execution::set_done,R), if that expression is valid. If the function selected by tag_invoke does not signal the receiver R’s done channel, the program is ill-formed with no diagnostic required.
execution::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]
-
-
The name execution::get_scheduler denotes a customization point object. For some subexpression r, let R be decltype((r)). If R does not satisfy execution::receiver, execution::get_scheduler is ill-formed. Otherwise, execution::get_scheduler(r) is
-expression equivalent to:
-
-
-
tag_invoke(execution::get_scheduler,as_const(r)), if this expression is well formed and satisfies execution::scheduler, and is noexcept.
-
-
Otherwise, execution::get_scheduler(r) is ill-formed.
execution::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]
-
-
The name execution::get_allocator denotes a customization point object. For some subexpression r, let R be decltype((r)). If R does not satisfy execution::receiver, execution::get_allocator is ill-formed. Otherwise, execution::get_allocator(r) is
-expression equivalent to:
-
-
-
tag_invoke(execution::get_allocator,as_const(r)), if this expression is well formed and models Allocator, and is noexcept.
-
-
Otherwise, execution::get_allocator(r) is ill-formed.
execution::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]
-
-
The name execution::get_stop_token denotes a customization point object. For some subexpression r, let R be decltype((r)). If R does not satisfy execution::receiver, execution::get_stop_token is ill-formed. Otherwise, execution::get_stop_token(r) is expression equivalent to:
-
-
-
tag_invoke(execution::get_stop_token,as_const(r)), if this expression is well formed and satisfies stoppable_token, and is noexcept.
-
-
Otherwise, never_stop_token{}.
-
-
-
Let r be a receiver, s be a sender, and op_state be an operation state resulting from an execution::connect(s,r) call. Let token be a stop token resulting from an execution::get_stop_token(r) call. token must remain valid at least until a call to
-a receiver completion-signal function of r returns successfully. [Note: this means that, unless it knows about further guarantees provided by the receiver r, the implementation of op_state should not use token after it makes a call to a receiver
-completion-signal function of r. This also implies that stop callbacks registered on token by the implementation of op_state or s must be destroyed before such a call to a receiver completion-signal function of r. --end note]
-
-
9.5. Operation states [execution.op_state]
-
-
-
The operation_state concept defines the requirements for an operation state type, which allows for starting the execution of work.
execution::start is used to start work represented by an operation state object.
-
-
The name execution::start denotes a customization point object. The expression execution::start(O) for some lvalue subexpression O is expression-equivalent to:
-
-
-
tag_invoke(execution::start,O), if that expression is valid. If the function selected by tag_invoke does not start the work represented by the operation state O, the program is ill-formed with no diagnostic required.
-
-
Otherwise, execution::start(O) is ill-formed.
-
-
-
The caller of execution::start(O) must guarantee that the lifetime of the operation state object O extends at least until one of the receiver completion-signal functions of a receiver R passed into the execution::connect call that produced O 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]
-
-
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 sender concept defines the requirements for a sender type. The sender_to concept defines the requirements for a sender type capable of being connected with a specific receiver type.
The class sender_base is used as a base class to tag sender types which do not expose member templates value_types, error_types, and a static member constant expression sends_done.
-
-
The class template sender_traits is used to query a sender type for facts associated with the signal it sends.
-
-
The primary class template sender_traits<S> also recognizes awaitables as typed senders. For this clause ([execution]):
-
-
-
An awaitable is an expression that would be well-formed as the operand of a co_await expression within a given context.
-
-
For any type T, is-awaitable<T> is 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 await_transform. For a coroutine promise type P, is-awaitable<T,P> is 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 a such that decltype((a)) is type A, await-result-type<A> is an alias for decltype(e), where e is a's await-resume expression ([expr.await]) within the context of a coroutine whose promise type does not define a member await_transform. For a coroutine promise type P, await-result-type<A,P> is an alias for decltype(e), where e is a's await-resume expression ([expr.await]) within the context of a coroutine whose promise type is P.
-
-
-
The primary class template sender_traits<S> is defined as if inheriting from an implementation-defined class template sender-traits-base<S> defined as follows:
-
-
-
If has-sender-types<S> is true, then sender-traits-base<S> is equivalent to:
template<classS>
- structsender-traits-base{
- using__unspecialized=void;// exposition only
- };
-
-
-
-
The exposition-only type variant-or-empty<Ts...> names the type variant<Ts...> if sizeof...(Ts) is greater than zero; otherwise, it names an implementation defined class type equivalent to the following:
If value_types_of_t<S,Tuple,Variant> for some sender type S is well formed, it shall be a type Variant<Tuple<Args0...>,Tuple<Args1...>,...,Tuple<ArgsN...>>>, where the type packs Args0 through ArgsN are the packs of types the sender S passes as
-arguments to execution::set_value after a receiver object. If such sender S odr-uses ([basic.def.odr]) execution::set_value(r,args...) for some receiver r, where decltype(args)... is not one of the type packs Args0... through ArgsN..., the program is ill-formed with no
-diagnostic required.
-
-
If error_types_of_t<S,Variant> for some sender type S is well formed, it shall be a type Variant<E0,E1,...,EN>, where the types E0 through EN are the types the sender S passes as arguments to execution::set_error after a receiver
-object. If such sender S odr-uses execution::set_error(r,e) for some receiver r, where decltype(e) is not one of the types E0 through EN, the program is ill-formed with no diagnostic required.
-
-
If sender_traits<S>::sends_done is well formed and false, and such sender S odr-uses execution::set_done(r) for some receiver r, the program is ill-formed with no diagnostic required.
-
-
Users may specialize sender_traits on program-defined types.
execution::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.
-
-
The name execution::connect denotes a customization point object. For some subexpressions s and r, let S be decltype((s)) and R be decltype((r)), and let S' and R' be the decayed types of S and R, respectively. If R does not satisfy execution::receiver, execution::connect(s,r) is ill-formed. Otherwise, the expression execution::connect(s,r) is expression-equivalent to:
-
-
-
tag_invoke(execution::connect,s,r), if that expression is valid, its type satisfies execution::operation_state, and S satisfies execution::sender. If the function selected by tag_invoke does not return an operation state for which execution::start starts work described by s, the program is ill-formed with no diagnostic required.
-
-
Otherwise, connect-awaitable(s,r) if is-awaitable<S,connect-awaitable-promise> is true and that expression is valid, where connect-awaitable is a coroutine equivalent to the following:
where connect-awaitable-promise is the promise type connect-awaitable, and where connect-awaitable suspends at the initial suspends point ([dcl.fct.def.coroutine]), and:
-
-
-
set-value-expr first evaluates co_await(S&&)s, then suspends the coroutine and evaluates execution::set_value((R&&)r) if await-result-type<S,connect-awaitable-promise> is cvvoid; otherwise, it evaluates auto&&res=co_await(S&&)s, then suspends the coroutine and evaluates execution::set_value((R&&)r,(decltype(res))res).
-
If the call to execution::set_value exits with an exception, the coroutine is resumed and the exception is immediately propagated in the context of the coroutine.
-
[Note: If the call to execution::set_value exits normally, then the connect-awaitable coroutine is never resumed. --end note]
-
-
set-error-expr first suspends the coroutine and then executes execution::set_error((R&&)r,std::move(ep)).
-
[Note: The connect-awaitable coroutine is never resumed after the call to execution::set_error. --end note]
-
-
operation-state-task is a type that models operation_state. Its execution::start resumes the connect-awaitable coroutine, advancing it past the initial suspend point.
-
-
The type connect-awaitable-promise satisfies receiver. [Note: It need not model receiver. -- end note].
-
-
Let p be an lvalue reference to the promise of the connect-awaitable coroutine, let b be a const lvalue reference to the receiver r, and let c be any customization point object excluding those of type set_value_t, set_error_t and set_done_t. Then std::tag_invoke(c,p,as...) is expression-equivalent to c(b,as...) for any set of arguments as....
-
-
The expression p.unhandled_done() is expression-equivalent to (execution::set_done((R&&)r),noop_coroutine()).
-
-
For some expression e, the expression p.await_transform(e) is expression-equivalent to tag_invoke(as_awaitable,e,p) if that expression is well-formed; otherwise, it is expression-equivalent to e.
-
-
The operand of the requires-clause of connect-awaitable is equivalent to receiver_of<R> if await-result-type<S,connect-awaitable-promise> is cvvoid; otherwise, it is receiver_of<R,await-result-type<S,connect-awaitable-promise>>.
-
-
Otherwise, execution::connect(s,r) is ill-formed.
-
-
-
Standard sender types shall always expose an rvalue-qualified overload of a customization of execution::connect. Standard sender types shall only expose an lvalue-qualified overload of a customization of execution::connect if they are copyable.
execution::get_completion_scheduler is used to ask a sender object for the completion scheduler for one of its signals.
-
-
The name execution::get_completion_scheduler denotes a customization point object template. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::sender, execution::get_completion_scheduler<CPO>(s) is ill-formed for all template arguments CPO. If the template
-argument CPO in execution::get_completion_scheduler<CPO> is not one of execution::set_value_t, execution::set_error_t, or execution::set_done_t, execution::get_completion_scheduler<CPO> is ill-formed. Otherwise, execution::get_completion_scheduler<CPO>(s) is expression-equivalent to:
-
-
-
tag_invoke(execution::get_completion_scheduler<CPO>,as_const(s)), if this expression is well formed and satisfies execution::scheduler, and is noexcept.
-
-
Otherwise, execution::get_completion_scheduler<CPO>(s) is ill-formed.
-
-
-
If, for some sender s and customization point object CPO, execution::get_completion_scheduler<decltype(CPO)>(s) is well-formed and results in a scheduler sch, and the sender s invokes CPO(r,args...), for some receiver r which has been connected to s, with additional arguments args..., on an execution agent which does not belong to the associated execution context of sch, the behavior is undefined.
execution::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.
-
-
The name execution::schedule denotes a customization point object. For some subexpression s, the expression execution::schedule(s) is expression-equivalent to:
-
-
-
tag_invoke(execution::schedule,s), if that expression is valid and its type satisfies execution::sender. If the function selected by tag_invoke does not return a sender whose set_value completion scheduler is equivalent to s, the program is ill-formed with no diagnostic required.
-
-
Otherwise, execution::schedule(s) is ill-formed.
-
-
-
9.6.4.3. execution::just[execution.senders.just]
-
-
-
execution::just is used to create a sender that propagates a set of values to a connected receiver.
execution::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.
-
-
The name execution::transfer_just denotes a customization point object. For some subexpressions s and vs..., let S be decltype((s)) and Vs... be decltype((vs)). If S does not satisfy execution::scheduler, or any type V in Vs does not
-satisfy movable-value, execution::transfer_just(s,vs...) is ill-formed. Otherwise, execution::transfer_just(s,vs...) is expression-equivalent to:
-
-
-
tag_invoke(execution::transfer_just,s,vs...), if that expression is valid and its type satisfies execution::typed_sender. If the function selected by tag_invoke does not return a sender whose set_value completion scheduler is equivalent to s and sends
-values equivalent to vs... to a receiver connected to it, the program is ill-formed with no diagnostic required.
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 execution::connect, and execution::start is called on the resulting operation state. This requirement applies to any function that is selected by the implementation of the sender adaptor.
-
-
Unless otherwise specified, all sender adaptors which accept a single sender 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.
-
-
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 execution::connect. This requirements
-applies to any sender returned from a function that is selected by the implementation of such sender adaptor.
A pipeable sender adaptor closure object is a function object that accepts one or more sender arguments and returns a sender. For a sender adaptor closure object C and an expression S such that decltype((S)) models sender, the following
-expressions are equivalent and yield a sender:
-
C(S)
-S|C
-
-
Given an additional pipeable sender adaptor closure object D, the expression C|D produces another pipeable sender adaptor closure object E:
-
E is a perfect forwarding call wrapper ([func.require]) with the following properties:
-
-
-
Its target object is an object d of type decay_t<decltype((D))> direct-non-list-initialized with D.
-
-
It has one bound argument entity, an object c of type decay_t<decltype((C))> direct-non-list-initialized with C.
-
-
Its call pattern is d(c(arg)), where arg is the argument used in a function call expression of E.
-
-
The expression C|D is well-formed if and only if the initializations of the state entities of E are all well-formed.
-
-
An object t of type T is a pipeable sender adaptor closure object if T models derived_from<sender_adaptor_closure<T>>, T has no other base
-classes of type sender_adaptor_closure<U> for any other type U, and T does not model sender.
-
-
The template parameter D for sender_adaptor_closure may be an incomplete type. Before any expression of type cvD appears as
-an operand to the | operator, D shall be complete and model derived_from<sender_adaptor_closure<D>>. The behavior of an expression involving an
-object of type cvD as an operand to the | operator is undefined if overload resolution selects a program-defined operator| function.
-
-
A pipeable sender adaptor object is a customization point object that accepts a sender as its first argument and returns a 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 adaptor accepts more than one argument, then let s be an expression such that decltype((s)) models sender,
-let args... be arguments such that adaptor(s,args...) is a well-formed expression as specified in the rest of this subclause
-([execution.senders.adaptor.objects]), and let BoundArgs be a pack that denotes decay_t<decltype((args))>.... The expression adaptor(args...) produces a pipeable sender adaptor closure object f that is a perfect forwarding call wrapper with the following properties:
-
-
-
Its target object is a copy of adaptor.
-
-
Its bound argument entities bound_args consist of objects of types BoundArgs... direct-non-list-initialized with std::forward<decltype((args))>(args)..., respectively.
-
-
Its call pattern is adaptor(r,bound_args...), where r is the argument used in a function call expression of f.
-
-
-
The expression adaptor(args...) is well-formed if and only if the initializations of the bound argument entities of the result, as specified above,
-are all well-formed.
execution::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.
-
-
The name execution::on denotes a customization point object. For some subexpressions sch and s, let Sch be decltype((sch)) and S be decltype((s)). If Sch does not satisfy execution::scheduler, or S does not satisfy execution::sender, execution::on is ill-formed. Otherwise, the expression execution::on(sch,s) is expression-equivalent to:
-
-
-
tag_invoke(execution::on,sch,s), if that expression is valid and its type satisfies execution::sender. If the function selected above does not return a sender which starts s on an execution agent of the associated execution context of sch when
-started, the program is ill-formed with no diagnostic required.
-
-
Otherwise, constructs a sender s1. When s1 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r:
-
-
-
When execution::set_value(r) is called, it calls execution::connect(s,r2), where r2 is as specified below, which results in op_state3. It calls execution::start(op_state3). If any of these throws an exception, it calls execution::set_error on out_r, passing current_exception() as the second argument.
-
-
When execution::set_error(r,e) is called, it calls execution::set_error(out_r,e).
-
-
When execution::set_done(r) is called, it calls execution::set_done(out_r).
-
-
-
Calls execution::schedule(sch), which results in s2. It then calls execution::connect(s2,r), resulting in op_state2.
-
-
op_state2 is wrapped by a new operation state, op_state1, that is returned to the caller.
-
-
-
r2 is a receiver that wraps a reference to out_r. It forwards all receiver completion signals and receiver queries to out_r. Additionally, it implements the get_scheduler receiver query. The scheduler returned from the query is equivalent to the sch argument that was passed to execution::on.
-
-
When execution::start is called on op_state1, it calls execution::start on op_state2.
-
-
The lifetime of op_state2, once constructed, lasts until either op_state3 is constructed or op_state1 is destroyed, whichever comes first. The lifetime of op_state3, once constructed, lasts until op_state1 is destroyed.
execution::transfer is used to adapt a sender into a sender with a different associated set_value completion scheduler. [Note: it results in a transition between different execution contexts when executed. --end note]
-
-
The name execution::transfer denotes a customization point object. For some subexpressions sch and s, let Sch be decltype((sch)) and S be decltype((s)). If Sch does not satisfy execution::scheduler, or S does not satisfy execution::sender, execution::transfer is ill-formed. Otherwise, the expression execution::transfer(s,sch) is expression-equivalent to:
-
-
-
tag_invoke(execution::transfer,get_completion_scheduler<set_value_t>(s),s,sch), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::transfer,s,sch), if that expression is valid and its type satisfies 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 execution::schedule_from(sch,s2), where s2 is a sender which sends equivalent to those sent by s, the program is ill-formed with no diagnostic required.
-
-
Senders returned from execution::transfer shall not propagate the sender queries get_completion_scheduler<CPO> to an input sender. They shall return a scheduler equivalent to the sch argument from those queries.
execution::schedule_from is used to schedule work dependent on the completion of a sender onto a scheduler’s associated execution context. [Note: schedule_from is not meant to be used in user code; they are used in the implementation of transfer. -end note]
-
-
The name execution::schedule_from denotes a customization point object. For some subexpressions sch and s, let Sch be decltype((sch)) and S be decltype((s)). If Sch does not satisfy execution::scheduler, or S does not satisfy execution::typed_sender, execution::schedule_from is ill-formed. Otherwise, the expression execution::schedule_from(sch,s) is expression-equivalent to:
-
-
-
tag_invoke(execution::schedule_from,sch,s), if that expression is valid and its type satisfies execution::sender. If the function selected by tag_invoke does not return a sender which completes on an execution agent belonging to the associated
-execution context of sch and sends signals equivalent to those sent by s, the program is ill-formed with no diagnostic required.
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r.
-
-
Calls execution::connect(s,r), which results in an operation state op_state2.
-
-
When a receiver completion-signal Signal(r,args...) is called, it constructs a receiver r2:
-
-
-
When execution::set_value(r2) is called, it calls Signal(out_r,args...).
-
-
When execution::set_error(r2,e) is called, it calls execution::set_error(out_r,e).
-
-
When execution::done(r2) is called, it calls execution::set_done(out_r).
-
-
It then calls execution::schedule(sch), resulting in a sender s3. It then calls execution::connect(s3,r2), resulting in an operation state op_state3. It then calls execution::start(op_state3). If any of these throws an exception,
-it catches it and calls execution::set_error(out_r,current_exception()).
-
-
Returns an operation state op_state that contains op_state2. When execution::start(op_state) is called, calls execution::start(op_state2). The lifetime of op_state3 ends when op_state is destroyed.
-
-
-
-
Senders returned from execution::schedule_from shall not propagate the sender queries get_completion_scheduler<CPO> to an input sender. They shall return a scheduler equivalent to the sch argument from those queries.
execution::then is used to attach invocables as continuation for successful completion of the input sender.
-
-
The name execution::then denotes a customization point object. For some subexpressions s and f, let S be decltype((s)). If S does not satisfy execution::sender, execution::then is ill-formed. Otherwise, the expression execution::then(s,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::then,get_completion_scheduler<set_value_t>(s),s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::then,s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r:
-
-
-
When execution::set_value(r,args...) is called, calls invoke(f,args...) and passes the result v to execution::set_value(out_r,v). If any of these throws an exception, it catches it and calls execution::set_error(out_r,current_exception()).
-
-
When execution::set_error(r,e) is called, calls execution::set_error(out_r,e).
-
-
When execution::set_done(r) is called, calls execution::set_done(out_r).
-
-
-
Calls execution::connect(s,r), which results in an operation state op_state2.
-
-
Returns an operation state op_state that contains op_state2. When execution::start(op_state) is called, calls execution::start(op_state2).
-
-
-
If the function selected above does not return a sender which invokes f with the result of the set_value signal of s, passing the return value as the value to any connected receivers, and propagates the other completion-signals sent by s, the program is
- ill-formed with no diagnostic required.
execution::upon_error is used to attach invocables as continuation for unsuccessul completion of the input sender.
-
-
The name execution::upon_error denotes a customization point object. For some subexpressions s and f, let S be decltype((s)). If S does not satisfy execution::sender, execution::upon_error is ill-formed. Otherwise, the expression execution::upon_error(s,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::upon_error,get_completion_scheduler<set_error_t>(s),s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::upon_error,s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r:
-
-
-
When execution::set_value(r,args...) is called, calls execution::set_value(out_r,args...).
-
-
When execution::set_error(r,e) is called, calls invoke(f,e) and passes the result v to execution::set_value(out_r,v). If any of these throws an exception, it catches it and calls execution::set_error(out_r,current_exception()).
-
-
When execution::set_done(r) is called, calls execution::set_done(out_r).
-
-
-
Calls execution::connect(s,r), which results in an operation state op_state2.
-
-
Returns an operation state op_state that contains op_state2. When execution::start(op_state) is called, calls execution::start(op_state2).
-
-
-
If the function selected above does not return a sender which invokes f with the result of the set_error signal of s, passing the return value as the value to any connected receivers, and propagates the other completion-signals sent by s, the program is
- ill-formed with no diagnostic required.
execution::upon_done is used to attach invocables as continuation for the completion of the input sender using the "done" channel.
-
-
The name execution::upon_done denotes a customization point object. For some subexpressions s and f, let S be decltype((s)). If S does not satisfy execution::sender, execution::upon_done is ill-formed. Otherwise, the expression execution::upon_done(s,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::upon_done,get_completion_scheduler<set_done_t>(s),s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::upon_done,s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r:
-
-
-
When execution::set_value(r,args...) is called, calls execution::set_value(out_r,args...).
-
-
When execution::set_error(r,e) is called, calls execution::set_error(out_r,e).
-
-
When execution::set_done(r) is called, calls invoke(f) and passes the result v to execution::set_value(out_r,v). If any of these throws an exception, it catches it and calls execution::set_error(out_r,current_exception()).
-
-
-
Calls execution::connect(s,r), which results in an operation state op_state2.
-
-
Returns an operation state op_state that contains op_state2. When execution::start(op_state) is called, calls execution::start(op_state2).
-
-
-
If the function selected above does not return a sender which invokes f when the set_done signal of s is called, passing the return value as the value to any connected receivers, and propagates the other completion-signals sent by s, the program is
- ill-formed with no diagnostic required.
execution::let_value is used to insert continuations creating more work dependent on the results of their input senders into a sender chain.
-
-
The name execution::let_value denotes a customization point object. For some subexpressions s and f, let S be decltype((s)). If S does not satisfy execution::sender, execution::let_value is ill-formed. Otherwise, the expression execution::let_value(s,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::let_value,get_completion_scheduler<set_value_t>(s),s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::let_value,s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r.
-
-
-
When execution::set_value(r,args...) is called, decay-copies args... into op_state2 as args2..., then calls invoke(f,args2...), resulting in a sender s3. It then calls execution::connect(s3,out_r), resulting in an operation state op_state3. op_state3 is saved as a part of op_state2. It then calls execution::start(op_state3). If any of these throws an exception, it catches it and calls execution::set_error(out_r,current_exception()).
-
-
When execution::set_error(r,e) is called, calls execution::set_error(out_r,e).
-
-
When execution::set_done(r) is called, calls execution::set_done(out_r).
-
-
-
Calls execution::connect(s,r), which results in an operation state op_state2.
-
-
Returns an operation state op_state that contains op_state2. When execution::start(op_state) is called, calls execution::start(op_state2).
-
-
-
If the function selected above does not return a sender which invokes f when set_value is called, and making its completion dependent on the completion of a sender returned by f, and propagates the other completion-signals sent by s, the program is
- ill-formed with no diagnostic required.
execution::let_error is used to insert continuations creating more work dependent on the results of their input senders into a sender chain.
-
-
The name execution::let_error denotes a customization point object. For some subexpressions s and f, let S be decltype((s)). If S does not satisfy execution::sender, execution::let_error is ill-formed. Otherwise, the expression execution::let_error(s,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::let_error,get_completion_scheduler<set_error_t>(s),s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::let_error,s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r.
-
-
-
When execution::set_value(r,args...) is called, calls execution::set_value(out_r,args...).
-
-
When execution::set_error(r,e) is called, decay-copies e into op_state2 as e2, then calls invoke(f,e2), resulting in a sender s3. It then calls execution::connect(s3,out_r), resulting in an operation state op_state3. op_state3 is saved
-as a part of op_state2. It then calls execution::start(op_state3). If any of these throws an exception, it catches it and calls execution::set_error(out_r,current_exception()).
-
-
When execution::set_done(r) is called, calls execution::set_done(out_r).
-
-
-
Calls execution::connect(s,r), which results in an operation state op_state2.
-
-
Returns an operation state op_state that contains op_state2. When execution::start(op_state) is called, calls execution::start(op_state2).
-
-
-
If the function selected above does not return a sender which invokes f when set_error is called, and making its completion dependent on the completion of a sender returned by f, and propagates the other completion-signals sent by s, the program is
- ill-formed with no diagnostic required.
execution::let_done is used to insert continuations creating more work dependent on the results of their input senders into a sender chain.
-
-
The name execution::let_done denotes a customization point object. For some subexpressions s and f, let S be decltype((s)). If S does not satisfy execution::sender, execution::let_done is ill-formed. Otherwise, the expression execution::let_done(s,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::let_done,get_completion_scheduler<set_done_t>(s),s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::let_done,s,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r.
-
-
-
When execution::set_value(r,args...) is called, calls execution::set_value(out_r,args...).
-
-
When execution::set_error(r,e) is called, calls execution::set_error(out_r,e).
-
-
When execution::set_done(r) is called, calls invoke(f), resulting in a sender s3. It then calls execution::connect(s3,out_r), resulting in an operation state op_state3. op_state3 is saved as a part of op_state2.
-It then calls execution::start(op_state3). If any of these throws an exception, it catches it and calls execution::set_error(out_r,current_exception()).
-
-
-
Calls execution::connect(s,r). which results in an operation state op_state2.
-
-
Returns an operation state op_state that contains op_state2. When execution::start(op_state) is called, calls execution::start(op_state2).
-
-
-
If the function selected above does not return a sender which invokes f when set_done is called, and making its completion dependent on the completion of a sender returned by f, and propagates the other completion-signals sent by s, the program is
- ill-formed with no diagnostic required.
execution::bulk is used to run a task repeatedly for every index in an index space.
-
-
The name execution::bulk denotes a customization point object. For some subexpressions s, shape, and f, let S be decltype((s)), Shape be decltype((shape)), and F be decltype((f)). If S does not satisfy execution::sender or Shape does not satisfy integral, execution::bulk is ill-formed. Otherwise, the expression execution::bulk(s,shape,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::bulk,get_completion_scheduler<set_value_t>(s),s,shape,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::bulk,s,shape,f), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r:
-
-
-
When execution::set_value(r,args...) is called, calls f(i,args...) for each i of type Shape from 0 to shape, then calls execution::set_value(out_r,args...). If any of these throws an exception, it catches it and calls execution::set_error(out_r,current_exception()).
-
-
When execution::set_error(r,e) is called, calls execution::set_error(out_r,e).
-
-
When execution::set_done(r) is called, calls execution::set_done(out_r,e).
-
-
-
Calls execution::connect(s,r), which results in an operation state op_state2.
-
-
Returns an operation state op_state that contains op_state2. When execution::start(op_state) is called, calls execution::start(op_state2).
-
-
-
If the function selected above does not return a sender which invokes f(i,args...) for each i of type Shape from 0 to shape when the input sender sends values args..., 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.
execution::split is used to adapt an arbitrary sender into a sender that can be connected multiple times.
-
-
The name execution::split denotes a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::typed_sender, execution::split is ill-formed. Otherwise, the expression execution::split(s) is expression-equivalent to:
-
-
-
tag_invoke(execution::split,get_completion_scheduler<set_value_t>(s),s), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::split,s), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, constructs a sender s2, which:
-
-
-
Creates an object sh_state. The lifetime of sh_state shall last for at least as long as the lifetime of the last operation state object returned from execution::connect(s,some_r) for some receiver some_r.
-
-
Constructs a receiver r:
-
-
-
When execution::set_value(r,args...) is called, saves the expressions args... as subobjects of sh_state.
-
-
When execution::set_error(r,e) is called, saves the expression e as a subobject of sh_state.
-
-
When execution::set_done(r) is called, saves this fact in sh_state.
-
-
-
Calls execution::connect(s,r), resulting in an operation state op_state2. op_state2 is saved as a subobject of sh_state.
-
-
When s2 is connected with a receiver out_r, it returns an operation state object op_state. When execution::start(op_state) is called, it calls execution::start(op_state2), if this is the first time this expression would be evaluated. When both execution::start(op_state) and Signal(r,args...) have been called, calls Signal(out_r,args2...), where args2... is a pack of lvalues referencing the subobjects of sh_state that have been saved by the
-original call to Signal(r,args...).
-
-
-
If the function selected above does not return a sender which sends references to values sent by s, propagating the other channels, the program is ill-formed with no diagnostic required.
execution::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_with_variant 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.
-
-
The name execution::when_all denotes a customization point object. For some subexpressions si..., let Si... be decltype((si)).... The expression execution::when_all(si...) is ill-formed if any of the following is true:
-
-
-
If the number of subexpressions si... is 0, or
-
-
If any type Si does not satisfy execution::typed_sender, or
-
-
If for any type Si, the type value_types_of_t<Si,tuple,zero-or-one> is ill-formed, where zero-or-one is a template alias equivalent to the following:
Otherwise, the expression execution::when_all(si...) is expression-equivalent to:
-
-
-
tag_invoke(execution::when_all,si...), if that expression is valid and its type satisfies execution::sender. If the function selected by tag_invoke does not return a sender that sends a concatenation of values sent by si... when they all complete with set_value, the program is ill-formed with no diagnostic required.
-
-
Otherwise, constructs a sender w of type W. When w is connected with some receiver out_r of type OutR, it returns an operation state op_state specified as below:
-
-
-
For each sender si, constructs a receiver ri such that:
-
-
-
If execution::set_value(ri,ti...) is called for every ri, op_state's associated stop callback optional is reset and execution::set_value(out_r,t0...,t1...,...,tn-1...) is called, where n the number of subexpressions in si....
-
-
Otherwise, execution::set_error or execution::set_done was called for at least one receiver ri. If the first such to complete did so with the call execution::set_error(ri,e), request_stop is called on op_state's associated stop source. When all child operations have completed, op_state's associated stop callback optional is reset and execution::set_error(out_r,e) is called.
-
-
Otherwise, request_stop is called on op_state's associated stop source. When all child operations have completed, op_state's associated stop callback optional is reset and execution::set_done(out_r) is called.
-
-
For each receiver ri, execution::get_stop_token(ri) is well-formed and returns the results of calling get_token() on op_state's associated stop source.
-
-
-
For each sender si, calls execution::connect(si,ri), resulting in operation states child_opi.
-
-
Returns an operation state op_state that contains:
-
-
-
Each operation state child_opi,
-
-
A stop source of type in_place_stop_source,
-
-
A stop callback of type optional<stop_token_of_t<OutR&>::callback_type<stop-callback-fn>>, where stop-callback-fn is an implementation defined class type equivalent to the following:
Emplace constructs the stop callback optional with the arguments execution::get_stop_token(out_r) and stop-callback-fn{stop-src}, where stop-src refers to the stop source of op_state.
-
-
Then, it checks to see if stop-src.stop_requested() is true. If so, it calls execution::set_done(out_r).
-
-
Otherwise, calls execution::start(child_opi) for each child_opi.
-
-
-
The associated types of the sender W are as follows:
-
-
-
value_types_of_t<W,Tuple,Variant> is:
-
-
-
Variant<> if for any type Si, the type value_types_of_t<Si,Tuple,Variant> is Variant<>.
-
-
Otherwise, Variant<Tuple<V0...,V1,...,Vn-1...>> where n is the count of types in Si..., and where Vi... is a set of types such that for the type Si, value_types_of_t<Si,Tuple,Variant> is an alias for Variant<Tuple<Vi...>>.
-
-
-
error_types_of_t<W,Variant> is Variant<exception_ptr,Ui...>, where Ui... is the unique set of types in E0...,E1,...,En-1..., where Ei... is a set of types such that for the type Si, error_types_of_t<Si,Variant> is an alias for Variant<Ei...>.
-
-
sender_traits<W>::sends_done is true.
-
-
-
-
-
The name execution::when_all_with_variant denotes a customization point object. For some subexpressions s..., let S be decltype((s)). If any type Si in S... does not satisfy execution::typed_sender, execution::when_all_with_variant is ill-formed. Otherwise, the expression execution::when_all_with_variant(s...) is expression-equivalent to:
-
-
-
tag_invoke(execution::when_all_with_variant,s...), if that expression is valid and its type satisfies execution::sender. If the function selected by tag_invoke does not return a sender which sends the types into-variant-type<S>... when they all complete with set_value, the program is ill-formed with no diagnostic required.
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 that only send a single set of values each, while also making sure
-that they complete on the specified scheduler. execution::transfer_when_all_with_variant 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]
-
-
The name execution::transfer_when_all denotes a customization point object. For some subexpressions sch and s..., let Sch be decltype(sch) and S be decltype((s)). If Sch does not satisfy scheduler, or any type Si in S... does not satisfy execution::typed_sender, or the number of the arguments sender_traits<Si>::value_types passes into the Variant template parameter is not 1, execution::transfer_when_all is ill-formed. Otherwise, the expression execution::transfer_when_all(sch,s...) is expression-equivalent to:
-
-
-
tag_invoke(execution::transfer_when_all,sch,s...), if that expression is valid and its type satisfies execution::sender. If the function selected by tag_invoke does not return a sender which sends a concatenation of values sent by s... when
-they all complete with set_value, 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 of sch, the program is ill-formed with no diagnostic
-required.
-
-
Otherwise, transfer(when_all(s...),sch).
-
-
-
The name execution::transfer_when_all_with_variant denotes a customization point object. For some subexpressions s..., let S be decltype((s)). If any type Si in S... does not satisfy execution::typed_sender, execution::transfer_when_all_with_variant is ill-formed. Otherwise, the expression execution::transfer_when_all_with_variant(s...) is expression-equivalent to:
-
-
-
tag_invoke(execution::transfer_when_all_with_variant,s...), if that expression is valid and its type satisfies execution::sender. If the function selected by tag_invoke does not return a sender which sends the types into-variant-type<S>... when they all complete with set_value, the program is ill-formed with no diagnostic required.
Senders returned from execution::transfer_when_all shall not propagate the sender queries get_completion_scheduler<CPO> to input senders. They shall return a scheduler equivalent to the sch argument from those queries.
execution::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.
-
-
The template into-variant-type is used to compute the type sent by a sender returned from execution::into_variant.
execution::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.
-
-
The name execution::done_as_optional denotes a customization point object. For some subexpression s., let S be decltype((s)). If the type S does not satisfy single-typed-sender, execution::done_as_optional(s) is ill-formed. Otherwise, the expression execution::done_as_optional(s) is expression-equivalent to:
execution::done_as_error is used to handle a done signal by mapping it into the error channel as an exception_ptr that refers to a custom exception type. The result is a sender that never completes with done, reporting cancellation by completing with an error.
-
-
The template into-variant-type is used to compute the type sent by a sender returned from execution::into_variant.
execution::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.
-
-
The name execution::ensure_started denotes a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::typed_sender, execution::ensure_started is ill-formed. Otherwise, the expression execution::ensure_started(s) is expression-equivalent to:
-
-
-
tag_invoke(execution::ensure_started,get_completion_scheduler<set_value_t>(s),s), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise, tag_invoke(execution::ensure_started,s), if that expression is valid and its type satisfies execution::sender.
-
-
Otherwise:
-
-
-
Constructs a receiver r.
-
-
Calls execution::connect(s,r), resulting in operation state op_state, and then calls execution::start(op_state).
-
-
Constructs a sender s2. When s2 is connected with some receiver out_r, it results in an operation state op_state2. Once both execution::start(op_state2) and one of the receiver completion-signals has been called on r:
-
-
-
If execution::set_value(r,ts...) has been called, calls execution::set_value(out_r,ts...).
-
-
If execution::set_error(r,e) has been called, calls execution::set_error(out_r,e).
-
-
If execution::set_done(r) has been called, calls execution::set_done(out_r).
-
-
The lifetime of op_state lasts until all three of the following have occured:
-
-
-
the lifetime of op_state2 has ended,
-
-
the lifetime of s2 has ended, and
-
-
a receiver completion-signal has been called on r.
-
-
-
-
If the function selected above does not eagerly start the sender s and return a sender which propagates the signals sent by s once started, the program is ill-formed with no diagnostic required.
-
-
Note: The wording for execution::ensure_started 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.
execution::start_detached is used to eagerly start a sender without the caller needing to manage the lifetimes of any objects.
-
-
The name execution::start_detached denotes a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::sender, execution::start_detached is ill-formed. Otherwise, the expression execution::start_detached(s) is expression-equivalent to:
-
-
-
tag_invoke(execution::start_detached,execution::get_completion_scheduler<execution::set_value_t>(s),s), if that expression is valid and its type is void.
-
-
Otherwise, tag_invoke(execution::start_detached,s), if that expression is valid and its type is void.
-
-
Otherwise:
-
-
-
Constructs a receiver r:
-
-
-
When set_value(r,ts...) is called, it does nothing.
-
-
When set_error(r,e) is called, it calls std::terminate.
-
-
When set_done(r) is called, it does nothing.
-
-
-
Calls execution::connect(s,r), resulting in an operation state op_state, then calls execution::start(op_state). The lifetime of op_state lasts until one of the receiver completion-signals of r is called.
-
-
-
If the function selected above does not eagerly start the sender s after connecting it with a receiver which ignores the set_value and set_done signals and calls std::terminate on the set_error signal, the program is ill-formed with no diagnostic required.
this_thread::sync_wait and this_thread::sync_wait_with_variant 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.
-
-
The templates sync-wait-type and sync-wait-with-variant-type are used to determine the return types of this_thread::sync_wait and this_thread::sync_wait_with_variant.
The name this_thread::sync_wait denotes a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::typed_sender, or the number of the arguments sender_traits<S>::value_types passes into the Variant template parameter is not 1, this_thread::sync_wait is ill-formed. Otherwise, this_thread::sync_wait is expression-equivalent to:
-
-
-
tag_invoke(this_thread::sync_wait,execution::get_completion_scheduler<execution::set_value_t>(s),s), if this expression is valid and its type is sync-wait-type<S>.
-
-
Otherwise, tag_invoke(this_thread::sync_wait,s), if this expression is valid and its type is sync-wait-type<S>.
-
-
Otherwise:
-
-
-
Constructs a receiver r.
-
-
Calls execution::connect(s,r), resulting in an operation state op_state, then calls execution::start(op_state).
-
-
Blocks the current thread until a receiver completion-signal of r is called. When it is:
-
-
-
If execution::set_value(r,ts...) has been called, returns sync-wait-type<S>(make_tuple(ts...))>.
-
-
If execution::set_error(r,e...) has been called, if remove_cvref_t(decltype(e)) is exception_ptr, calls std::rethrow_exception(e). Otherwise, throws e.
-
-
If execution::set_done(r) has been called, returns sync-wait-type<S(nullopt)>.
-
-
-
-
-
The name this_thread::sync_wait_with_variant denotes a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::typed_sender, this_thread::sync_wait_with_variant is ill-formed. Otherwise, this_thread::sync_wait_with_variant is expression-equivalent to:
-
-
-
tag_invoke(this_thread::sync_wait_with_variant,execution::get_completion_scheduler<execution::set_value_t>(s),s), if this expression is valid and its type is sync-wait-with-variant-type<S>.
-
-
Otherwise, tag_invoke(this_thread::sync_wait_with_variant,s), if this expression is valid and its type is sync-wait-with-variant-type<S>.
Any receiver r created by an implementation of sync_wait and sync_wait_with_variant shall implement the get_scheduler 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 of execution::run_loop is one valid implementation. -- end note]
-
-
9.7. execution::execute[execution.execute]
-
-
-
execution::execute is used to create fire-and-forget tasks on a specified scheduler.
-
-
The name execution::execute denotes a customization point object. For some subexpressions sch and f, let Sch be decltype((sch)) and F be decltype((f)). If Sch does not satisfy execution::scheduler or F does not satisfy invocable<>, execution::execute is ill-formed. Otherwise, execution::execute is expression-equivalent to:
-
-
-
tag_invoke(execution::execute,sch,f), if that expression is valid and its type is void. If the function selected by tag_invoke does not invoke the function f on an execution agent belonging to the associated execution context of sch, or if it
-does not call std::terminate if an error occurs after control is returned to the caller, the program is ill-formed with no diagnostic required.
receiver_adaptor is used to simplify the implementation of one receiver type in terms of another. It defines tag_invoke overloads that forward to named members if they exist, and to the adapted receiver otherwise.
-
-
This section makes use of the following exposition-only entities:
This section specifies some execution contexts on which work can be scheduled.
-
-
9.9.1. run_loop[execution.contexts.run_loop]
-
-
-
A run_loop is an execution context on which work can be scheduled. It maintains a simple, thread-safe first-in-first-out queue of work. Its run() member function removes elements from the queue and executes them in a loop on whatever thread of execution calls run().
-
-
A run_loop instance has an associated count that corresponds to the number of work items that are in its queue. Additionally, a run_loop has an associated state that can be one of starting, running, or finishing.
-
-
Concurrent invocations of the member functions of run_loop, other than run and its destructor, do not introduce data races. The member functions pop_front, push_back, and finish execute atomically.
-
-
[Note: Implementations are encouraged to use an intrusive queue of operation states to hold the work units to make scheduling allocation-free. — end note]
-
classrun_loop{
- // [execution.contexts.run_loop.types] Associated types
- classrun-loop-scheduler;// exposition only
- classrun-loop-sender;// exposition only
- structrun-loop-opstate-base{// exposition only
- virtualvoidexecute()=0;
- run_loop*loop_;
- run-loop-opstate-base*next_;
- };
- template<receiver_ofR>
- usingrun-loop-opstate=unspecified;// exposition only
-
- // [execution.contexts.run_loop.members] Member functions:
- run-loop-opstate-base*pop_front();// exposition only
- voidpush_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-schedulerget_scheduler();
- voidrun();
- voidfinish();
-};
-
run-loop-scheduler is an implementation defined type that models the scheduler concept.
-
-
Instances of run-loop-scheduler remain valid until the end of the lifetime of the run_loop instance from which they were obtained.
-
-
Two instances of run-loop-scheduler compare equal if and only if they were obtained from the same run_loop instance.
-
-
Let sch be an expression of type run-loop-scheduler. The expression execution::schedule(sch) is not potentially throwing and has type run-loop-sender.
-
-
classrun-loop-sender;
-
-
-
-
run-loop-sender is an implementation defined type that models the sender_of concept; i.e.,sender_of<run-loop-sender> is true. Additionally, the types reported by its error_types associated type is exception_ptr, and the value of its sends_done trait is true.
-
-
An instance of run-loop-sender remains valid until the end of the lifetime of its associated execution::run_loop instance.
-
-
Let s be an expression of type run-loop-sender, let r be an expression such that decltype(r) models the receiver_of concept, and let C be one of set_value_t, set_error_t, or set_done_t. Then:
-
-
-
The expression execution::connect(s,r) has type run-loop-opstate<decay_t<decltype(r)>> and is potentially throwing if and only if the initialiation of decay_t<decltype(r)> from r is potentially throwing.
-
-
The expression get_completion_scheduler<C>(s) is not potentially throwing, has type run-loop-scheduler, and compares equal to the run-loop-scheduler instance from which s was obtained.
run-loop-opstate<R> is an alias for an unspecified non-template class type that inherits unambiguously from run-loop-opstate-base.
-
-
Let o be a non-const lvalue of type run-loop-opstate<R>, and let REC(o) be a non-const lvalue reference to an instance of type R that was initialized with the expression r passed to the invocation of execution::connect that returned o. Then:
-
-
-
The object to which REC(o) refers remains valid for the lifetime of the object to which o refers.
-
-
The type run-loop-opstate<R> overrides run-loop-opstate-base::execute() such that o.execute() is equivalent to the following:
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:
Alias template single-sender-value-type is defined as follows:
-
-
-
If value_types_of_t<S,Tuple,Variant> would have the form Variant<Tuple<T>>, then single-sender-value-type<S> is an alias for type T.
-
-
Otherwise, if value_types_of_t<S,Tuple,Variant> would have the form Variant<Tuple<>> or Variant<>, then single-sender-value-type<S> is an alias for type void.
-
-
Otherwise, single-sender-value-type<S> is ill-formed.
-
-
-
The type sender-awaitable<S> names an unspecified non-template class type equivalent to the following:
Let r be an rvalue expression of type awaitable-receiver, let cr be a const lvalue that refers to r, let v be an expression of type result_t, let err be an arbitrary expression of type Err, let c be a customization point object, and let as... be a pack of arguments. Then:
-
-
-
If value_t is void, then execution::set_value(r) is expression-equivalent to (result_ptr_->emplace<1>(),continuation_.resume()); otherwise, execution::set_value(r,v) is expression-equivalent to (result_ptr_->emplace<1>(v),continuation_.resume()).
-
-
execution::set_error(r,e) is expression-equivalent to (result_ptr_->emplace<2>(AS_EXCEPT_PTR(err)),continuation_.resume()), where AS_EXCEPT_PTR(err) is:
-
-
-
err if decay_t<Err> names the same type as exception_ptr,
-
-
Otherwise, make_exception_ptr(system_error(err)) if decay_t<Err> names the same type as error_code,
-
-
Otherwise, make_exception_ptr(err).
-
-
-
execution::set_done(r) is expression-equivalent to continuation_.promise().unhandled_done().resume().
-
-
tag_invoke(c,cr,as...) is expression-equivalent to c(as_const(p),as...) when the type of c is not one of execution::set_value_t, execution::set_error_t, or execution::set_done_t.
as_awaitable is a customization point object. For some subexpressions e and p where p is an lvalue, E names the type decltype((e)) and P names the type decltype((p)), as_awaitable(e,p) is expression-equivalent to the following:
-
-
-
tag_invoke(as_awaitable,e,p) if that expression is well-formed and is-awaitable<tag_invoke_result_t<as_awaitable_t,E,P>,decay_t<P>> is true.
-
-
Otherwise, e if is-awaitable<E> is true.
-
-
Otherwise, sender-awaitable{e,p} if awaitable-sender<E,P> is true.
with_awaitable_senders, when used as the base class of a coroutine promise type, makes senders awaitable in that coroutine type.
-
In addition, it provides a default implementation of unhandled_done() such that if a sender completes by calling execution::set_done, it is treated as if an uncatchable "done" exception were thrown from the await-expression. In practice, the coroutine is never resumed, and the unhandled_done of the coroutine caller’s promise type is called.
ISO/IEC JTC1/SC22/WG21 14882: Programming Language — C++
-
Audience:
-
SG1, LEWG
-
-
-
-
-
-
-
-
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 needs 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 of concurrency primitives (std::atomic, std::mutex, std::counting_semaphore, etc) and lower level building blocks (std::thread, etc), we lack a Standard vocabulary and framework for asynchrony and parallelism that C++ programmers desperately need. std::async/std::future/std::promise, 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.
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. A sender describes asynchronous work and sends a signal (value, error, or stopped) 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 std::invocable, and calls the std::invocable on the signal sent by the input sender. The sender returned by then sends the result of that invocation. In this case, the input sender came from schedule, so its void, meaning it won’t send us a value, so our std::invocable takes no parameters. But we return an int, which will be sent to the next recipient.
-
-
Now, we add another operation to the chain, again using § 4.21.2 execution::then. This time, we get sent a value - the int from the previous step. We add 42 to it, and then return the result.
-
-
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 std::optional<std::tuple<...>> with the value sent by the last sender, or an empty std::optional if the last sender sent a stopped signal, or it throws an exception if the last sender sent an error.
This example builds an asynchronous computation of an inclusive scan:
-
-
-
It scans a sequence of doubles (represented as the std::span<constdouble>input) and stores the result in another sequence of doubles (represented as std::span<double>output).
-
-
It takes a scheduler, which specifies what execution context the scan should be launched on.
-
-
It also takes a tile_count parameter that controls the number of execution agents that will be spawned.
-
-
First we need to allocate temporary storage needed for the algorithm, which we’ll do with a std::vector, partials. We need one double of temporary storage for each execution agent we create.
-
-
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 sch, which means the next item in the chain will use sch.
-
-
Senders and sender adaptors support composition via operator|, similar to C++ ranges. We’ll use operator| to attach the next piece of work, which will spawn tile_count execution agents using § 4.21.9 execution::bulk (see § 4.13 Most sender adaptors are pipeable for details).
-
-
Each agent will call a std::invocable, passing it two arguments. The first is the agent’s index (i) in the § 4.21.9 execution::bulk operation, in this case a unique integer in [0,tile_count). The second argument is what the input sender sent - the temporary storage.
-
-
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 std::inclusive_scan over our elements. We store the scan result for our last element, which is the sum of all of our elements, in our temporary storage 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 partials. 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.
-
-
§ 4.21.2 execution::then takes an input sender and an std::invocable and calls the std::invocable with the value sent by the input sender. Inside our std::invocable, we call std::inclusive_scan on partials, which the input senders will send to us.
-
-
Then we return partials, which the next phase will need.
-
-
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 partials to integrate the sums from other tiles into our elements, completing the inclusive scan.
-
-
async_inclusive_scan returns a sender that sends the output std::span<double>. A consumer of the algorithm can chain additional work that uses the scan result. At the point at which async_inclusive_scan returns, the computation may not have completed. In fact, it may not have even started.
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:
-
-
-
async_read 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 a std::span<std::byte>, and a handle to an I/O context. It will asynchronously read into the input buffer, up to the size of the std::span. It returns a sender which will send the number of bytes read once the read completes.
-
-
async_read_array 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 a dynamic_buffer object that owns the data that was sent.
-
-
dynamic_buffer is an aggregate struct that contains a std::unique_ptr<std::byte[]> and a size.
-
-
The first thing we do inside of async_read_array is create a sender that will send a new, empty dynamic_array object using § 4.20.2 execution::just. We can attach more work to the pipeline using operator| composition (see § 4.13 Most sender adaptors are pipeable for details).
-
-
We need the lifetime of this dynamic_array object to last for the entire pipeline. So, we use let_value, which takes an input sender and a std::invocable that must return a sender itself (see § 4.21.4 execution::let_* for details). let_value sends the value from the input sender to the std::invocable. Critically, the lifetime of the sent object will last until the sender returned by the std::invocable completes.
-
-
Inside of the let_valuestd::invocable, we have the rest of our logic. First, we want to initiate an async_read of the buffer size. To do that, we need to send a std::span pointing to buf.size. We can do that with § 4.20.2 execution::just.
Next, we pipe a std::invocable that will be invoked after the async_read completes using § 4.21.2 execution::then.
-
-
That std::invocable gets sent the number of bytes read.
-
-
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 std::span<std::byte> to the storage for the data from the std::invocable. This will be sent to the next recipient in the pipeline.
-
-
And that recipient will be another async_read, which will read the data.
-
-
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 dynamic_buffer object. It will get sent by the sender returned by async_read_array. We can attach more things to that sender to use the data in the buffer.
-
-
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 async_recv() operation for a Windows Socket.
-
structoperation_base:WSAOVERALAPPED{
- usingcompletion_fn=void(operation_base*op,DWORDbytesTransferred,interrorCode)noexcept;
-
- // Assume IOCP event loop will call this when this OVERLAPPED structure is dequeued.
- completion_fn*completed;
-};
-
-template<typenameReceiver>
-structrecv_op:operation_base{
- recv_op(SOCKETs,void*data,size_tlen,Receiverr)
- :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);
- }
-
- friendvoidtag_invoke(std::tag_t<std::execution::start>,recv_op&self)noexcept{
- // Avoid even calling WSARecv() if operation already cancelled
- autost=std::execution::get_stop_token(
- std::execution::get_env(self.receiver));
- if(st.stop_requested()){
- std::execution::set_stopped(std::move(self.receiver));
- return;
- }
-
- // Store and cache result here in case it changes during execution
- constboolstopPossible=st.stop_possible();
- if(!stopPossible){
- self.ready.store(true,std::memory_order_relaxed);
- }
-
- // Launch the operation
- DWORDbytesTransferred=0;
- DWORDflags=0;
- intresult=WSARecv(self.sock,&self.buffer,1,&bytesTransferred,&flags,
- static_cast<WSAOVERLAPPED*>(&self), NULL);
- if(result==SOCKET_ERROR){
- interrorCode=WSAGetLastError();
- if(errorCode!=WSA_IO_PENDING)){
- if(errorCode==WSA_OPERATION_ABORTED){
- std::execution::set_stopped(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();
-
- BOOLok=WSAGetOverlappedResult(self.sock,(WSAOVERLAPPED*)&self,&bytesTransferred,FALSE,&flags);
- if(ok){
- std::execution::set_value(std::move(self.receiver),bytesTransferred);
- }else{
- interrorCode=WSAGetLastError();
- std::execution::set_error(std::move(self.receiver),
- std::error_code(errorCode,std::system_category()));
- }
- }
- }
- }
-
- structcancel_cb{
- recv_op&op;
-
- voidoperator()()noexcept{
- CancelIoEx((HANDLE)op.sock,(OVERLAPPED*)(WSAOVERLAPPED*)&op);
- }
- };
-
- staticvoidon_complete(operation_base*op,DWORDbytesTransferred,interrorCode)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()));
- }
- }
- }
-
- Receiverreceiver;
- SOCKETsock;
- WSABUFbuffer;
- std::optional<typenamestop_callback_type_t<Receiver>
- ::templatecallback_type<cancel_cb>>stopCallback;
- std::atomic<bool>ready{false};
-};
-
-structrecv_sender{
- SOCKETsock;
- void*data;
- size_tlen;
-
- template<typenameReceiver>
- friendrecv_op<Receiver>tag_invoke(std::tag_t<std::execution::connect>
- constrecv_sender&s,
- Receiverr){
- returnrecv_op<Receiver>{s.sock,s.data,s.len,std::move(r)};
- }
-};
-
-recv_senderasync_recv(SOCKETs,void*data,size_tlen){
- returnrecv_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.
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.
In this code, NN::async_read_some and NN::async_write_some are asynchronous socket-based networking APIs that return senders. EX::repeat_effect_until, EX::let_value, and EX::then 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 repeat_effect_until 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.
This code builds a then 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 (set_error and set_stopped), as well as all receiver queries,
-are passed through unchanged.
-
In detail, it does the following:
-
-
-
Defines a receiver in terms of execution::receiver_adaptor that aggregates
-another receiver and an invocable that:
-
-
-
Defines a constrained tag_invoke overload for transforming the value
-channel.
-
-
Defines another constrained overload of tag_invoke that passes all other
-customizations through unchanged.
-
-
The tag_invoke overloads are actually implemented by execution::receiver_adaptor; they dispatch either to named members, as
-shown above with _then_receiver::set_value, or to the adapted receiver.
-
-
Defines a sender that aggregates another sender and the invocable, which defines a tag_invoke customization for std::execution::connect that wraps the incoming receiver in the receiver from (1) and passes it and the incoming sender to std::execution::connect, returning the result. It also defines a tag_invoke customization of get_completion_signatures that declares the sender’s completion signatures when executed within a particular environment.
-
-
1.5.2. retry
-
usingnamespacestd;
-namespaceexec=execution;
-
-template<classFrom,classTo>
-using_decays_to=same_as<decay_t<From>,To>;
-
-// _conv needed so we can emplace construct non-movable types into
-// a std::optional.
-template<invocableF>
- requiresis_nothrow_move_constructible_v<F>
-struct_conv{
- Ff_;
- explicit_conv(Ff)noexcept:f_((F&&)f){}
- operatorinvoke_result_t<F>()&&{
- return((F&&)f_)();
- }
-};
-
-template<classS,classR>
-struct_op;
-
-// pass through all customizations except set_error, which retries the operation.
-template<classS,classR>
-struct_retry_receiver
- :exec::receiver_adaptor<_retry_receiver<S,R>>{
- _op<S,R>*o_;
-
- R&&base()&&noexcept{return(R&&)o_->r_;}
- constR&base()const&noexcept{returno_->r_;}
-
- explicit_retry_receiver(_op<S,R>*o):o_(o){}
-
- voidset_error(auto&&)&&noexcept{
- o_->_retry();// This causes the op to be retried
- }
-};
-
-// Hold the nested operation state in an optional so we can
-// re-construct and re-start it if the operation fails.
-template<classS,classR>
-struct_op{
- Ss_;
- Rr_;
- optional<
- exec::connect_result_t<S&,_retry_receiver<S,R>>>o_;
-
- _op(Ss,Rr):s_((S&&)s),r_((R&&)r),o_{_connect()}{}
- _op(_op&&)=delete;
-
- auto_connect()noexcept{
- return_conv{[this]{
- returnexec::connect(s_,_retry_receiver<S,R>{this});
- }};
- }
- void_retry()noexcepttry{
- o_.emplace(_connect());// potentially throwing
- exec::start(*o_);
- }catch(...){
- exec::set_error((R&&)r_,std::current_exception());
- }
- friendvoidtag_invoke(exec::start_t,_op&o)noexcept{
- exec::start(*o.o_);
- }
-};
-
-template<classS>
-struct_retry_sender{
- Ss_;
- explicit_retry_sender(Ss):s_((S&&)s){}
-
- template<exec::receiverR>
- requiresexec::sender_to<S&,R>
- friend_op<S,R>tag_invoke(exec::connect_t,_retry_sender&&self,Rr){
- return{(S&&)self.s_,(R&&)r};
- }
-
- template<class>using_void=void;
- template<class...Ts>using_value=exec::set_value_t(Ts...);
-
- // Declare the signatures with which this sender can complete
- template<classEnv>
- friendautotag_invoke(exec::get_completion_signatures_t,const_retry_sender&,Env)
- ->exec::make_completion_signatures<
- constS&,Env,
- exec::completion_signatures<exec::set_error_t(std::exception_ptr)>,
- _value,_void>;
-};
-
-template<exec::senderS>
-exec::senderautoretry(Ss){
- return_retry_sender{(S&&)s};
-}
-
-
The retry algorithm takes a multi-shot sender and causes it to repeat on error, passing
-through values and stopped signals. Each time the input sender is restarted, a new receiver
-is connected and the resulting operation state is stored in an optional, which allows us
-to reinitialize it multiple times.
-
This example does the following:
-
-
-
Defines a _conv utility that takes advantage of C++17’s guaranteed copy elision to
-emplace a non-movable type in a std::optional.
-
-
Defines a _retry_receiver 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 set_error, which causes a _retry() function to be called instead.
-
-
Defines an operation state that aggregates the input sender and receiver, and declares
-storage for the nested operation state in an optional. Constructing the operation
-state constructs a _retry_receiver with a pointer to the (under construction) operation
-state and uses it to connect to the aggregated sender.
-
-
Starting the operation state dispatches to start on the inner operation state.
-
-
The _retry() 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.
-
-
After reinitializing the inner operation state, _retry() calls start on it, causing
-the failed operation to be rescheduled.
-
-
Defines a _retry_sender that implements the connect customization point to return
-an operation state constructed from the passed-in sender and receiver.
-
-
_retry_sender also implements the get_completion_signatures customization point to describe the ways this sender may complete when executed in a particular execution context.
-
-
1.6. Examples: Schedulers
-
In this section we look at some schedulers of varying complexity.
The inline scheduler is a trivial scheduler that completes immediately and synchronously on
-the thread that calls std::execution::start on the operation state produced by its sender.
-In other words, start(connect(schedule(inline-scheduler),receiver)) is
-just a fancy way of saying set_value(receiver), with the exception of the fact that start wants to be passed an lvalue.
-
Although not a particularly useful scheduler, it serves to illustrate the basics of
-implementing one. The inline_scheduler:
-
-
-
Customizes execution::schedule to return an instance of the sender type _sender.
-
-
The _sender type models the sender concept and provides the metadata
-needed to describe it as a sender of no values that can send an exception_ptr as an error and that never calls set_stopped. This
-metadata is provided with the help of the execution::completion_signatures utility.
-
-
The _sender type customizes execution::connect to accept a receiver of no
-values. It returns an instance of type _op that holds the receiver by
-value.
-
-
The operation state customizes std::execution::start to call std::execution::set_value on the receiver, passing any exceptions to std::execution::set_error as an 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 std::execution::run_loop.
The single_thread_context 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 execution::run_loop 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 this_thread::sync_wait, 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. Examples: Server theme
-
In this section we look at some examples of how one would use senders to implement an HTTP server. The examples ignore the low-level details of the HTTP server and looks at how senders can be combined to achieve the goals of the project.
-
General application context:
-
-
-
server application that processes images
-
-
execution contexts:
-
-
-
1 dedicated thread for network I/O
-
-
N worker threads used for CPU-intensive work
-
-
M threads for auxiliary I/O
-
-
optional GPU context that may be used on some types of servers
-
-
-
all parts of the applications can be asynchronous
-
-
no locks shall be used in user code
-
-
1.7.1. Composability with execution::let_*
-
Example context:
-
-
-
we are looking at the flow of processing an HTTP request and sending back the response
-
-
show how one can break the (slightly complex) flow into steps with execution::let_* functions
-
-
different phases of processing HTTP requests are broken down into separate concerns
-
-
each part of the processing might use different execution contexts (details not shown in this example)
-
-
error handling is generic, regardless which component fails; we always send the right response to the clients
-
-
Goals:
-
-
-
show how one can break more complex flows into steps with let_* functions
-
-
exemplify the use of let_value, let_error, let_stopped, and just algorithms
-
-
namespaceex=std::execution;
-
-// Returns a sender that yields an http_request object for an incoming request
-ex::senderautoschedule_request_start(read_requests_ctxctx){...}
-// Sends a response back to the client; yields a void signal on success
-ex::senderautosend_response(consthttp_response&resp){...}
-// Validate that the HTTP request is well-formed; forwards the request on success
-ex::senderautovalidate_request(consthttp_request&req){...}
-
-// Handle the request; main application logic
-ex::senderautohandle_request(consthttp_request&req){
- //...
- returnex::just(http_response{200,result_body});
-}
-
-// Transforms server errors into responses to be sent to the client
-ex::senderautoerror_to_response(std::exception_ptrerr){
- try{
- std::rethrow_exception(err);
- }catch(conststd::invalid_argument&e){
- returnex::just(http_response{404,e.what()});
- }catch(conststd::exception&e){
- returnex::just(http_response{500,e.what()});
- }catch(...){
- returnex::just(http_response{500,"Unknown server error"});
- }
-}
-// Transforms cancellation of the server into responses to be sent to the client
-ex::senderautostopped_to_response(){
- returnex::just(http_response{503,"Service temporarily unavailable"});
-}
-//...
-// The whole flow for transforming incoming requests into responses
-ex::senderautosnd=
- // get a sender when a new request comes
- schedule_request_start(the_read_requests_ctx)
- // make sure the request is valid; throw if not
- |ex::let_value(validate_request)
- // process the request in a function that may be using a different execution context
- |ex::let_value(handle_request)
- // If there are errors transform them into proper responses
- |ex::let_error(error_to_response)
- // If the flow is cancelled, send back a proper response
- |ex::let_stopped(stopped_to_response)
- // write the result back to the client
- |ex::let_value(send_response)
- // done
- ;
-// execute the whole flow asynchronously
-ex::start_detached(std::move(snd));
-
- The example shows how one can separate out the concerns for interpreting requests, validating requests, running the main logic for handling the request, generating error responses, handling cancellation and sending the response back to the client.
-They are all different phases in the application, and can be joined together with the let_* functions.
-
All our functions return execution::sender objects, so that they can all generate success, failure and cancellation paths.
-For example, regardless where an error is generated (reading request, validating request or handling the response), we would have one common block to handle the error, and following error flows is easy.
-
Also, because of using execution::sender objects at any step, we might expect any of these steps to be completely asynchronous; the overall flow doesn’t care.
-Regardless of the execution context in which the steps, or part of the steps are executed in, the flow is still the same.
-
1.7.2. Moving between execution contexts with execution::on and execution::transfer
-
Example context:
-
-
-
reading data from the socket before processing the request
-
-
reading of the data is done on the I/O context
-
-
no processing of the data needs to be done on the I/O context
-
-
Goals:
-
-
-
show how one can change the execution context
-
-
exemplify the use of on and transfer algorithms
-
-
namespaceex=std::execution;
-
-size_tlegacy_read_from_socket(intsock,char*buffer,size_tbuffer_len){}
-voidprocess_read_data(constchar*read_data,size_tread_len){}
-//...
-
-// A sender that just calls the legacy read function
-autosnd_read=ex::just(sock,buf,buf_len)|ex::then(legacy_read_from_socket);
-// The entire flow
-autosnd=
- // start by reading data on the I/O thread
- ex::on(io_sched,std::move(snd_read))
- // do the processing on the worker threads pool
- |ex::transfer(work_sched)
- // process the incoming data (on worker threads)
- |ex::then([buf](intread_len){process_read_data(buf,read_len);})
- // done
- ;
-// execute the whole flow asynchronously
-ex::start_detached(std::move(snd));
-
-
The example assume that we need to wrap some legacy code of reading sockets, and handle execution context switching.
-(This style of reading from socket may not be the most efficient one, but it’s working for our purposes.)
-For performance reasons, the reading from the socket needs to be done on the I/O thread, and all the processing needs to happen on a work-specific execution context (i.e., thread pool).
-
Calling execution::on will ensure that the given sender will be started on the given scheduler.
-In our example, snd_read is going to be started on the I/O scheduler.
-This sender will just call the legacy code.
-
The completion signal will be issued in the I/O execution context, so we have to move it to the work thread pool.
-This is achieved with the help of the execution::transfer algorithm.
-The rest of the processing (in our case, the last call to then) will happen in the work thread pool.
-
The reader should notice the difference between execution::on and execution::transfer.
-The execution::on algorithm will ensure that the given sender will start in the specified context, and doesn’t care where the completion signal for that sender is sent.
-The execution::transfer algorithm will not care where the given sender is going to be started, but will ensure that the completion signal of will be transferred to the given context.
-
1.8. 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.9. Design changes from P0443
-
-
-
The executor concept has been removed and all of its proposed functionality
-is now based on schedulers and senders, as per SG1 direction.
-
-
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.
This paper extends the sender traits/typed sender design to support typed
-senders whose value/error types depend on type information provided late via
-the receiver.
-
-
Support for untyped senders is dropped; the typed_sender concept is renamed sender; sender_traits is replaced with completion_signatures_of_t.
-
-
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.
-
-
Some additional utilities are added:
-
-
-
run_loop: An execution context that provides a multi-producer,
-single-consumer, first-in-first-out work queue.
-
-
receiver_adaptor: A utility for algorithm authors for defining one
-receiver type in terms of another.
-
-
completion_signatures and make_completion_signatures:
-Utilities for describing the ways in which a sender can complete in a
-declarative syntax.
-
-
-
1.10. 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.10.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.10.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.10.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.11. Field experience
-
1.11.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 (with slight variations -- the design of P2300 has evolved due to LEWG feedback), many of this paper’s algorithms (some under different names), and much more besides.
-
Libunifex has several concrete schedulers in addition to the run_loop suggested here (where it is called manual_event_loop). 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.11.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 let_value are really nice additions.
-
-
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 f2 in the continuation will always be ready here f1.then([](future<T>f2){...}) is enough of a reason to separate the channels. All the other obvious reasons apply as well of course.
-
-
For futures we have a thing called hpx::dataflow which is an optimized version of when_all(...).then(...) which avoids intermediate allocations. With the sender/receiver when_all(...)|then(...) we get that "for free".
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 execution::start_detached, but otherwise implements the concepts, customization points, traits, queries, coroutine integration, sender factories, pipe support, execution::receiver_adaptor, and execution::run_loop.
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.11.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:
Fix specification of get_completion_scheduler on the transfer, schedule_from and transfer_when_all algorithms; the completion scheduler cannot be guaranteed
-for set_error.
-
-
The value of sends_stopped for the default sender traits of types that are
-generally awaitable was changed from false to true to acknowledge the
-fact that some coroutine types are generally awaitable and may implement the unhandled_stopped() protocol in their promise types.
-
-
Fix the incorrect use of inline namespaces in the <execution> header.
-
-
Shorten the stable names for the sections.
-
-
sync_wait now handles std::error_code specially by throwing a std::system_error on failure.
-
-
Fix how ADL isolation from class template arguments is specified so it
-doesn’t constrain implmentations.
-
-
Properly expose the tag types in the header <execution> synopsis.
-
-
Enhancements:
-
-
-
Support for "dependently-typed" senders, where the completion signatures -- and
-thus the sender metadata -- depend on the type of the receiver connected
-to it. See the section dependently-typed
-senders below for more information.
-
-
Add a read(query) sender factory for issuing a query
-against a receiver and sending the result through the value channel. (This is
-a useful instance of a dependently-typed sender.)
-
-
Add completion_signatures utility for declaratively defining a typed
-sender’s metadata and a make_completion_signatures utility for adapting
-another sender’s completions in helpful ways.
-
-
Add make_completion_signatures utility for specifying a sender’s completion
-signatures by adapting those of another sender.
-
-
Drop support for untyped senders and rename typed_sender to sender.
-
-
set_done is renamed to set_stopped. All occurances of "done" in
-indentifiers replaced with "stopped"
-
-
Add customization points for controlling the forwarding of scheduler,
-sender, receiver, and environment queries through layers of adaptors;
-specify the behavior of the standard adaptors in terms of the new
-customization points.
-
-
Add get_delegatee_scheduler query to forward a scheduler that can be used
-by algorithms or by the scheduler to delegate work and forward progress.
-
-
Add schedule_result_t alias template.
-
-
More precisely specify the sender algorithms, including precisely what their
-completion signatures are.
-
-
stopped_as_error respecified as a customization point object.
-
-
tag_invoke respecified to improve diagnostics.
-
-
2.1.1. Dependently-typed senders
-
Background:
-
In the sender/receiver model, as with coroutines, contextual information about
-the current execution is most naturally propagated from the consumer to the
-producer. In coroutines, that means information like stop tokens, allocators and
-schedulers are propagated from the calling coroutine to the callee. In
-sender/receiver, that means that that contextual information is associated with
-the receiver and is queried by the sender and/or operation state after the
-sender and the receiver are connect-ed.
-
Problem:
-
The implication of the above is that the sender alone does not have all the
-information about the async computation it will ultimately initiate; some of
-that information is provided late via the receiver. However, the sender_traits mechanism, by which an algorithm can introspect the value and error types the
-sender will propagate, only accepts a sender parameter. It does not take into
-consideration the type information that will come in late via the receiver. The
-effect of this is that some senders cannot be typed senders when they
-otherwise could be.
-
Example:
-
To get concrete, consider the case of the "get_scheduler()" sender: when connect-ed and start-ed, it queries the receiver for its associated
-scheduler and passes it back to the receiver through the value channel. That
-sender’s "value type" is the type of the receiver’s scheduler. What then
-should sender_traits<get_scheduler_sender>::value_types report for the get_scheduler()'s value type? It can’t answer because it doesn’t know.
-
This causes knock-on problems since some important algorithms require a typed
-sender, such as sync_wait. To illustrate the problem, consider the following
-code:
-
namespaceex=std::execution;
-
-ex::senderautotask=
- ex::let_value(
- ex::get_scheduler(),// Fetches scheduler from receiver.
- [](autocurrent_sched){
- // Lauch some nested work on the current scheduler:
- returnex::on(current_sched,nestedwork...);
- });
-
-std::this_thread::sync_wait(std::move(task));
-
-
The code above is attempting to schedule some work onto the sync_wait's run_loop execution context. But let_value only returns a typed sender when
-the input sender is typed. As we explained above, get_scheduler() is not
-typed, so task is likewise not typed. Since task isn’t typed, it cannot be
-passed to sync_wait which is expecting a typed sender. The above code would
-fail to compile.
-
Solution:
-
The solution is conceptually quite simple: extend the sender_traits mechanism
-to optionally accept a receiver in addition to the sender. The algorithms can
-use sender_traits<Sender,Receiver> to inspect the
-async operation’s completion signals. The typed_sender concept would also need
-to take an optional receiver parameter. This is the simplest change, and it
-would solve the immediate problem.
-
Design:
-
Using the receiver type to compute the sender traits turns out to have pitfalls
-in practice. Many receivers make use of that type information in their
-implementation. It is very easy to create cycles in the type system, leading to
-inscrutible errors. The design pursued in R4 is to give receivers an associated environment object -- a bag of key/value pairs -- and to move the contextual
-information (schedulers, etc) out of the receiver and into the environment. The sender_traits template and the typed_sender concept, rather than taking a
-receiver, take an environment. This is a much more robust design.
-
A further refinement of this design would be to separate the receiver and the
-environment entirely, passing then as separate arguments along with the sender to connect. This paper does not propose that change.
-
Impact:
-
This change, apart from increasing the expressive power of the sender/receiver abstraction, has the following impact:
-
-
-
Typed senders become moderately more challenging to write. (The new completion_signatures and make_completion_signatures utilities are added
-to ease this extra burden.)
-
-
Sender adaptor algorithms that previously constrained their sender arguments
-to satisfy the typed_sender concept can no longer do so as the receiver is
-not available yet. This can result in type-checking that is done later, when connect is ultimately called on the resulting sender adaptor.
-
-
Operation states that own receivers that add to or change the environment
-are typically larger by one pointer. It comes with the benefit of far fewer
-indirections to evaluate queries.
-
-
"Has it been implemented?"
-
Yes, the reference implementation, which can be found at
-https://github.com/brycelelbach/wg21_p2300_std_execution, has implemented this
-design as well as some dependently-typed senders to confirm that it works.
-
Implementation experience
-
Although this change has not yet been made in libunifex, the most widely adopted sender/receiver implementation, a similar design can be found in Folly’s coroutine support library. In Folly.Coro, it is possible to await a special awaitable to obtain the current coroutine’s associated scheduler (called an executor in Folly).
-
For instance, the following Folly code grabs the current executor, schedules a task for execution on that executor, and starts the resulting (scheduled) task by enqueueing it for execution.
-
// From Facebook’s Folly open source library:
-template<classT>
-folly::coro::Task<void>CancellableAsyncScope::co_schedule(folly::coro::Task<T>&&task){
- this->add(std::move(task).scheduleOn(co_awaitco_current_executor));
- co_return;
-}
-
-
Facebook relies heavily on this pattern in its coroutine code. But as described
-above, this pattern doesn’t work with R3 of std::execution because of the lack
-of dependently-typed schedulers. The change to sender_traits in R4 rectifies that.
-
Why now?
-
The authors are loathe to make any changes to the design, however small, at this
-stage of the C++23 release cycle. But we feel that, for a relatively minor
-design change -- adding an extra template parameter to sender_traits and typed_sender -- the returns are large enough to justify the change. And there
-is no better time to make this change than as early as possible.
-
One might wonder why this missing feature not been added to sender/receiver
-before now. The designers of sender/receiver have long been aware of the need.
-What was missing was a clean, robust, and simple design for the change, which we
-now have.
-
Drive-by:
-
We took the opportunity to make an additional drive-by change: Rather than
-providing the sender traits via a class template for users to specialize, we
-changed it into a sender query: get_completion_signatures(sender,env). That function’s return type is used as the sender’s traits.
-The authors feel this leads to a more uniform design and gives sender authors a
-straightforward way to make the value/error types dependent on the cv- and
-ref-qualification of the sender if need be.
-
Details:
-
Below are the salient parts of the new support for dependently-typed senders in
-R4:
-
-
-
Receiver queries have been moved from the receiver into a separate environment
-object.
-
-
Receivers have an associated environment. The new get_env CPO retrieves a
-receiver’s environment. If a receiver doesn’t implement get_env, it returns
-an unspecified "empty" environment -- an empty struct.
-
-
sender_traits now takes an optional Env parameter that is used to
-determine the error/value types.
-
-
The primary sender_traits template is replaced with a completion_signatures_of_t alias implemented in terms of a new get_completion_signatures CPO that dispatches
-with tag_invoke. get_completion_signatures takes a sender and an optional
-environment. A sender can customize this to specify its value/error types.
-
-
Support for untyped senders is dropped. The typed_sender concept has been
-renamed to sender and now takes an optional environment.
-
-
The environment argument to the sender concept and the get_completion_signatures CPO defaults to no_env. All environment queries fail (are ill-formed) when
-passed an instance of no_env.
-
-
A type S is required to satisfy sender<S> to be
-considered a sender. If it doesn’t know what types it will complete with
-independent of an environment, it returns an instance of the placeholder
-traits dependent_completion_signatures.
-
-
If a sender satisfies both sender<S> and sender<S,Env>, then the completion signatures
-for the two cannot be different in any way. It is possible for an
-implementation to enforce this statically, but not required.
-
-
All of the algorithms and examples have been updated to work with
-dependently-typed senders.
-
-
2.2. R3
-
The changes since R2 are as follows:
-
Fixes:
-
-
-
Fix specification of the on algorithm to clarify lifetimes of
-intermediate operation states and properly scope the get_scheduler query.
-
-
Fix a memory safety bug in the implementation of connect-awaitable.
-
-
Fix recursive definition of the scheduler concept.
-
-
Enhancements:
-
-
-
Add run_loop execution context.
-
-
Add receiver_adaptor utility to simplify writing receivers.
-
-
Require a scheduler’s sender to model sender_of and provide a completion scheduler.
-
-
Specify the cancellation scope of the when_all algorithm.
-
-
Make as_awaitable a customization point.
-
-
Change connect's handling of awaitables to consider those types that are awaitable owing to customization of as_awaitable.
-
-
Add value_types_of_t and error_types_of_t alias templates; rename stop_token_type_t to stop_token_of_t.
-
-
Add a design rationale for the removal of the possibly eager algorithms.
-
-
Expand the section on field experience.
-
-
2.3. R2
-
The changes since R1 are as follows:
-
-
-
Remove the eagerly executing sender algorithms.
-
-
Extend the execution::connect customization point and the sender_traits<> template to recognize awaitables as typed_senders.
-
-
Add utilities as_awaitable() and with_awaitable_senders<> so a coroutine type can trivially make senders awaitable with a coroutine.
-
-
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 bulk algorithm.
-
-
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 execution::just_error and execution::just_stopped.
-
-
Add sender adaptors execution::stopped_as_optional and execution::stopped_as_error.
-
-
Document more production uses of sender/receiver at scale.
-
-
Various fixes of typos and bugs.
-
-
2.4. 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 unschedule adaptor.
-
-
Various fixes of typos and bugs.
-
-
2.5. R0
-
Initial revision.
-
3. Design - introduction
-
The following three 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]: std::execution; however, for brevity, the std:: part of this name is omitted. When you see execution::foo, treat that as std::execution::foo.
-
-
Universal references and explicit calls to std::move/std::forward are omitted in code samples and signatures for simplicity; assume universal references and perfect forwarding unless stated otherwise.
-
-
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 std::invocable 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 std::invocable 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, schedule, 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::schedulerautosch=thread_pool.scheduler();
-execution::senderautosnd=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 execution::schedule API.
-
Future papers will propose additional scheduler concepts that extend scheduler to add other capabilities. For example:
-
-
-
A time_scheduler concept that extends scheduler to support time-based scheduling.
-Such a concept might provide access to schedule_after(sched,duration), schedule_at(sched,time_point) and now(sched) APIs.
-
-
Concepts that extend scheduler to support opening, reading and writing files asynchronously.
-
-
Concepts that extend scheduler to support connecting, sending data and receiving data over the network asynchronously.
-
-
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.
execution::schedulerautosch=thread_pool.scheduler();
-execution::senderautosnd=execution::schedule(sch);
-execution::senderautocont=execution::then(snd,[]{
- std::fstreamfile{"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 execution::connect) senders and return a sender.
-
-
sender consumers, algorithms that take (and potentially execution::connect) senders and do not return a sender.
-
-
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 std::thread (which they aren’t).
-
-
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 on 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 stopped) 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
-
get_completion_scheduler is a query that retrieves the completion scheduler for a specific completion signal from a sender.
-Calling get_completion_scheduler 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::schedulerautocpu_sched=new_thread_scheduler{};
-execution::schedulerautogpu_sched=cuda::scheduler();
-
-execution::senderautosnd0=execution::schedule(cpu_sched);
-execution::schedulerautocompletion_sch0=
- execution::get_completion_scheduler<execution::set_value_t>(snd0);
-// completion_sch0 is equivalent to cpu_sched
-
-execution::senderautosnd1=execution::then(snd0,[]{
- std::cout<<"I am running on cpu_sched!\n";
-});
-execution::schedulerautocompletion_sch1=
- execution::get_completion_scheduler<execution::set_value_t>(snd1);
-// completion_sch1 is equivalent to cpu_sched
-
-execution::senderautosnd2=execution::transfer(snd1,gpu_sched);
-execution::senderautosnd3=execution::then(snd2,[]{
- std::cout<<"I am running on gpu_sched!\n";
-});
-execution::schedulerautocompletion_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 execution::schedule, 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 execution::transfer sender adaptor performs a transition from one execution context to another:
-
execution::schedulerautosch1=...;
-execution::schedulerautosch2=...;
-
-execution::senderautosnd1=execution::schedule(sch1);
-execution::senderautothen1=execution::then(snd1,[]{
- std::cout<<"I am running on sch1!\n";
-});
-
-execution::senderautosnd2=execution::transfer(then1,sch2);
-execution::senderautothen2=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 std::unique_ptr that it will be transferring ownership of to the
-operation-state returned by a call to execution::connect so that the operation has access to
-this resource. In such a sender, calling execution::connect 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 execution::connect only has overloads for an rvalue-qualified sender. Callers must pass the sender
-as an rvalue to the call to execution::connect, 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 execution::connect to accept an lvalue reference to the
-sender. Callers can indicate that they want the sender to remain valid after the call to execution::connect by passing an lvalue reference to the sender to call these overloads. Multi-shot senders should also define
-overloads of execution::connect 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 execution::connect.
-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 execution::connect. 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 execution::connect on the input sender, such as in this_thread::sync_wait or execution::start_detached.
-
Some multi-use sender algorithms may require that an input sender be copy-constructible but will only call execution::connect 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 execution::connect 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 split sender adaptor facilitates connecting to a sender multiple times, regardless of whether it is single-shot or multi-shot:
-
autosome_algorithm(execution::senderauto&&input){
- execution::senderautomulti_shot=split(input);
- // "multi_shot" is guaranteed to be multi-shot,
- // regardless of whether "input" was multi-shot or not
-
- returnwhen_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.
-
when_all 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. when_all returns a sender that also does not have an associated scheduler.
-
transfer_when_all 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 when_all. You can think of it as a composition of transfer(when_all(inputs...),scheduler), 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 execution::connect() before calling execution::start(), 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 when_all(ops...) algorithm should cancel other operations as soon as one operation fails
-
-
a first_successful(ops...) algorithm should cancel the other operations as soon as one operation completes successfuly
-
-
a generic timeout(src,duration) algorithm needs to be able to cancel the src operation after the timeout duration has elapsed.
-
-
a stop_when(src,trigger) algorithm should cancel src if trigger completes first and cancel trigger if src completes first
-
-
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:
In this example, if we take the operation returned by query_server_b(query), this operation will
-receive a stop-request when any of the following happens:
-
-
-
first_successful algorithm will send a stop-request if query_server_a(query) completes successfully
-
-
when_all algorithm will send a stop-request if the load_file("some_file.jpg") operation completes with an error or stopped result.
-
-
timeout algorithm will send a stop-request if the operation does not complete within 5 seconds.
-
-
stop_when algorithm will send a stop-request if the user clicks on the "Cancel" button in the user-interface.
-
-
The parent operation consuming the composed_cancellation_example() sends a stop-request
-
-
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 std::stop_token-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 std::stoppable_token and std::stoppable_token_for concepts that generalise the interface of std::stop_token type to allow other types with different implementation strategies.
-
-
Add std::unstoppable_token concept for detecting whether a stoppable_token can never receive a stop-request.
-
-
Add std::in_place_stop_token, std::in_place_stop_source and std::in_place_stop_callback<CB> types that provide a more efficient implementation of a stop-token for use in structured concurrency situations.
-
-
Add std::never_stop_token for use in places where you never want to issue a stop-request
-
-
Add std::execution::get_stop_token() CPO for querying the stop-token to use for an operation from its receiver’s execution environment.
-
-
Add std::execution::stop_token_of_t<T> for querying the type of a stop-token returned from 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 execution::get_stop_token() CPO.
-This CPO takes a single parameter, which is the execution environment of the receiver passed to execution::connect, and returns a std::stoppable_token that the operation can use to check for stop-requests for that operation.
-
As the caller of execution::connect typically has control over the receiver
-type it passes, it is able to customise the execution::get_env() CPO for that
-receiver to return an execution environment that hooks the execution::get_stop_token() CPO to return a stop-token that the receiver 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’s execution environment does not customise the execution::get_stop_token() CPO then invoking the CPO on that receiver’s
-environment will invoke the default implementation which returns std::never_stop_token. This is a special stoppable_token type that is
-statically known to always return false from the stop_possible() 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 execution::get_stop_token(), 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 execution::start() 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 execution::start() 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 execution::start() implementation.
-
An implementation of execution::start() 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 execution::start 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 execution::start() call (which is still executing).
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 ensure_started 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 execution::connect?" or similarly, "What happens if a call
-to execution::connect is made, but the returned operation state is destroyed
-before execution::start 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 when_all(eager_op_1(),eager_op_2()). Imagine eager_op_1() starts an
-asynchronous operation successfully, but then eager_op_2() throws. For
-lazy senders, that failure happens in the context of the when_all 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 before when_all has been called.
-
It then becomes the responsibility, not of the algorithm, but of the end
-user to handle the exception and ensure that eager_op_1() is joined before
-allowing the exception to propagate. If they fail to do that, they incur
-undefined behavior.
-
-
Detach from the computation - let the operation continue in the background -
-like an implicit call to std::thread::detach(). 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::shared_ptr, to ensure resources are kept alive until the operations
-complete, and can make clean shutdown nigh impossible.
-
-
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 std::terminate if an eager sender is destructed
-without joining it. This is the approach that std::thread destructor
-takes.
-
-
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 std::jthread uses for its
-destructor. It reduces the risk of deadlock but does not eliminate it.
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 execution::start 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 sequence that puts async operations in sequence by starting an operation only
-after the preceding one has completed. In an expression like sequence(a(),then(src,[]{b();}),c()), one my reasonably assume that a(), b() and c() 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.
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 execution::get_stop_token is an std::unstoppable_token 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, get_forward_progress_guarantee, which returns one of the enumerators of a new enum type, forward_progress_guarantee. Each enumerator of forward_progress_guarantee 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 operator|.
-This mechanism is similar to the operator| 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.
-
a|b will pass the sender a as the first argument to the pipeable sender adaptor b. Pipeable sender adaptors support partial application of the parameters after the first. For example, all of the following are equivalent:
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:
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.
-
-
-
execution::when_all and execution::when_all_with_variant: 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::on: 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 to 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 R that is a range of senders, consider the following in a coroutine that returns an async generator type:
This transforms each element of the asynchronous sequence R with the function fn on demand, as the data arrives. The result is a new asynchronous sequence of the transformed values.
-
Now imagine that R is the simple expression views::iota(0)|views::transform(execution::just). 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 [fn(0), fn(1), fn(2), ...].
-
Far more interesting would be if R 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:
- returnread_socket_async(socket,span{buff})
- // For success (partial and full), specify the next action:
- |execution::let_value([](error_codeerr,size_tbytes_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(autosnd:read_socket_async(socket,span{buff})){
- try{
- if(optional<size_t>bytes_read=
- co_awaitexecution::stopped_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 sender 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 await_transform trickery from a promise type to make them awaitable.)
-
For an example, imagine a coroutine type called task<T> that knows nothing
-about senders. It doesn’t implement any of the sender customization points.
-Despite that fact, and despite the fact that the this_thread::sync_wait algorithm is constrained with the sender concept, the following would compile
-and do what the user wants:
-
task<int>doSomeAsyncWork();
-
-intmain(){
- // OK, awaitable types satisfy the requirements for senders:
- autoo=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 execution::with_awaitable_senders 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 retry algorithm:
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 with_awaitable_senders 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 into_variant sender algorithm -- before co_await-ing that sender.
-
4.18. Cancellation of a sender can unwind a stack of coroutines
-
When looking at the sender-based retry 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 execution::set_stopped?
-
When your task type’s promise inherits from with_awaitable_senders, what happens is this: the coroutine behaves as if an uncatchable exception had been thrown from the co_await 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 with_awaitable_senders, or more generally implement a member function called unhandled_stopped, the exception unwinds the chain of coroutines as if an exception were thrown except that it bypasses catch(...) clauses.
-
In order to "catch" this uncatchable stopped exception, one of the calling coroutines in the stack would have to await a sender that maps the stopped channel into either a value or an error. That is achievable with the execution::let_stopped, execution::upon_stopped, execution::stopped_as_optional, or execution::stopped_as_error sender adaptors. For instance, we can use execution::stopped_as_optional to "catch" the stopped signal and map it into an empty optional as shown below:
-
if(autoopt=co_awaitexecution::stopped_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 connect-ing an awaitable and a receiver, the adaptation layer awaits the awaitable within a coroutine that implements unhandled_stopped in its promise type. The effect of this is that an "uncatchable" stopped exception propagates seamlessly out of awaitables, causing execution::set_stopped to be called on the receiver.
-
Obviously, unhandled_stopped is a library extension of the coroutine promise interface. Many promise types will not implement unhandled_stopped. When an uncatchable stopped exception tries to propagate through such a coroutine, it is treated as an unhandled exception and terminate is called. The solution, as described above, is to use a sender adaptor to handle the stopped exception before awaiting it. It goes without saying that any future Standard Library coroutine types ought to implement unhandled_stopped. 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 execution::par, 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.
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 policy while using scheduler to create any work to be run. The expression may be ill-formed if scheduler 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 executing_on 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 void 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.
execution::schedulerautosch1=get_system_thread_pool().scheduler();
-
-execution::senderautosnd1=execution::schedule(sch1);
-// snd1 describes the creation of a new task on the system thread pool
-
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 set_value.
Returns a sender whose value completion scheduler is the provided scheduler, which sends the provided values in the same manner as just.
-
execution::senderautovals=execution::transfer_just(
- get_system_thread_pool().scheduler(),
- 1,2,3
-);
-execution::senderautosnd=execution::then(vals,[](auto...args){
- std::print(args...);
-});
-// when snd is executed, it will print "123"
-
-
This adaptor is included as it greatly simplifies lifting values into senders.
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 set_error. 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 set_error.
-
4.20.5. execution::just_stopped
-
execution::senderautojust_stopped();
-
-
Returns a sender with no completion schedulers, which completes immediately by calling the receiver’s set_stopped.
Returns a sender that reaches into a receiver’s environment and pulls out the current value associated with the customization point denoted by Tag. It then sends the value read back to the receiver through the value channel. For instance, get_scheduler() (with no arguments) is a sender that asks the receiver for the currently suggested scheduler and passes it to the receiver’s set_value completion-signal.
-
This can be useful when scheduling nested dependent work. The following sender pulls the current schduler into the value channel and then schedules more work onto it.
-
execution::senderautotask=
- execution::get_scheduler()
- |execution::let_value([](autosched){
- returnexecution::on(sched,somenestedworkhere);
- });
-
-this_thread::sync_wait(std::move(task));// wait for it to finish
-
-
This code uses the fact that sync_wait associates a scheduler with the receiver that it connects with task. get_scheduler() reads that scheduler out of the receiver, and passes it to let_value's receiver’s set_value function, which in turn passes it to the lambda. That lambda returns a new sender that uses the scheduler to schedule some nested work onto sync_wait's scheduler.
-
4.21. User-facing sender adaptors
-
A sender adaptor is an algorithm that takes one or more senders, which it may execution::connect, as parameters, and returns a sender, whose completion is related to the sender arguments it has received.
execution::schedulerautocpu_sched=get_system_thread_pool().scheduler();
-execution::schedulerautogpu_sched=cuda::scheduler();
-
-execution::senderautocpu_task=execution::schedule(cpu_sched);
-// cpu_task describes the creation of a new task on the system thread pool
-
-execution::senderautogpu_task=execution::transfer(cpu_task,gpu_sched);
-// gpu_task describes the transition of the task graph described by cpu_task to the gpu
-
then 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.
-
then is guaranteed to not begin executing function until the returned sender is started.
-
execution::senderautoinput=get_input();
-execution::senderautosnd=execution::then(input,[](auto...args){
- std::print(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.
upon_error and upon_stopped are similar to then, but where then works with values sent by the input sender, upon_error works with errors, and upon_stopped is invoked when the "stopped" signal is sent.
let_value is very similar to then: 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 then sends exactly what that function ends up returning - let_value requires that the function return a sender, and the sender returned by let_value 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.
-
let_value is guaranteed to not begin executing function until the returned sender is started.
-
let_error and let_stopped are similar to let_value, but where let_value works with values sent by the input sender, let_error works with errors, and let_stopped is invoked when the "stopped" signal is sent.
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.
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.
Returns a sender that maps the value channel from a T to an optional<decay_t<T>>, and maps the stopped channel to a value of an empty optional<decay_t<T>>.
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 function will begin executing until the returned sender is started. Each invocation of function 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 bulk execute with the same guarantee. This allows, for instance, a scheduler to execute all invocations of the function in parallel.
-
The bulk operation is intended to be used at the point where the number of agents to be created is known and provided to bulk via its shape 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, bulk can be combined with additional operations such as let_value to deliver dynamic shape information to the bulk 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.
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.
when_all 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 set_value 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 when_all. It completes inline on the execution context on which the last input sender completes, unless stop is requested before when_all is started, in which case it completes inline within the call to start.
-
when_all_with_variant does the same, but it adapts all the input senders using into_variant, and so it does not constrain the input arguments as when_all does.
execution::schedulerautosched=thread_pool.scheduler();
-
-execution::senderautosends_1=...;
-execution::senderautosends_abc=...;
-
-execution::senderautoboth=execution::when_all(sched,
- sends_1,
- sends_abc
-);
-
-execution::senderautofinal=execution::then(both,[](auto...args){
- std::cout<<std::format("the two args: {}, {}",args...);
-});
-// when final executes, it will print "the two args: 1, abc"
-
Once ensure_started returns, it is known that the provided sender has been connected and start 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 execution::connect() is called, or if execution::connect() is called but the
-returned operation-state is destroyed before execution::start() 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 std::shared_ptr 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 execution::connect, as parameters, and does not return a sender.
this_thread::sync_wait is a sender consumer that submits the work described by the provided sender for execution, similarly to ensure_started, except that it blocks the current std::thread or thread of main 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, sync_wait 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, sync_wait throws that error as an exception, or rethrows the original exception if the error is of type std::exception_ptr.
-
If the provided sender sends the "stopped" signal instead of values, sync_wait returns an empty optional.
-
For an explanation of the requires clause, see § 5.8 All senders are typed. That clause also explains another sender consumer, built on top of sync_wait: sync_wait_with_variant.
-
Note: This function is specified inside std::this_thread, and not inside execution. This is because sync_wait 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 std::this_thread, 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 std::this_fiber::sync_wait would be provided. We also expect that runtimes with execution agents that use different synchronization mechanisms than std::thread's will provide their own flavors of sync_wait 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.
A receiver is a callback that supports more than one channel. In fact, it supports three of them:
-
-
-
set_value, which is the moral equivalent of an operator() or a function call, which signals successful completion of the operation its execution depends on;
-
-
set_error, 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; and
-
-
set_stopped, which signals that the operation completed without succeeding (set_value) and without failing (set_error). 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.
-
-
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 set_value failed with an exception, either set_error or set_stopped 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 std::promise, which provides the first two signals as set_value and set_error, 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.
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: start, 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 execution::ensure_started and this_thread::sync_wait, 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.
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 connect will be fulfilled.
-
execution::senderautosnd=someinputsender;
-execution::receiverautorcv=somereceiver;
-execution::operation_stateautostate=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 then, 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 execution::then(sender,invocable) to be equivalent to:
-
-
-
sender.then(invocable), if that expression is well formed; otherwise
-
-
then(sender,invocable), performed in a context where this call always performs ADL, if that expression is well formed; otherwise
-
-
a default implementation of then, which returns a sender adaptor, and then define the exact semantics of said adaptor.
-
-
However, this definition is problematic. Imagine another sender adaptor, bulk, 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 bulk to have specialized behavior, which invokes a kernel of more than one thread (with its size defined by the call to bulk); therefore, we would like to customize bulk for CUDA senders to achieve this. However, there’s no reason for CUDA kernels to
-necessarily customize the then sender adaptor, as the generic implementation is perfectly sufficient. This creates a problem, though; consider the following snippet:
-
execution::schedulerautocuda_sch=cuda_scheduler{};
-
-execution::senderautoinitial=execution::schedule(cuda_sch);
-// the type of initial is a type defined by the cuda_scheduler
-// let’s call it cuda::schedule_sender<>
-
-execution::senderautonext=execution::then(cuda_sch,[]{return1;});
-// 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::senderautokernel_sender=execution::bulk(next,shape,[](inti){...});
-
-
How can we specialize the bulk sender adaptor for our wrapped schedule_sender? 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):
However, if the input sender is not just a then_sender_adaptor like in the example above, but another sender that overrides bulk 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 execution::<sender-algorithm>(sender,args...), for any given sender algorithm that accepts a sender as its first argument, should be
-equivalent to:
-
-
-
tag_invoke(<sender-algorithm>,get_completion_scheduler<Signal>(sender),sender,args...), if that expression is well-formed; otherwise
-
-
tag_invoke(<sender-algorithm>,sender,args...), if that expression is well-formed; otherwise
-
-
a default implementation, if there exists a default implementation of the given sender algorithm.
-
-
where Signal is one of set_value, set_error, or set_stopped; for most sender algorithms, the completion scheduler for set_value would be used, but for some (like upon_error or let_stopped), one of the others would be used.
-
For sender algorithms which accept concepts other than sender as their first argument, we propose that the customization scheme remains as it has been in [P0443R14] so far, except it should also use tag_invoke.
-
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 execution::start 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 execution::transfer 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 transfer 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 transfer.
-
To allow for such customization from both ends, we propose the inclusion of a secondary transitioning sender adaptor, called schedule_from. This adaptor is a form of schedule, 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 transfer, to ensure that both schedulers have a say in how the transitions are made. Any scheduler that specializes transfer(snd,sch) shall ensure that the
-return value of their customization is equivalent to schedule_from(sch,snd2), where snd2 is a successor of snd that sends values equivalent to those sent by snd.
-
The default implementation of transfer(snd,sched) is schedule_from(sched,snd).
-
5.8. All senders are typed
-
All senders must 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 execution::then is hard.
-
The mechanism for this advertisement is similar to the one in [P0443R14]; the
-way to query the types is through completion_signatures_of_t<S,[Env]>::value_types<tuple_like,variant_like>.
-
completion_signatures_of_t::value_types 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 when_all). 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 std::optional to handle the set_stopped signal. However, this assumes that those values can be represented as a tuple,
-like here:
-
execution::senderautosends_1=...;
-execution::senderautosends_2=...;
-execution::senderautosends_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 value_types of a sender which sends Types... to be as follows:
If senders could only ever send one specific set of values, this would probably need to be the required form of value_types 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) set_value 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 value_types, one that represents a variant-like type. If we permit such senders, we would almost certainly need to require that the canonical form of value_types 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 Types1..., Types2..., ..., TypesN... to be as follows:
This, however, introduces a couple of complications:
-
-
-
A just(1) sender would also need to follow this structure, so the correct type for storing the value sent by it would be std::variant<std::tuple<int>> 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 where value_types 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.
-
-
As a consequence of (1): because sync_wait needs to store the above type, it can no longer return just a std::tuple<int> for just(1); it has to return std::variant<std::tuple<int>>. 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 of sync_wait must be the same across all sender types.
-
-
One possible solution to (2) above is to place a requirement on sync_wait that it can only accept senders which send only a single set of values, therefore removing the need for std::variant to appear in its API; because of this, we propose to expose both sync_wait, which is a simple, user-friendly version of the sender consumer, but requires that value_types have only one possible variant, and sync_wait_with_variant, which accepts any sender, but returns an optional whose value type is the variant of all the
-possible tuples sent by the input sender:
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: sync_wait. We imagine that if, in the future, C++ was to
-gain fibers support, we would want to also have std::this_fiber::sync_wait, in addition to std::this_thread::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.
-
-
This paper proposes to instead use the mechanism described in [P1895R0]: tag_invoke; the wording for tag_invoke has been incorporated into the proposed specification in this paper.
-
In short, instead of using globally reserved names, tag_invoke uses the type of the customization point object itself as the mechanism to find customizations. It globally reserves only a single name - tag_invoke - which itself is used the same way that
-ranges-style customization points are used. All other customization points are defined in terms of tag_invoke. For example, the customization for std::this_thread::sync_wait(s) will call tag_invoke(std::this_thread::sync_wait,s), instead of attempting
-to invoke s.sync_wait(), and then sync_wait(s) if the member call is not valid.
-
Using tag_invoke 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<typenameTag,typename...Args>
-friendautotag_invoke(Tag&&tag,wrapper&self,Args&&...args){
- returnstd::forward<Tag>(tag)(self.subobject,std::forward<Args>(args)...);
-}
-
-// but override one of them with a specific value
-friendautotag_invoke(specific_customization_point_t,wrapper&self){
- returnself.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 tag_invoke, dispatching to manually constructed vtables containing pointers to specialized implementations for the wrapped objects. For an example of such a polymorphic wrapper, see unifex::any_unique (example).
-
-
6. Specification
-
Much of this wording follows the wording of [P0443R14].
[Editorial: Add the header <execution> to Table 23: C++ library headers [tab:headers.cpp]]
-
In subclause [conforming], after [lib.types.movedfrom], add the following new subclause with suggested stable name [lib.tmpl-heads].
-
-
- 16.4.6.17 Class template-heads
-
-
-
If a class template’s template-head is marked with "arguments are not
-associated entities"", any template arguments do not contribute to the
-associated entities ([basic.lookup.argdep]) of a function call where a
-specialization of the class template is an associated entity. In such a case,
-the class template may be implemented as an alias template referring to a
-templated class, or as a class template where the template arguments
-themselves are templated classes.
-
-
[Example:
-
template<classT>// arguments are not associated entities
-structS{};
-
-namespaceN{
- intf(auto);
- structA{};
-}
-
-intx=f(S<N::A>{});// error: N::f not a candidate
-
-
The template S specified above may be implemented as
Insert this section as a new subclause, between Searchers [func.search] and Class template hash[unord.hash].
-
-
-
-
-
The name std::tag_invoke denotes a customization point object [customization.point.object]. Given subexpressions T and A..., the expression std::tag_invoke(T,A...) is expression-equivalent [defns.expression-equivalent] to tag_invoke(T,A...) if it is a well-formed expression with overload resolution performed in a context in which unqualified lookup for tag_invoke finds only the declaration
-
voidtag_invoke();
-
-
Otherwise, std::tag_invoke(T,A...) is ill-formed.
-
-
[Note: Diagnosable ill-formed cases above result in substitution failure when std::tag_invoke(T,A...) appears in the immediate context of a template instantiation. —end note]
Insert this section as a new subclause between Header <stop_token> synopsis [thread.stoptoken.syn] and Class stop_token[stoptoken].
-
-
-
-
-
The stoppable_token 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, T::callback_type<CB>, 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. The stoppable_token_for concept checks for a stop token type compatible with a given
-callback type. The unstoppable_token concept checks for a stop token type that does not allow stopping.
Let t and u be distinct object of type T. The type T models stoppable_token only if:
-
-
-
All copies of a stoppable_token reference the same logical shared stop state and shall report values consistent with each other.
-
-
If t.stop_possible() evaluates to false then, if u, references the same logical shared stop state, u.stop_possible() shall also subsequently evaluate to false and u.stop_requested() shall also subsequently evaluate to false.
-
-
If t.stop_requested() evaluates to true then, if u, references the same logical shared stop state, u.stop_requested() shall also subsequently evaluate to true and u.stop_possible() shall also subsequently evaluate to true.
-
-
Given a callback-type, CB, and a callback-initializer argument, init, of type Initializer then constructing an instance, cb, of type T::callback_type<CB>, passing t as the first argument and init as the second argument to the constructor, shall,
-if t.stop_possible() is true, construct an instance, callback, of type CB, direct-initialized with init, and register callback with t’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.
-
-
-
If t.stop_requested() is true at the time callback is registered then callback may be invoked immediately inline inside the call to cb’s constructor.
-
-
If callback is invoked then, if u references the same shared stop state as t, an evaluation of u.stop_requested() will be true if the beginning of the invocation of callback strongly-happens-before the evaluation of u.stop_requested().
-
-
If t.stop_possible() evaluates to false then the construction of cb is not required to construct and initialize callback.
-
-
-
Construction of a T::callback_type<CB> instance shall only throw exceptions thrown by the initialization of the CB instance from the value of type Initializer.
-
-
Destruction of the T::callback_type<CB> object, cb, removes callback from the shared stop state such that callback will not be invoked after the destructor returns.
-
-
-
If callback is currently being invoked on another thread then the destructor of cb will block until the invocation of callback returns such that the return from the invocation of callback strongly-happens-before the destruction of callback.
-
-
Destruction of a callback cb shall not block on the completion of the invocation of some other callback registered with the same shared stop state.
-
-
-
-
-
-
9.1.3. Class stop_token[stoptoken]
-
9.1.3.1. General [stoptoken.general]
-
Modify the synopsis of class stop_token in section General [stoptoken.general] as follows:
Insert a new subclause, Class never_stop_token[stoptoken.never], after section Class template stop_callback[stopcallback], as a new subclause of Stop tokens [thread.stoptoken].
-
9.1.4.1. General [stoptoken.never.general]
-
-
-
The class never_stop_token provides an implementation of the unstoppable_token concept. It provides a stop token interface, but also provides static information that a stop is never possible nor requested.
9.1.5. Class in_place_stop_token[stoptoken.inplace]
-
Insert a new subclause, Class in_place_stop_token[stoptoken.inplace], after the section added above, as a new subclause of Stop tokens [thread.stoptoken].
-
9.1.5.1. General [stoptoken.inplace.general]
-
-
-
The class in_place_stop_token provides an interface for querying whether a stop request has been made (stop_requested) or can ever be made (stop_possible) using an associated in_place_stop_source object ([stopsource.inplace]).
-An in_place_stop_token can also be passed to an in_place_stop_callback ([stopcallback.inplace]) constructor to register a callback to be called when a stop request has been made from an associated in_place_stop_source.
9.1.6. Class in_place_stop_source[stopsource.inplace]
-
Insert a new subclause, Class in_place_stop_source[stopsource.inplace], after the section added above, as a new subclause of Stop tokens [thread.stoptoken].
-
9.1.6.1. General [stopsource.inplace.general]
-
-
-
The class in_place_stop_source implements the semantics of making a stop request, without the need for a dynamic allocation of a shared state.
-A stop request made on a in_place_stop_source object is visible to all associated in_place_stop_token ([stoptoken.inplace]) objects.
-Once a stop request has been made it cannot be withdrawn (a subsequent stop request has no effect).
-All uses of in_place_stop_token objects associated with a given in_place_stop_source object must happen before the invocation of the destructor of that in_place_stop_token object.
Returns: true if the stop state inside *this has not yet received a stop request; otherwise, false.
-
-
[[nodiscard]]boolstop_requested()constnoexcept;
-
-
-
-
Returns: true if the stop state inside *this has received a stop request; otherwise, false.
-
-
boolrequest_stop()noexcept;
-
-
-
-
Effects: Atomically determines whether the stop state inside *this has received a stop request, and if not, makes a stop request.
-The determination and making of the stop request are an atomic read-modify-write operation ([intro.races]).
-If the request was made, the callbacks registered by associated in_place_stop_callback objects are synchronously called.
-If an invocation of a callback exits via an exception then terminate is invoked ([except.terminate]).
-
-
Postconditions: stop_possible() is false and stop_requested() is true.
-
-
Returns: true if this call made a stop request; otherwise false.
-
-
9.1.7. Class template in_place_stop_callback[stopcallback.inplace]
-
Insert a new subclause, Class template in_place_stop_callback[stopcallback.inplace], after the section added above, as a new subclause of Stop tokens [thread.stoptoken].
Mandates: in_place_stop_callback is instantiated with an argument for the template parameter Callback that satisfies both invocable and destructible.
-
-
Preconditions: in_place_stop_callback is instantiated with an argument for the template parameter Callback that models both invocable and destructible.
-
-
Recommended practice: Implementation should use the storage of the in_place_stop_callback objects to store the state necessary for their association with an in_place_stop_source object.
-
-
9.1.7.2. Constructors and destructor [stopcallback.inplace.cons]
Constraints: Callback and C satisfy constructible_from<Callback,C>.
-
-
Preconditions: Callback and C model constructible_from<Callback,C>.
-
-
Effects: Initializes callback_ with std::forward<C>(cb).
-If st.stop_requested() is true, then std::forward<Callback>(callback_)() is evaluated in the current thread before the constructor returns.
-Otherwise, if st has an associated in_place_stop_source object, registers the callback with the stop state of the in_place_stop_source that st is associated with such that std::forward<Callback>(callback_)() is evaluated by the first call to request_stop() on an associated in_place_stop_source.
-The in_place_stop_callback object being initialized becomes associated with the in_place_stop_source object that st is associated with, if any.
-
-
Throws: Any exception thrown by the initialization of callback_.
-
-
Remarks: If evaluating std::forward<Callback>(callback_)() exits via an exception, then terminate is invoked ([except.terminate]).
-
-
~in_place_stop_callback();
-
-
-
-
Effects: Unregisters the callback from the stop state of the associated in_place_stop_source object, if any.
-The destructor does not block waiting for the execution of another callback registered by an associated stop_callback.
-If callback_ is concurrently executing on another thread, then the return from the invocation of callback_ strongly happens before ([intro.races]) callback_ is destroyed.
-If callback_ is executing on the current thread, then the destructor does not block ([defns.block]) waiting for the return from the invocation of callback_.
-
-
Remarks: A program has undefined behavior if the invocation of this function does not strongly happen before the beginning of the invocation of the destructor of the associated in_place_stop_source object, if any.
-
-
10. Execution control library [exec]
-
-
-
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.
-
-
-
Table 1: Execution control library summary [tab:execution.summary]
[Note: A large number of execution control primitives are customization point objects. For an object one might define multiple types of customization point objects, for which different rules apply.**** Table 2 shows the types of customization point objects used in the execution control library:
-
-
-
Table 2: Types of customization point objects in the execution control library [tab:execution.cpos]
-
-
-
Customization point object type
-
Purpose
-
Examples
-
-
core
-
provide core execution functionality, and connection between core components
-
connect, start, execute
-
-
completion signals
-
called by senders to announce the completion of the work (success, error, or cancellation)
-
execution::get_scheduler is used to ask an object for its associated scheduler.
-
-
The name execution::get_scheduler denotes a customization point object. For some subexpression r, if the type of r is (possibly cv-qualified) no_env, then execution::get_scheduler(r) is ill-formed. Otherwise, it is expression equivalent to:
-
-
-
tag_invoke(execution::get_scheduler,as_const(r)), if this expression is well formed.
-
-
-
Mandates: The tag_invoke expression above is not
-potentially-throwing and its type satisfies execution::scheduler.
-
-
-
Otherwise, execution::get_scheduler(r) is ill-formed.
-
-
-
execution::get_scheduler() (with no arguments) is expression-equivalent to execution::read(execution::get_scheduler).
execution::get_delegatee_scheduler is used to ask an object for a scheduler that may be used to delegate work to for the purpose of forward progress delegation.
-
-
The name execution::get_delegatee_scheduler denotes a customization point object. For some subexpression r, if the type of r is (possibly cv-qualified) no_env, then execution::get_delegatee_scheduler(r) is ill-formed. Otherwise, it is expression equivalent to:
-
-
-
tag_invoke(execution::get_delegatee_scheduler,as_const(r)), if this expression is well formed.
-
-
-
Mandates: The tag_invoke expression above is not
-potentially-throwing and its type satisfies execution::scheduler.
-
-
-
Otherwise, execution::get_delegatee_scheduler(r) is ill-formed.
-
-
-
execution::get_delegatee_scheduler() (with no arguments) is expression-equivalent to execution::read(execution::get_delegatee_scheduler).
execution::get_allocator is used to ask an object for its associated allocator.
-
-
The name execution::get_allocator denotes a customization point object. For some subexpression r, if the type of r is (possibly cv-qualified) no_env, then execution::get_allocator(r) is ill-formed. Otherwise, it is expression equivalent to:
-
-
-
tag_invoke(execution::get_allocator,as_const(r)), if this expression is well formed.
-
-
-
Mandates: The tag_invoke expression above is not
-potentially-throwing and its type satisfies Allocator.
-
-
-
Otherwise, execution::get_allocator(r) is ill-formed.
-
-
-
execution::get_allocator() (with no arguments) is expression-equivalent to execution::read(execution::get_allocator).
execution::get_stop_token is used to ask an object for an associated stop token.
-
-
The name execution::get_stop_token denotes a customization point object. For some subexpression r, if the type of r is (possibly cv-qualified) no_env, then execution::get_stop_token(r) is ill-formed. Otherwise, it is expression equivalent to:
-
-
-
tag_invoke(execution::get_stop_token,as_const(r)), if this expression is well formed.
-
-
-
Mandates: The tag_invoke expression above is not
-potentially-throwing and its type satisfies stoppable_token.
-
-
-
Otherwise, never_stop_token{}.
-
-
-
execution::get_stop_token() (with no arguments) is expression-equivalent to execution::read(execution::get_stop_token).
-
-
10.4. Execution environments [exec.env]
-
-
-
An execution environment contains state associated with the completion of an asynchronous operation. Every receiver has an associated execution environment, accessible with the get_env receiver query. The state of an execution environment is accessed with customization point objects. An execution environment may respond to any number of these environment queries.
-
-
An environment query is a customization point object that accepts as its first argument an execution environment. For an environment query EQ and an object e of type no_env, the expression EQ(e) shall be ill-formed.
no_env is a special environment used by the sender concept and by the get_completion_signatures customization point when the user has specified no environment argument. [Note: A user may choose to not specify an environment in order to see if a sender knows its completion signatures independent of any particular execution environment. -- end note]
-
-
10.4.2. execution::get_env[exec.get_env]
-
namespaceexec-envs{// exposition only
- structget_env_t;
-}
-inlineconstexprexec-envs::get_env_tget_env{};
-
-
-
-
get_env is a customization point object. For some subexpression r, get_env(r) is expression-equivalent to
-
-
-
tag_invoke(execution::get_env,r) if that expression is well-formed.
-
-
Otherwise, empty-env{}.
-
-
-
If get_env(r) is an lvalue, the object it refers to shall be valid while r is valid.
execution::forwarding_env_query is used to ask a customization point object whether it is a environment query that should be forwarded through environment adaptors.
-
-
The name execution::forwarding_env_query denotes a customization point object. For some subexpression t, execution::forwarding_env_query(t) is expression equivalent to:
-
-
-
tag_invoke(execution::forwarding_env_query,t), contextually converted to bool, if the tag_invoke expression is well formed.
-
-
-
Mandates: The tag_invoke expression is indeed contextually
-convertible to bool, that expression and the contextual conversion
-are not potentially-throwing and are core constant expressions if t is a core constant expression.
-
-
-
Otherwise, true.
-
-
-
10.5. Schedulers [exec.sched]
-
-
-
The scheduler concept defines the requirements of a type that allows for scheduling of work on its associated execution context.
Let S be the type of a scheduler and let E be the type of an execution environment for which sender<schedule_result_t<S>,E> is true. Then sender_of<schedule_result_t<S>,E> shall be true.
-
-
None of a scheduler’s copy constructor, destructor, equality comparison, or swap member functions shall exit via an exception.
-
-
None of these member functions, nor a scheduler type’s schedule function, shall introduce data races as a result of concurrent invocations of those functions from different threads.
-
-
For any two (possibly const) values s1 and s2 of some scheduler type S, s1==s2 shall return true only if both s1 and s2 are handles to the same associated execution context.
-
-
For a given scheduler expression s, the expression execution::get_completion_scheduler<set_value_t>(execution::schedule(s)) shall compare equal to s.
-
-
A scheduler type’s destructor shall not block pending completion of any receivers connected to the sender objects returned from schedule. [Note: The ability to wait for completion of submitted function objects may be provided by the associated execution
-context of the scheduler. —end note]
execution::forwarding_scheduler_query is used to ask a customization point object whether it is a scheduler query that should be forwarded through scheduler adaptors.
-
-
The name execution::forwarding_scheduler_query denotes a customization point object. For some subexpression t, execution::forwarding_scheduler_query(t) is expression equivalent to:
-
-
-
tag_invoke(execution::forwarding_scheduler_query,t), contextually converted to bool, if the tag_invoke expression is well formed.
-
-
-
Mandates: The tag_invoke expression is indeed contextually
-convertible to bool, that expression and the contextual conversion
-are not potentially-throwing and are core constant expressions if t is a core constant expression.
execution::get_forward_progress_guarantee is used to ask a scheduler about the forward progress guarantees of execution agents created by that scheduler.
-
-
The name execution::get_forward_progress_guarantee denotes a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::scheduler, execution::get_forward_progress_guarantee is ill-formed.
-Otherwise, execution::get_forward_progress_guarantee(s) is expression equivalent to:
-
-
-
tag_invoke(execution::get_forward_progress_guarantee,as_const(s)), if this expression is well formed.
-
-
-
Mandates: The tag_invoke expression above is not potentially
-throwing and its type is execution::forward_progress_guarantee.
If execution::get_forward_progress_guarantee(s) for some scheduler s returns execution::forward_progress_guarantee::concurrent, all execution agents created by that scheduler shall provide the concurrent forward progress guarantee. If it returns execution::forward_progress_guarantee::parallel, all execution agents created by that scheduler shall provide at least the parallel forward progress guarantee.
this_thread::execute_may_block_caller is used to ask a scheduler s whether a call execution::execute(s,f) with any invocable f may block the thread where such a call occurs.
-
-
The name this_thread::execute_may_block_caller denotes a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::scheduler, this_thread::execute_may_block_caller is ill-formed. Otherwise, this_thread::execute_may_block_caller(s) is expression equivalent to:
-
-
-
tag_invoke(this_thread::execute_may_block_caller,as_const(s)), if this expression is well formed.
-
-
-
Mandates: The tag_invoke expression above is not potentially
-throwing and its type is bool.
-
-
-
Otherwise, true.
-
-
-
If this_thread::execute_may_block_caller(s) for some scheduler s returns false, no execution::execute(s,f) call with some invocable f shall block the calling thread.
-
-
10.6. Receivers [exec.recv]
-
-
-
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, set_error, and set_stopped. These are collectively known as a receiver’s completion-signal operations.
-
-
The receiver concept defines the requirements for a receiver type with an unknown set of value types. The receiver_of concept defines the requirements for a receiver type with a known set of value types, whose error type is exception_ptr.
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 execution::start has been called on the operation state object that was returned by execution::connect to connect that receiver to a sender.
-
-
Once execution::start 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.
-
-
If execution::set_value exits with an exception, it is still valid to call execution::set_error or execution::set_stopped on the receiver, but it is no longer valid to call execution::set_value on the receiver.
-
-
-
Once one of a receiver’s completion-signal operations has completed non-exceptionally, the receiver contract has been satisfied.
-
-
Receivers have an associated execution environment that is accessible by passing the receiver to execution::get_env. A sender can obtain information about the current execution environment by querying the environment of the receiver to which it is connected. The set of environment queries is extensible and includes:
-
-
-
execution::get_scheduler: used to obtain a suggested scheduler to
-be used when a sender needs to launch additional work. [Note: the
-presence of this query on a receiver’s environment does not bind a
-sender to use its result. --end note]
-
-
execution::get_delegatee_scheduler: used to obtain a delegatee
-scheduler on which an algorithm or scheduler may delegate work for the
-purpose of ensuring forward progress.
-
-
execution::get_allocator: used to obtain a suggested allocator to be used when a sender 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_stop_token: used to obtain the associated stop
-token to be used by a sender 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]
-
-
-
Let r be a receiver, s be a sender, and op_state be an operation state
-resulting from an execution::connect(s,r) call. Let token be a stop
-token resulting from an execution::get_stop_token(execution::get_env(r)) call. token must remain valid at least until a call to a receiver
-completion-signal function of r returns successfully. [Note: this
-means that, unless it knows about further guarantees provided by the
-receiver r, the implementation of op_state should not use token after
-it makes a call to a receiver completion-signal function of r. This also
-implies that any stop callbacks registered on token by the implementation
-of op_state or s must be destroyed before such a call to a receiver
-completion-signal function of r. --end note]
-
-
10.6.1. execution::set_value[exec.set_value]
-
-
-
execution::set_value is used to send a value completion signal to a receiver.
-
-
The name execution::set_value denotes a customization point object. The expression execution::set_value(R,Vs...) for some subexpressions R and Vs... is expression-equivalent to:
-
-
-
tag_invoke(execution::set_value,R,Vs...), if that expression is valid. If the function selected by tag_invoke does not send the value(s) Vs... to the receiver R’s value channel, the program is ill-formed with no diagnostic required.
-
-
Otherwise, execution::set_value(R,Vs...) is ill-formed.
-
-
-
10.6.2. execution::set_error[exec.set_error]
-
-
-
execution::set_error is used to send a error signal to a receiver.
-
-
The name execution::set_error denotes a customization point object. The expression execution::set_error(R,E) for some subexpressions R and E is expression-equivalent to:
-
-
-
tag_invoke(execution::set_error,R,E), if that expression is valid. If the function selected by tag_invoke does not send the error E to the receiver R’s error channel, the program is ill-formed with no diagnostic required.
-
-
-
Mandates: The tag_invoke expression above is not potentially
-throwing.
-
-
-
Otherwise, execution::set_error(R,E) is ill-formed.
-
-
-
10.6.3. execution::set_stopped[exec.set_stopped]
-
-
-
execution::set_stopped is used to send a stopped signal to a receiver.
-
-
The name execution::set_stopped denotes a customization point object. The expression execution::set_stopped(R) for some subexpression R is expression-equivalent to:
-
-
-
tag_invoke(execution::set_stopped,R), if that expression is valid. If the function selected by tag_invoke does not signal the receiver R’s stopped channel, the program is ill-formed with no diagnostic required.
-
-
-
Mandates: The tag_invoke expression above is not potentially
-throwing.
-
-
-
Otherwise, execution::set_stopped(R) is ill-formed.
execution::forwarding_receiver_query is used to ask a customization point object whether it is a receiver query that should be forwarded through receiver adaptors.
-
-
The name execution::forwarding_receiver_query denotes a customization point object. For some subexpression t, execution::forwarding_receiver_query(t) is expression equivalent to:
-
-
-
tag_invoke(execution::forwarding_receiver_query,t), contextually converted to bool, if the tag_invoke expression is well formed.
-
-
-
Mandates: The tag_invoke expression is indeed contextually
-convertible to bool, that expression and the contextual conversion
-are not potentially-throwing and are core constant expressions if t is a core constant expression.
-
-
-
Otherwise, false if the type of t is one of set_value_t, set_error_t, or set_stopped_t.
-
-
Otherwise, true.
-
-
-
[Note: Currently the only standard receiver query is execution::get_env -- end note]
-
-
10.7. Operation states [exec.op_state]
-
-
-
The operation_state concept defines the requirements for an operation state type, which allows for starting the execution of work.
Any operation state types defined by the implementation are non-movable types.
-
-
10.7.1. execution::start[exec.op_state.start]
-
-
-
execution::start is used to start work represented by an operation state object.
-
-
The name execution::start denotes a customization point object. The expression execution::start(O) for some lvalue subexpression O is expression-equivalent to:
-
-
-
tag_invoke(execution::start,O), if that expression is valid. If the function selected by tag_invoke does not start the work represented by the operation state O, the program is ill-formed with no diagnostic required.
-
-
-
Mandates: The tag_invoke expression above is not potentially
-throwing.
-
-
-
Otherwise, execution::start(O) is ill-formed.
-
-
-
The caller of execution::start(O) must guarantee that the lifetime of the operation state object O extends at least until one of the receiver completion-signal functions of a receiver R passed into the execution::connect call that produced O 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]
-
-
10.8. Senders [exec.snd]
-
-
-
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 sender concept defines the requirements for a sender type. The sender_to concept defines the requirements for a sender type capable of being connected with a specific receiver type.
The alias template completion_signatures_of_t is used to query a sender type for facts associated with the signals it sends.
-
-
completion_signatures_of_t also recognizes awaitables as senders. For this clause ([exec]):
-
-
-
An awaitable is an expression that would be well-formed as the operand of a co_await expression within a given context.
-
-
For any type T, is-awaitable<T> is 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 await_transform. For a coroutine promise type P, is-awaitable<T,P> is 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 a such that decltype((a)) is type A, await-result-type<A> is an alias for decltype(e), where e is a's await-resume expression ([expr.await]) within the context of a coroutine whose promise type does not define a member await_transform. For a coroutine promise type P, await-result-type<A,P> is an alias for decltype(e), where e is a's await-resume expression ([expr.await]) within the context of a coroutine whose promise type is P.
-
-
-
For types S and E, the type completion_signatures_of_t<S,E> is an
-alias for decltype(get_completion_signatures(declval<S>(),declval<E>())) if that expression is well-formed and names a type other than no-completion-signatures. Otherwise, it is ill-formed.
-
-
execution::get_completion_signatures is a customization point object. Let s be an expression such that decltype((s)) is S, and let e be an
-expression such that decltype((e)) is E. Then get_completion_signatures(s) is expression-equivalent to get_completion_signatures(s,no_env{}) and get_completion_signatures(s,e) is expression-equivalent to:
-
-
-
tag_invoke_result_t<get_completion_signatures_t,S,E>{} if that expression is well-formed,
-
-
Otherwise, if remove_cvref_t<S>::completion_signatures is well-formed and names a type, then a prvalue of remove_cvref_t<S>::completion_signatures,
-
-
Otherwise, if is-awaitable<S> is true, then
-
-
-
If await-result-type<S> is cvvoid then a prvalue of a type equivalent to:
The exposition-only type variant-or-empty<Ts...> is
- defined as follows:
-
-
-
If sizeof...(Ts) is greater than zero, variant-or-empty<Ts...> names the type variant<Us...> where Us... is the pack decay_t<Ts>... with duplicate types removed.
-
-
Otherwise, variant-or-empty<Ts...> names an implementation defined class type equivalent to the following:
Let r be an rvalue receiver of type R, and let S be the type of a
-sender. If value_types_of_t<S,env_of_t<R>,Tuple,Variant> is well
-formed, it shall name the type Variant<Tuple<Args0...>,Tuple<Args1...>,...,Tuple<ArgsN...>>>, where the type packs Args0 through ArgsN are the packs of types the sender S passes as arguments to execution::set_value (besides the receiver object).
-If such sender S odr-uses ([basic.def.odr]) execution::set_value(r,args...), where decltype(args)... is not one of the type packs Args0... through ArgsN... (ignoring differences in
-rvalue-reference qualification), the program is ill-formed with no
-diagnostic required.
-
-
Let r be an rvalue receiver of type R, and let S be the type of a
-sender. If error_types_of_t<S,env_of_t<R>,Variant> is well formed, it
-shall name the type Variant<E0,E1,...,EN>, where the types E0 through EN are the types the sender S passes as arguments to execution::set_error (besides the receiver object). If such sender S odr-uses execution::set_error(r,e), where decltype(e) is not one of the types E0 through EN (ignoring differences in rvalue-reference qualification), the program is
-ill-formed with no diagnostic required.
-
-
Let r be an rvalue receiver of type R, and let S be the type of a
-sender. If completion_signatures_of_t<S,env_of_t<R>>::sends_stopped is well formed and false, and such sender S odr-uses execution::set_stopped(r), the program
-is ill-formed with no diagnostic required.
-
-
Let S be the type of a sender, let E be the type of an execution
-environment other than execution::no_env such that sender<S,E> is true. Let Tuple, Variant1, and Variant2 be variadic alias templates
-or class templates such that following types are well-formed:
-
-
-
value_types_of_t<S,no_env,Tuple,Variant1>
-
-
error_types_of_t<S,no_env,Variant2>
-
-
then the following shall also be true:
-
-
-
value_types_of_t<S,E,Tuple,Variant1> shall also be well-formed and shall
- name the same type as value_types_of_t<S,no_env,Tuple,Variant1>,
-
-
error_types_of_t<S,E,Variant2> shall also be well-formed and shall
- name the same type as error_types_of_t<S,no_env,Variant2>, and
-
-
completion_signatures_of_t<S,E>::sends_stopped shall have the same
- value as completion_signatures_of_t<S,no_env>::sends_stopped.
dependent_completion_signatures is a placeholder completion signatures
-descriptor that can be used to report that a type might be a sender within a
-particular execution environment, but it isn’t a sender in an arbitrary
-execution environment.
-
-
If decay_t<E> is no_env, dependent_completion_signatures<E> is equivalent to:
Otherwise, dependent_completion_signatures<E> is an empty struct.
-
-
10.8.2. execution::connect[exec.connect]
-
-
-
execution::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.
-
-
The name execution::connect denotes a customization point object. For some subexpressions s and r, let S be decltype((s)) and R be decltype((r)), and let S' and R' be the decayed types of S and R, respectively. If R does not satisfy execution::receiver, execution::connect(s,r) is ill-formed. Otherwise, the expression execution::connect(s,r) is expression-equivalent to:
-
-
-
tag_invoke(execution::connect,s,r), if that expression is valid and S satisfies execution::sender. If the function selected by tag_invoke does not return an operation state for which execution::start starts work described by s, the program is ill-formed with no diagnostic required.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies operation_state.
-
-
-
Otherwise, connect-awaitable(s,r) if is-awaitable<S,connect-awaitable-promise> is true and that expression is valid, where connect-awaitable is a coroutine equivalent to the following:
where connect-awaitable-promise is the promise type of connect-awaitable, and where connect-awaitable suspends at the initial suspends point ([dcl.fct.def.coroutine]), and:
-
-
-
set-value-expr first evaluates co_await(S&&)s, then suspends the coroutine and evaluates execution::set_value((R&&)r) if await-result-type<S,connect-awaitable-promise> is cvvoid; otherwise, it evaluates auto&&res=co_await(S&&)s, then suspends the coroutine and evaluates execution::set_value((R&&)r,(decltype(res))res).
-
If the call to execution::set_value exits with an exception, the coroutine is resumed and the exception is immediately propagated in the context of the coroutine.
-
[Note: If the call to execution::set_value exits normally, then the connect-awaitable coroutine is never resumed. --end note]
-
-
set-error-expr first suspends the coroutine and then executes execution::set_error((R&&)r,std::move(ep)).
-
[Note: The connect-awaitable coroutine is never resumed after the call to execution::set_error. --end note]
-
-
operation-state-task is a type that models operation_state. Its execution::start resumes the connect-awaitable coroutine, advancing it past the initial suspend point.
-
-
Let p be an lvalue reference to the promise of the connect-awaitable coroutine, let b be a const lvalue reference to the receiver r. Then tag_invoke(tag,p,as...) is expression-equivalent to tag(b,as...) for any set of arguments as... and for any tag whose type satisfies forwarding-receiver-query.
-
-
The expression p.unhandled_stopped() is expression-equivalent to (execution::set_stopped((R&&)r),noop_coroutine()).
-
-
For some expression e, the expression p.await_transform(e) is expression-equivalent to tag_invoke(as_awaitable,e,p) if that expression is well-formed; otherwise, it is expression-equivalent to e.
-
-
The operand of the requires-clause of connect-awaitable is equivalent to receiver_of<R> if await-result-type<S,connect-awaitable-promise> is cvvoid; otherwise, it is receiver_of<R,await-result-type<S,connect-awaitable-promise>>.
-
-
Otherwise, execution::connect(s,r) is ill-formed.
-
-
-
Standard sender types shall always expose an rvalue-qualified overload of a customization of execution::connect. Standard sender types shall only expose an lvalue-qualified overload of a customization of execution::connect if they are copyable.
execution::forwarding_sender_query is used to ask a customization point object whether it is a sender query that should be forwarded through sender adaptors.
-
-
The name execution::forwarding_sender_query denotes a customization point object. For some subexpression t, execution::forwarding_sender_query(t) is expression equivalent to:
-
-
-
tag_invoke(execution::forwarding_sender_query,t) contextually converted to bool, if the tag_invoke expression is well formed.
-
-
-
Mandates: The tag_invoke expression is indeed contextually
-convertible to bool, that expression and the contextual conversion
-are not potentially-throwing and are core constant expressions if t is a core constant expression.
execution::get_completion_scheduler is used to ask a sender object for the completion scheduler for one of its signals.
-
-
The name execution::get_completion_scheduler denotes a customization point object template. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::sender, execution::get_completion_scheduler<CPO>(s) is ill-formed for all template arguments CPO. If the template
-argument CPO in execution::get_completion_scheduler<CPO> is not one of execution::set_value_t, execution::set_error_t, or execution::set_stopped_t, execution::get_completion_scheduler<CPO> is ill-formed. Otherwise, execution::get_completion_scheduler<CPO>(s) is expression-equivalent to:
-
-
-
tag_invoke(execution::get_completion_scheduler<CPO>,as_const(s)) if this expression is well formed.
-
-
-
Mandates: The tag_invoke expression above is not potentially throwing and its type satisfies execution::scheduler.
-
-
-
Otherwise, execution::get_completion_scheduler<CPO>(s) is ill-formed.
-
-
-
If, for some sender s and customization point object CPO, execution::get_completion_scheduler<decltype(CPO)>(s) is well-formed and results in a scheduler sch, and the sender s invokes CPO(r,args...), for some receiver r which has been connected to s, with additional arguments args..., on an execution agent which does not belong to the associated execution context of sch, the behavior is undefined.
-
-
10.8.4. Sender factories [exec.factories]
-
10.8.4.1. General [exec.factories.general]
-
-
-
Subclause [exec.factories] defines sender factories, which are utilities that return senders without accepting senders as arguments.
-
-
10.8.4.2. execution::schedule[exec.schedule]
-
-
-
execution::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.
-
-
The name execution::schedule denotes a customization point object. For some subexpression s, the expression execution::schedule(s) is expression-equivalent to:
-
-
-
tag_invoke(execution::schedule,s), if that expression is valid. If the function selected by tag_invoke does not return a sender whose set_value completion scheduler is equivalent to s, the program is ill-formed with no diagnostic required.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, execution::schedule(s) is ill-formed.
-
-
-
10.8.4.3. execution::just[exec.just]
-
-
-
execution::just is used to create a sender that propagates a set of values to a connected receiver.
execution::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.
-
-
The name execution::transfer_just denotes a customization point object. For some subexpressions s and vs..., let S be decltype((s)) and Vs... be decltype((vs)). If S does not satisfy execution::scheduler, or any type V in Vs does not
-satisfy movable-value, execution::transfer_just(s,vs...) is ill-formed. Otherwise, execution::transfer_just(s,vs...) is expression-equivalent to:
-
-
-
tag_invoke(execution::transfer_just,s,vs...), if that expression is valid. If the function selected by tag_invoke does not return a sender whose set_value completion scheduler is equivalent to s and sends
-values equivalent to auto(vs)... to a receiver connected to it, the program is ill-formed with no diagnostic required.
-
-
-
Mandates:execution::sender_of<R,no_env,decltype(auto(vs))...>, where R is the type of the tag_invoke expression above.
execution::read is used to create a sender that retrieves a value from the receiver’s associated environment and sends it back to the receiver through the value channel.
-
-
execution::read is a customization point object of an unspecified class type equivalent to:
-
template<classTag>
- structread-sender;// exposition only
-
-structread-t{// exposition only
- template<classTag>
- read-sender<Tag>operator()(Tag)constnoexcept{return{};}
-};
-
-
-
read-sender is an exposition only class template equivalent to:
Subclause [exec.adapt] 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 execution::connect, and execution::start is called on the resulting operation state. This requirement applies to any function that is selected by the implementation of the sender adaptor.
-
-
A type T is a forwarding sender query if it is the type of a customization point object that models forwarding-sender-query. Unless otherwise specified, all sender adaptors that accept a single sender argument return sender objects that propagate forwarding sender queries to that single sender argument. This requirement applies to any function that is selected by the implementation of the
-sender adaptor.
-
-
A type T is a forwarding receiver query if it is the type of a customization point object that models forwarding-receiver-query. Unless otherwise specified, whenever a sender adaptor constructs a receiver that it passes to another sender’s connect, that receiver shall propagate forwarding receiver queries to a receiver accepted as an argument of execution::connect. This requirements
-applies to any sender returned from a function that is selected by the implementation of such sender adaptor.
-
-
For any sender type, receiver type, operation state type, execution environment type, or coroutine promise type that is part of the implementation of any sender adaptor in this subclause and that is a class template, the template arguments do not contribute to the associated entities ([basic.lookup.argdep]) of a function call where a specialization of the class template is an associated entity.
A pipeable sender adaptor closure object is a function object that accepts one or more sender arguments and returns a sender. For a sender adaptor closure object C and an expression S such that decltype((S)) models sender, the following
-expressions are equivalent and yield a sender:
-
C(S)
-S|C
-
-
Given an additional pipeable sender adaptor closure object D, the expression C|D produces another pipeable sender adaptor closure object E:
-
E is a perfect forwarding call wrapper ([func.require]) with the following properties:
-
-
-
Its target object is an object d of type decay_t<decltype((D))> direct-non-list-initialized with D.
-
-
It has one bound argument entity, an object c of type decay_t<decltype((C))> direct-non-list-initialized with C.
-
-
Its call pattern is d(c(arg)), where arg is the argument used in a function call expression of E.
-
-
The expression C|D is well-formed if and only if the initializations of the state entities of E are all well-formed.
-
-
An object t of type T is a pipeable sender adaptor closure object if T models derived_from<sender_adaptor_closure<T>>, T has no other base
-classes of type sender_adaptor_closure<U> for any other type U, and T does not model sender.
-
-
The template parameter D for sender_adaptor_closure may be an incomplete type. Before any expression of type cvD appears as
-an operand to the | operator, D shall be complete and model derived_from<sender_adaptor_closure<D>>. The behavior of an expression involving an
-object of type cvD as an operand to the | operator is undefined if overload resolution selects a program-defined operator| function.
-
-
A pipeable sender adaptor object is a customization point object that accepts a sender as its first argument and returns a 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 adaptor accepts more than one argument, then let s be an expression such that decltype((s)) models sender,
-let args... be arguments such that adaptor(s,args...) is a well-formed expression as specified in the rest of this subclause
-([exec.adapt.objects]), and let BoundArgs be a pack that denotes decay_t<decltype((args))>.... The expression adaptor(args...) produces a pipeable sender adaptor closure object f that is a perfect forwarding call wrapper with the following properties:
-
-
-
Its target object is a copy of adaptor.
-
-
Its bound argument entities bound_args consist of objects of types BoundArgs... direct-non-list-initialized with std::forward<decltype((args))>(args)..., respectively.
-
-
Its call pattern is adaptor(r,bound_args...), where r is the argument used in a function call expression of f.
-
-
-
The expression adaptor(args...) is well-formed if and only if the initializations of the bound argument entities of the result, as specified above,
-are all well-formed.
-
10.8.5.3. execution::on[exec.on]
-
-
-
execution::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.
-
-
The name execution::on denotes a customization point object. For some subexpressions sch and s, let Sch be decltype((sch)) and S be decltype((s)). If Sch does not satisfy execution::scheduler, or S does not satisfy execution::sender, execution::on is ill-formed. Otherwise, the expression execution::on(sch,s) is expression-equivalent to:
-
-
-
tag_invoke(execution::on,sch,s), if that expression is valid. If the function selected above does not return a sender which starts s on an execution agent of the associated execution context of sch when
-started, the program is ill-formed with no diagnostic required.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, constructs a sender s1. When s1 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r such that:
-
-
-
When execution::set_value(r) is called, it calls execution::connect(s,r2), where r2 is as specified below, which results in op_state3. It calls execution::start(op_state3). If any of these throws an exception, it calls execution::set_error on out_r, passing current_exception() as the second argument.
-
-
execution::set_error(r,e) is expression-equivalent to execution::set_error(out_r,e).
-
-
execution::set_stopped(r) is expression-equivalent to execution::set_stopped(out_r).
-
-
-
Calls execution::schedule(sch), which results in s2. It then calls execution::connect(s2,r), resulting in op_state2.
-
-
op_state2 is wrapped by a new operation state, op_state1, that is returned to the caller.
-
-
-
r2 is a receiver that wraps a reference to out_r and forwards all
- receiver completion-signals to it. In addition, execution::get_env(r2) returns an object e such that execution::get_scheduler(e) returns a copy of sch, and tag_invoke(tag,e,args...) is expression-equivalent to tag(e,args...) for all arguments args... and for all tag whose type
- satisfies forwarding-env-query and is not execution::get_scheduler_t.
-
-
When execution::start is called on op_state1, it calls execution::start on op_state2.
-
-
The lifetime of op_state2, once constructed, lasts until either op_state3 is constructed or op_state1 is destroyed, whichever comes first. The lifetime of op_state3, once constructed, lasts until op_state1 is destroyed.
-
-
-
10.8.5.4. execution::transfer[exec.transfer]
-
-
-
execution::transfer is used to adapt a sender into a sender with a different associated set_value completion scheduler. [Note: it results in a transition between different execution contexts when executed. --end note]
-
-
The name execution::transfer denotes a customization point object. For some subexpressions sch and s, let Sch be decltype((sch)) and S be decltype((s)). If Sch does not satisfy execution::scheduler, or S does not satisfy execution::sender, execution::transfer is ill-formed. Otherwise, the expression execution::transfer(s,sch) is expression-equivalent to:
-
-
-
tag_invoke(execution::transfer,get_completion_scheduler<set_value_t>(s),s,sch), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, tag_invoke(execution::transfer,s,sch), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies 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 execution::schedule_from(sch,s2), where s2 is a sender which sends equivalent to those sent by s, the program is ill-formed with no diagnostic required.
-
-
Senders returned from execution::transfer shall not propagate the sender queries get_completion_scheduler<CPO> to an input sender. They will implement get_completion_scheduler<CPO>, where CPO is one of set_value_t and set_stopped_t; this query returns a scheduler equivalent to the sch argument from those queries. The get_completion_scheduler<set_error_t> is not implemented, as the scheduler cannot be guaranteed in case an error is thrown while trying to schedule work on the given scheduler object.
execution::schedule_from is used to schedule work dependent on the completion of a sender onto a scheduler’s associated execution context. [Note: schedule_from is not meant to be used in user code; they are used in the implementation of transfer. -end note]
-
-
The name execution::schedule_from denotes a customization point object. For some subexpressions sch and s, let Sch be decltype((sch)) and S be decltype((s)). If Sch does not satisfy execution::scheduler, or S does not satisfy execution::sender, execution::schedule_from is ill-formed. Otherwise, the expression execution::schedule_from(sch,s) is expression-equivalent to:
-
-
-
tag_invoke(execution::schedule_from,sch,s), if that expression is valid. If the function selected by tag_invoke does not return a sender which completes on an execution agent belonging to the associated
-execution context of sch and sends signals equivalent to those sent by s, the program is ill-formed with no diagnostic required.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r such that when a receiver completion-signal Signal(r,args...) is called, it decay-copies args... into op_state (see below) as args'... and constructs a receiver r2 such that:
-
-
-
When execution::set_value(r2) is called, it calls Signal(out_r,std::move(args')...).
-
-
execution::set_error(r2,e) is expression-equivalent to execution::set_error(out_r,e).
-
-
execution::set_stopped(r2) is expression-equivalent to execution::set_stopped(out_r).
-
-
It then calls execution::schedule(sch), resulting in a sender s3. It then calls execution::connect(s3,r2), resulting in an operation state op_state3. It then calls execution::start(op_state3). If any of these throws an exception,
-it catches it and calls execution::set_error(out_r,current_exception()). If any of these expressions would be ill-formed, then Signal(r,args...) is ill-formed.
-
-
Calls execution::connect(s,r) resulting in an operation state op_state2. If this expression would be ill-formed, execution::connect(s2,out_r) is ill-formed.
-
-
Returns an operation state op_state that contains op_state2. When execution::start(op_state) is called, calls execution::start(op_state2). The lifetime of op_state3 ends when op_state is destroyed.
-
-
-
Given an expression e, let E be decltype((e)). If sender<S,E> is false, the type of tag_invoke(get_completion_signatures,s2,e) shall
-be equivalent to dependent_completion_signatures<E>. Otherwise, let Es... be the set of types in the type-list named by error_types_of_t<S,E,type-list>, and let Vs... be the set of unique types in the type-list named by value_types_of_t<S,E,set-value-signature,type-list>, where set-value-signature is the alias template:
Let Bs... be the set of unique types in [Es...,exception_ptr]. If either completion_signatures_of_t<schedule_result_t<Sch>,E>::sends_stopped or completion_signatures_of_t<S,E>::sends_stopped is true, then the type of tag_invoke(get_completion_signatures,s2,e) is a class type equivalent to:
Senders returned from execution::schedule_from shall not propagate the sender queries get_completion_scheduler<CPO> to an input sender. They will implement get_completion_scheduler<CPO>, where CPO is one of set_value_t and set_stopped_t; this query returns a scheduler equivalent to the sch argument from those queries. The get_completion_scheduler<set_error_t> is not implemented, as the scheduler cannot be guaranteed in case an error is thrown while trying to schedule work on the given scheduler object.
-
-
10.8.5.6. execution::then[exec.then]
-
-
-
execution::then is used to attach an invocable as a continuation for the successful completion of the input sender.
-
-
The name execution::then denotes a customization point object. For some
-subexpressions s and f, let S be decltype((s)), let F be the
-decayed type of f, and let f' be an xvalue refering to an object
-decay-copied from f. If S does not satisfy execution::sender, or F does not model movable-value, execution::then is
-ill-formed. Otherwise, the expression execution::then(s,f) is
-expression-equivalent to:
-
-
-
tag_invoke(execution::then,get_completion_scheduler<set_value_t>(s),s,f), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, tag_invoke(execution::then,s,f), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r such that:
-
-
-
When execution::set_value(r,args...) is called, let v be the
-expression invoke(f',args...). If decltype(v) is void,
-calls execution::set_value(out_r); otherwise, it calls execution::set_value(out_r,v). If any of these throw an
-exception, it catches it and calls execution::set_error(out_r,current_exception()). If any of these expressions would be ill-formed, the expression execution::set_value(r,args...) is ill-formed.
-
-
execution::set_error(r,e) is expression-equivalent to execution::set_error(out_r,e).
-
-
execution::set_stopped(r) is expression-equivalent to execution::set_stopped(out_r).
-
-
-
Returns an expression equivalent to execution::connect(s,r).
-
-
Given an expression e, let E be decltype((e)). If sender<S,E> is false, the type of tag_invoke(get_completion_signatures,s2,e) shall be equivalent to dependent_completion_signatures<E>. Otherwise, let V be value_types_of_t<S,E,result-type,type-list>, where result-type is
-the alias template:
If V is ill-formed, type of tag_invoke(get_completion_signatures,s2,e) shall be equivalent to dependent_completion_signatures<E>.
-
-
Otherwise, let As... be the unique set of types in the type-list named by V, and let Bs... be
-the unique set of types in the type-list named by error_types_of_t<S,E,type-list> with the addition of exception_ptr. If completion_signatures_of_t<S,E>::sends_stopped is true, the type of tag_invoke(get_completion_signatures,s2,e) shall be equivalent to:
where unary-set-value-signature<T> is an alias for set_value_t() if T is void; otherwise, set_value_t(T).
-
-
-
-
If the function selected above does not return a sender that invokes f with the result of the set_value signal of s, passing the return value as the value to any connected receivers, and propagates the other completion-signals sent by s, the program is
- ill-formed with no diagnostic required.
-
-
10.8.5.7. execution::upon_error[exec.upon_error]
-
-
-
execution::upon_error is used to attach an invocable as a continuation for the unsuccessful completion of the input sender.
-
-
The name execution::upon_error denotes a customization point object. For
-some subexpressions s and f, let S be decltype((s)), let F be the
-decayed type of f, and let f' be an xvalue refering to an object
-decay-copied from f. If S does not satisfy execution::sender, or F does not model movable-value, execution::upon_error is
-ill-formed. Otherwise, the expression execution::upon_error(s,f) is
-expression-equivalent to:
-
-
-
tag_invoke(execution::upon_error,get_completion_scheduler<set_error_t>(s),s,f), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, tag_invoke(execution::upon_error,s,f), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r such that:
-
-
-
execution::set_value(r,args...) is expression-equivalent to execution::set_value(out_r,args...).
-
-
When execution::set_error(r,e) is called, let v be the
-expression invoke(f',e). If decltype(v) is void, calls execution::set_value(out_r); otherwise, it calls execution::set_value(out_r,v). If any of these throw an
-exception, it catches it and calls execution::set_error(out_r,current_exception()). If any of these expressions would be
-ill-formed, the expression execution::set_error(r,e) is
-ill-formed.
-
-
execution::set_stopped(r) is expression-equivalent to execution::set_stopped(out_r).
-
-
-
Returns an expression equivalent to execution::connect(s,r).
-
-
Given some expression e, let E be decltype((e)). If sender<S,E> is false, the type of tag_invoke(get_completion_signatures,s2,e) shall be equivalent to dependent_completion_signatures<E>. Otherwise, let V be error_types_of_t<S,E,result-type-list>,
-where result-type-list is the alias template:
If V is ill-formed, type of tag_invoke(get_completion_signatures,s2,e) shall be equivalent to dependent_completion_signatures<E>.
-
-
Otherwise, let As... be the set of types in the type-list named by V, let Bs... be the
-set of types in the type-list named by value_types_of_t<S,E,set-value-signature,type-list>, and let Cs... be the unique set of
-types in [unary-set-value-signature<As>...,Bs...], where unary-set-value-signature<T> is an alias for set_value_t() if T is void; otherwise, set_value_t(T), and set-value-signature is the alias template:
If the function selected above does not return a sender which invokes f with the result of the set_error signal of s, passing the return value as the value to any connected receivers, and propagates the other completion-signals sent by s, the program is
- ill-formed with no diagnostic required.
execution::upon_stopped is used to attach an invocable as a continuation for the completion of the input sender using the "stopped" channel.
-
-
The name execution::upon_stopped denotes a customization point object. For
-some subexpressions s and f, let S be decltype((s)), let F be the
-decayed type of f, and let f' be an xvalue refering to an object
-decay-copied from f. If S does not satisfy execution::sender, or F does not model both movable-value and invocable, execution::upon_stopped is ill-formed. Otherwise, the expression execution::upon_stopped(s,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::upon_stopped,get_completion_scheduler<set_stopped_t>(s),s,f), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, tag_invoke(execution::upon_stopped,s,f), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r such that:
-
-
-
execution::set_value(r,args...) is expression-equivalent to execution::set_value(out_r,args...).
-
-
execution::set_error(r,e) is expression-equivalent to execution::set_error(out_r,e).
-
-
When execution::set_stopped(r) is called, let v be the
-expression invoke(f'). If v has type void, calls execution::set_value(out_r); otherwise, calls execution::set_value(out_r,v). If any of these throw an
-exception, it catches it and calls execution::set_error(out_r,current_exception()). If any of these expressions would be
-ill-formed, the expression execution::set_stopped(r) is
-ill-formed.
-
-
-
Returns an expression equivalent to execution::connect(s,r).
-
-
Given some expression e, let E be decltype((e)). If sender<S,E> is false, the type of tag_invoke(get_completion_signatures,s2,e) shall be equivalent to dependent_completion_signatures<E>. Otherwise, let Vs... be
-the set of types in the type-list named by value_types_of_t<S,E,set-value-signature,type-list> and let Es... be the unique set of types
-in the type-list named by error_types_of_t<S,E,type-list> with the
-addition of exception_ptr, where set-value-signature is the alias template:
If invoke_result_t<F> is void, let B be set_value_t();
-otherwise, let B be set_value_t(invoke_result_t<F>). Let As... be the unique set of types in [Vs...,B]. The type of tag_invoke(get_completion_signatures,s2,e) shall be equivalent to:
-
completion_signatures<As...,set_error_t(Es)...>
-
-
-
-
If the function selected above does not return a sender which invokes f when s completes by calling set_stopped, passing the return value as the value to any connected receivers, and propagates the other completion-signals sent by s, the program is
- ill-formed with no diagnostic required.
-
-
10.8.5.9. execution::let_value[exec.let_value]
-
-
-
execution::let_value is used to insert continuations creating more work dependent on the results of their input senders into a sender chain.
-
-
The name execution::let_value denotes a customization point object. For some subexpressions s and f, let S be decltype((s)), let F be the decayed type of f, and let f' be an xvalue that refers to an object decay-copied from f. If S does not satisfy execution::sender, execution::let_value is ill-formed. Otherwise, the expression execution::let_value(s,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::let_value,get_completion_scheduler<set_value_t>(s),s,f), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, tag_invoke(execution::let_value,s,f), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, given a receiver out_r and an lvalue out_r' refering to an object decay-copied from out_r.
-
-
-
Let r be an rvalue of a receiver type R such that:
-
-
-
When execution::set_value(r,args...) is called, decay-copies args... into op_state2 as args'..., then calls invoke(f',args'...), resulting in a sender s3. It then calls execution::connect(s3,std::move(out_r')), resulting in an operation state op_state3. op_state3 is saved as a part of op_state2. It then calls execution::start(op_state3). If any of these throws an exception, it catches it and calls execution::set_error(std::move(out_r'),current_exception()). If any of these expressions would be ill-formed, execution::set_value(r,args...) is ill-formed.
-
-
execution::set_error(r,e) is expression-equivalent to execution::set_error(std::move(out_r'),e).
-
-
execution::set_stopped(r) is expression-equivalent to execution::set_stopped(std::move(out_r')).
-
-
-
execution::let_value(s,f) returns a sender s2 such that:
-
-
-
If the expression execution::connect(s,r) is ill-formed, execution::connect(s2,out_r) is ill-formed.
-
-
Otherwise, let op_state2 be the result of execution::connect(s,r). execution::connect(s2,out_r) returns an operation state op_state that stores op_state2. execution::start(op_state) is expression-equivalent to execution::start(op_state2).
-
-
-
Given some expression e, let E be decltype((e)). If sender<S,E> is false, the type of tag_invoke(get_completion_signatures,s2,e) shall be equivalent to dependent_completion_signatures<E>. Otherwise, let Xs... be the set of types in the type-list named by error_types_of_t<S,E,type-list>, and let V be value_types_of_t<S,E,result-type,type-list>, where result-type is the alias template:
If V is ill-formed, type of tag_invoke(get_completion_signatures,s2,e) shall be equivalent to dependent_completion_signatures<E>.
-
-
Otherwise, let S3s... be the unique set of types in the type-list named by V. If (sender<S3s,E>&&...) is false, the type of tag_invoke(get_completion_signatures,s2,e) shall be equivalent to dependent_completion_signatures<E>.
-
-
Otherwise, for each type S3i in S3s..., let Vsi... be the list of types in the type-list named by value_types_of_t<S3i,E,set-value-signature,type-list>, let Esi... be the list of types in the type-list named by error_types_of_t<S3i,E,type-list>, and let Dsi... be the list of values completion_signatures_of_t<S3s,E>::sends_stopped..., where set-value-signature is the following alias template:
Let Us... be the set of unique types in [Vs0...,Vs1...,...Vsn-1...] and let Ws... be the unique set of types in [exception_ptr,Xs...,Es0...,Es1...,...Esn-1...], where n is sizeof...(S3s).
-
-
If either completion_signatures_of_t<S,E>::sends_stopped or (Ds||...) is true, the type of get_completion_signatures(s2,e) is equivalent to:
If execution::let_value(s,f) does not return a sender that invokes f when set_value is called, and making its completion dependent on the completion of a sender returned by f, and propagates the other completion-signals sent by s, the program is
- ill-formed with no diagnostic required.
-
-
10.8.5.10. execution::let_error[exec.let_error]
-
-
-
execution::let_error is used to insert continuations creating more work dependent on the error of its input senders into a sender chain.
-
-
The name execution::let_error denotes a customization point object. For some subexpressions s and f, let S be decltype((s)), let F be the decayed type of f, and let f' be an xvalue that refers to an object decay-copied from f. If S does not satisfy execution::sender, execution::let_error is ill-formed. Otherwise, the expression execution::let_error(s,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::let_error,get_completion_scheduler<set_value_t>(s),s,f), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, tag_invoke(execution::let_error,s,f), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, given a receiver out_r and an lvalue out_r' refering to an object decay-copied from out_r.
-
-
-
Let r be an rvalue of a receiver type R such that:
-
-
-
execution::set_value(r,args...) is expression-equivalent to execution::set_value(std::move(out_r'),args...).
-
-
When execution::set_error(r,e) is called, decay-copies e into op_state2 as e', then calls invoke(f',e'), resulting in a sender s3. It then calls execution::connect(s3,std::move(out_r')), resulting in an operation state op_state3. op_state3 is saved as a part of op_state2. It then calls execution::start(op_state3). If any of these throws an exception, it catches it and calls execution::set_error(std::move(out_r'),current_exception()). If any of these expressions would be ill-formed, execution::set_error(r,e) is ill-formed.
-
-
execution::set_stopped(r) is expression-equivalent to execution::set_stopped(std::move(out_r')).
-
-
-
execution::let_error(s,f) returns a sender s2 such that:
-
-
-
If the expression execution::connect(s,r) is ill-formed, execution::connect(s2,out_r) is ill-formed.
-
-
Otherwise, let op_state2 be the result of execution::connect(s,r). execution::connect(s2,out_r) returns an operation state op_state that stores op_state2. execution::start(op_state) is expression-equivalent to execution::start(op_state2).
-
-
-
Given some expression e, let E be decltype((e)). If sender<S,E> is false, the type of tag_invoke(get_completion_signatures,s2,e) shall be equivalent to dependent_completion_signatures<E>. Otherwise, let As... be the set of types in the type-list named by value_types_of_t<S,E,set-value-signature,type-list>, and let W be error_types_of_t<S,E,result-type-list>, where set-value-signature is the alias template:
If W is ill-formed, type of tag_invoke(get_completion_signatures,s2,e) shall be equivalent to dependent_completion_signatures<E>.
-
-
Otherwise, let S3s... be the unique set of types in the type-list named by W. If (sender<S3s,E>&&...) is false, the type of tag_invoke(get_completion_signatures,s2,e) shall be equivalent to dependent_completion_signatures<E>.
-
-
Otherwise, for each type S3i in S3s..., let Vsi... be the list of types in the type-list named by value_types_of_t<S3i,E,set-value-signature,type-list>, let Esi... be the list of types in the type-list named by error_types_of_t<S3i,E,type-list>, and let Dsi... be the list of values completion_signatures_of_t<S3s,E>::sends_stopped....
-
-
Let Us... be the set of unique types in [As...,Vs0...,Vs1...,...Vsn-1...] and let Ws... be the unique set of types in [exception_ptr,Es0...,Es1...,...Esn-1...], where n is sizeof...(S3s).
-
-
If either completion_signatures_of_t<S,E>::sends_stopped or (Ds||...) is true, the type of get_completion_signatures(s2,e) is equivalent to:
If execution::let_error(s,f) does not return a sender that invokes f when set_error is called, and making its completion dependent on the completion of a sender returned by f, and propagates the other completion-signals sent by s, the program is
- ill-formed with no diagnostic required.
execution::let_stopped is used to insert continuations creating more work dependent on the error its input senders into a sender chain.
-
-
The name execution::let_stopped denotes a customization point object. For some subexpressions s and f, let S be decltype((s)), let F be the decayed type of f, and let f' be an xvalue that refers to an object decay-copied from f. If S does not satisfy execution::sender or if F does not satisfy invocable, execution::let_stopped is ill-formed. Otherwise, the expression execution::let_stopped(s,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::let_stopped,get_completion_scheduler<set_value_t>(s),s,f), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, tag_invoke(execution::let_stopped,s,f), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, given a receiver out_r and an lvalue out_r' refering to an object decay-copied from out_r.
-
-
-
Let r be an rvalue of a receiver type R such that:
-
-
-
execution::set_value(r,args...) is expression-equivalent to execution::set_value(std::move(out_r'),args...).
-
-
execution::set_error(r,e) is expression-equivalent to execution::set_value(std::move(out_r'),e).
-
-
When execution::set_stopped(r) is called, calls invoke(f'), resulting in a sender s3. It then calls execution::connect(s3,std::move(out_r')), resulting in an operation state op_state3. op_state3 is saved as a part of op_state2. It then calls execution::start(op_state3). If any of these throws an exception, it catches it and calls execution::set_error(std::move(out_r'),current_exception()). If any of these expressions would be ill-formed, execution::set_stopped(r) is ill-formed.
-
-
-
execution::let_stopped(s,f) returns a sender s2 such that:
-
-
-
If the expression execution::connect(s,r) is ill-formed, execution::connect(s2,out_r) is ill-formed.
-
-
Otherwise, let op_state2 be the result of execution::connect(s,r). execution::connect(s2,out_r) returns an operation state op_state that stores op_state2. execution::start(op_state) is expression-equivalent to execution::start(op_state2).
-
-
-
Given some expression e, let E be decltype((e)), and let S3 be invoke_result_t<F>. If either sender<S,E> or sender<S3,E> is false, the type of tag_invoke(get_completion_signatures,s2,e) shall be equivalent to dependent_completion_signatures<E>. Otherwise, let As... be the set of types in the type-list named by value_types_of_t<S,E,set-value-signature,type-list>, and let Ws... be the types in the type-list named by error_types_of_t<S,E,type-list>, where set-value-signature is the alias template:
Let Vs... be the list of types in the type-list named by value_types_of_t<S3,E,set-value-signature,type-list>, and let Es... be the list of types in the type-list named by error_types_of_t<S3,E,type-list>.
-
-
Let Us... be the set of unique types in [As...,Vs...] and let Ws... be the unique set of types in [exception_ptr,Es...],.
-
-
If completion_signatures_of_t<S3,E>::sends_stopped is true, the type of get_completion_signatures(s2,e) is equivalent to:
If execution::let_stopped(s,f) does not return a sender that invokes f when set_stopped is called, and making its completion dependent on the completion of a sender returned by f, and propagates the other completion-signals sent by s, the program is
- ill-formed with no diagnostic required.
-
-
10.8.5.12. execution::bulk[exec.bulk]
-
-
-
execution::bulk is used to run a task repeatedly for every index in an index space.
-
-
The name execution::bulk denotes a customization point object. For some subexpressions s, shape, and f, let S be decltype((s)), Shape be decltype((shape)), and F be decltype((f)). If S does not satisfy execution::sender or Shape does not satisfy integral, execution::bulk is ill-formed. Otherwise, the expression execution::bulk(s,shape,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::bulk,get_completion_scheduler<set_value_t>(s),s,shape,f), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, tag_invoke(execution::bulk,s,shape,f), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r:
-
-
-
When execution::set_value(r,args...) is called, calls f(i,args...) for each i of type Shape from 0 to shape, then calls execution::set_value(out_r,args...). If any of these throws an exception, it catches it and calls execution::set_error(out_r,current_exception()).
-
-
When execution::set_error(r,e) is called, calls execution::set_error(out_r,e).
-
-
When execution::set_stopped(r) is called, calls execution::set_stopped(out_r,e).
-
-
-
Calls execution::connect(s,r), which results in an operation state op_state2.
-
-
Returns an operation state op_state that contains op_state2. When execution::start(op_state) is called, calls execution::start(op_state2).
-
-
-
Given an expression e, let E be decltype((e)). If sender<S,E> is false, the type of tag_invoke(get_completion_signatures,s2,e) shall be
-equivalent to dependent_completion_signatures<E>. Otherwise, let Es... be
-the types in the type-list named by error_types_of_t<S,E,type-list>, and let Bs... be the set of unique types in [Es...,exception_ptr]. The type
-of tag_invoke(get_completion_signatures,s2,e) shall be a class type Tr such that, for variadic templates Tuple and Variant:
-
-
-
Tr::value_types<Tuple,Variant> names the same type as value_types_of_t<S,E,Tuple,Variant>.
-
-
Tr::error_types<Variant> names the type Variant<Bs...>.
-
-
Tr::sends_stopped is a core constant expression of type bool and value completion_signatures_of_t<S,E>::sends_stopped.
-
-
-
If the function selected above does not return a sender which invokes f(i,args...) for each i of type Shape from 0 to shape when the input sender sends values args..., 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.
-
-
10.8.5.13. execution::split[exec.split]
-
-
-
execution::split is used to adapt an arbitrary sender into a sender that can be connected multiple times.
-
-
Let split-env be the type of an execution environment such that, given an instance e, the expression get_stop_token(e) is well formed and has type stop_token.
-
-
The name execution::split denotes a customization point object. For some
-subexpression s, let S be decltype((s)). If execution::sender<S,split-env> is false, execution::split is ill-formed. Otherwise, the expression execution::split(s) is expression-equivalent to:
-
-
-
tag_invoke(execution::split,get_completion_scheduler<set_value_t>(s),s),
-if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, tag_invoke(execution::split,s), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, constructs a sender s2, which:
-
-
-
Creates an object sh_state that contains a stop_source, a list of
-pointers to operation states awaiting the completion of s, and that
-also reserves space for storing:
-
-
-
the operation state that results from connecting s with r described below, and
-
-
the sets of values and errors with which s may complete, with
-the addition of exception_ptr.
-
-
-
Constructs a receiver r such that:
-
-
-
When execution::set_value(r,args...) is called, decay-copies
-the expressions args... into sh_state. It then notifies all
-the operation states in sh_state's list of operation states
-that the results are ready. If any exceptions are thrown, the
-exception is caught and execution::set_error(r,current_exception()) is called instead.
-
-
When execution::set_error(r,e) is called, decay-copies e into sh_state. It then notifies the operation states in sh_state's list of operation states that the results are ready.
-
-
When execution::set_stopped(r) is called, it then notifies the
-operation states in sh_state's list of operation states that
-the results are ready.
-
-
get_env(r) is an expression e of type split-env such that execution::get_stop_token(e) is well-formed
-and returns the results of calling get_token() on sh_state's
-stop source.
-
-
-
Calls execution::connect(s,r), resulting in an operation state op_state2. op_state2 is saved in sh_state.
-
-
When s2 is connected with a receiver out_r of type OutR, it
-returns an operation state object op_state that contains:
-
-
-
An object out_r' of type OutR decay-copied from out_r,
-
-
A reference to sh_state,
-
-
A stop callback of type optional<stop_token_of_t<env_of_t<OutR>>::callback_type<stop-callback-fn>>,
-where stop-callback-fn is an implementation
-defined class type equivalent to the following:
If r's receiver contract has already been satisfied, then let Signal be whichever receiver completion-signal
-was used to complete r’s receiver contract ([exec.recv]). Calls Signal(out_r',args2...), where args2... is a
-pack of lvalues referencing the subobjects of sh_state that have
-been saved by the original call to Signal(r,args...) and returns.
-
-
Otherwise, it emplace constructs the stop callback optional with
-the arguments execution::get_stop_token(get_env(out_r')) and stop-callback-fn{stop-src}, where stop-src refers to the stop source of sh_state.
-
-
Then, it checks to see if stop-src.stop_requested() is true. If so, it
-calls execution::set_stopped(out_r').
-
-
Otherwise, it adds a pointer to op_state to the list of
-operation states in sh_state and calls execution::start(op_state2) if this would be the first such
-invocation.
-
-
-
When r completes it will notify op_state that the result are
-ready. Let Signal be whichever receiver
-completion-signal was used to complete r's receiver contract
-([exec.recv]). op_state's stop callback optional is reset. Then Signal(std::move(out_r'),args2...) is called,
-where args2... is a pack of lvalues referencing the subobjects of sh_state that have been saved by the original call to Signal(r,args...).
-
-
Ownership of sh_state is shared by s2 and by every op_state that results from connecting s2 to a receiver.
-
-
-
Given an expression e, let E be decltype((e)). If sender<S,E> is false, the type of tag_invoke(get_completion_signatures,s2,e) shall
-be equivalent to dependent_completion_signatures<E>. Otherwise, let Vs... be the set of unique types in the type-list named by value_types_of_t<S,E,set-value-signature,type-list>, and let Es... be the set of types in the type-list named by error_types_of_t<S,E,error-types>, where set-value-signature is the alias template:
Let Bs... be the set of unique types in [Es...,exception_ptr&]. If completion_signatures_of_t<S,E>::sends_stopped is true, then the type of tag_invoke(get_completion_signatures,s2,e) is a class type equivalent to:
If the function selected above does not return a sender which sends references to values sent by s, propagating the other channels, the program is ill-formed with no diagnostic required.
-
-
10.8.5.14. execution::when_all[exec.when_all]
-
-
-
execution::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_with_variant 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.
-
-
The name execution::when_all denotes a customization point object. For some subexpressions si..., let Si... be decltype((si)).... The expression execution::when_all(si...) is ill-formed if any of the following is true:
-
-
-
If the number of subexpressions si... is 0, or
-
-
If any type Si does not satisfy execution::sender.
-
-
Otherwise, the expression execution::when_all(si...) is expression-equivalent to:
-
-
-
tag_invoke(execution::when_all,si...), if that expression is valid. If the function selected by tag_invoke does not return a sender that sends a concatenation of values sent by si... when they all complete with set_value, the program is ill-formed with no diagnostic required.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, constructs a sender w of type W. When w is connected with some receiver out_r of type OutR, it returns an operation state op_state specified as below:
-
-
-
For each sender si, constructs a receiver ri such that:
-
-
-
If execution::set_value(ri,ti...) is called for every ri, op_state's associated stop callback optional is reset and execution::set_value(out_r,t0...,t1...,...,tn-1...) is called, where n the number of subexpressions in si....
-
-
Otherwise, execution::set_error or execution::set_stopped was called for at least one receiver ri. If the first such to complete did so with the call execution::set_error(ri,e), request_stop is called on op_state's associated stop source. When all child operations have completed, op_state's associated stop callback optional is reset and execution::set_error(out_r,e) is called.
-
-
Otherwise, request_stop is called on op_state's associated stop source. When all child operations have completed, op_state's associated stop callback optional is reset and execution::set_stopped(out_r) is called.
-
-
For each receiver ri, get_env(ri) is an expression e such that execution::get_stop_token(e) is well-formed and returns the results of calling get_token() on op_state's associated stop source, and for which tag_invoke(tag,e,args...) is expression-equivalent to tag(get_env(out_r),args...) for all arguments args... and all tag whose type satisfies forwarding-env-query and is not get_stop_token_t.
-
-
-
For each sender si, calls execution::connect(si,ri), resulting in operation states child_opi.
-
-
Returns an operation state op_state that contains:
-
-
-
Each operation state child_opi,
-
-
A stop source of type in_place_stop_source,
-
-
A stop callback of type optional<stop_token_of_t<env_of_t<OutR>>::callback_type<stop-callback-fn>>, where stop-callback-fn is an implementation defined class type equivalent to the following:
Emplace constructs the stop callback optional with the arguments execution::get_stop_token(get_env(out_r)) and stop-callback-fn{stop-src}, where stop-src refers to the stop source of op_state.
-
-
Then, it checks to see if stop-src.stop_requested() is true. If so, it calls execution::set_stopped(out_r).
-
-
Otherwise, calls execution::start(child_opi) for each child_opi.
-
-
-
Given some expression e, let E be decltype((e)) and let WE be a type such that stop_token_of_t<WE> is in_place_stop_token and tag_invoke_result_t<Tag,WE,As...> names the type, if any, of call-result-t<Tag,E,As...> for all types As... and all Tag besides get_stop_token_t.
-
-
-
-
If for any sender Si, sender<Si,WE> is false, the type of tag_invoke(get_completion_signatures,w,e) shall be equivalent to dependent_completion_signatures<E>.
-
-
Otherwise, if type value_types_of_t<Si,WE,decayed-tuple,zero-or-one> is ill-formed, the type of tag_invoke(get_completion_signatures,w,e) shall be equivalent to dependent_completion_signatures<E>, where zero-or-one is an alias template equivalent to the following:
Variant<> if for any type Si, the type value_types_of_t<Si,WE,Tuple,Variant> is Variant<>.
-
-
Otherwise, Variant<Tuple<V0...,V1,...,Vn-1...>> where n is the count of types in Si..., and where Vi... is a set of types such that for the type Si, value_types_of_t<Si,WE,Tuple,Variant> is an alias for Variant<Tuple<Vi...>>.
-
-
-
Tr::error_types<Variant> is Variant<Ui...>, where Ui... is the unique set of types in [exception_ptr,E0...,E1,...,En-1...], where Ei... is a set of types such that for the type Si, error_types_of_t<Si,WE,Variant> is an alias for Variant<Ei...>.
-
-
Tr::sends_stopped is true.
-
-
-
-
-
The name execution::when_all_with_variant denotes a customization point object. For some subexpressions s..., let S be decltype((s)). If any type Si in S... does not satisfy execution::sender, execution::when_all_with_variant is ill-formed. Otherwise, the expression execution::when_all_with_variant(s...) is expression-equivalent to:
-
-
-
tag_invoke(execution::when_all_with_variant,s...), if that expression
-is valid. If the function selected by tag_invoke does not return a
-sender that, when connected with a receiver of type R, sends the types into-variant-type<S,env_of_t<R>>... when they
-all complete with set_value, the program is ill-formed with no
-diagnostic required.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
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 that only send a single set of values each, while also making sure
-that they complete on the specified scheduler. execution::transfer_when_all_with_variant 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]
-
-
The name execution::transfer_when_all denotes a customization point object. For some subexpressions sch and s..., let Sch be decltype(sch) and S be decltype((s)). If Sch does not satisfy scheduler, or any type Si in S... does not satisfy execution::sender, execution::transfer_when_all is ill-formed. Otherwise, the expression execution::transfer_when_all(sch,s...) is expression-equivalent to:
-
-
-
tag_invoke(execution::transfer_when_all,sch,s...), if that expression is valid. If the function selected by tag_invoke does not return a sender that sends a concatenation of values sent by s... when
-they all complete with set_value, 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 of sch, the program is ill-formed with no diagnostic
-required.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
The name execution::transfer_when_all_with_variant denotes a customization
-point object. For some subexpressions sch and s..., let Sch be decltype((sch)) and let S be decltype((s)). If any type Si in S... does not satisfy execution::sender, execution::transfer_when_all_with_variant is
-ill-formed. Otherwise, the expression execution::transfer_when_all_with_variant(sch,s...) is expression-equivalent
-to:
-
-
-
tag_invoke(execution::transfer_when_all_with_variant,s...), if that
-expression is valid. If the function selected by tag_invoke does not
-return a sender that, when connected with a receiver of type R, sends
-the types into-variant-type<S,env_of_t<R>>... when they all complete with set_value, the program is ill-formed with
-no diagnostic required.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
Senders returned from execution::transfer_when_all shall not propagate the sender queries get_completion_scheduler<CPO> to input senders. They will implement get_completion_scheduler<CPO>, where CPO is one of set_value_t and set_stopped_t; this query returns a scheduler equivalent to the sch argument from those queries. The get_completion_scheduler<set_error_t> is not implemented, as the scheduler cannot be guaranteed in case an error is thrown while trying to schedule work on the given scheduler object.
execution::into_variant can be used to turn a sender which sends multiple sets of values into a sender which sends a variant of all of those sets of values.
-
-
The template into-variant-type is used to compute the type sent by a sender returned from execution::into_variant.
execution::stopped_as_optional is used to handle a stopped 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 stopped, reporting cancellation by completing with an empty optional.
-
-
The name execution::stopped_as_optional denotes a customization point object. For some subexpression s, let S be decltype((s)). Let get-env-sender be an expression such that, when it is connected with a receiver r, start on the resulting operation state completes immediately by calling execution::set_value(r,get_env(r)). The expression execution::stopped_as_optional(s) is expression-equivalent to:
execution::stopped_as_error is used to handle a stopped signal by mapping it into the error channel as a custom exception type. The result is a sender that never completes with stopped, reporting cancellation by completing with an error.
-
-
The name execution::stopped_as_error denotes a customization point object. For some subexpressions s and e, let S be decltype((s)) and let E be decltype((e)). If the type S does not satisfy sender or if the type E doesn’t satisfy movable-value, execution::stopped_as_error(s,e) is ill-formed. Otherwise, the expression execution::stopped_as_error(s,e) is expression-equivalent to:
execution::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.
-
-
Let ensure-started-env be the type of an execution
-environment such that, given an instance e, the expression get_stop_token(e) is well formed and has type stop_token.
-
-
The name execution::ensure_started denotes a customization point object.
-For some subexpression s, let S be decltype((s)). If execution::sender<S,ensure-started-env> is false, execution::ensure_started(s) is ill-formed. Otherwise, the
-expression execution::ensure_started(s) is expression-equivalent to:
-
-
-
tag_invoke(execution::ensure_started,get_completion_scheduler<set_value_t>(s),s), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, tag_invoke(execution::ensure_started,s), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, constructs a sender s2, which:
-
-
-
Creates an object sh_state that contains a stop_source, an
-initially null pointer to an operation state awaitaing completion,
-and that also reserves space for storing:
-
-
-
the operation state that results from connecting s with r described below, and
-
-
the sets of values and errors with which s may complete, with
-the addition of exception_ptr.
-
-
s2 shares ownership of sh_state with r described below.
-
-
Constructs a receiver r such that:
-
-
-
When execution::set_value(r,args...) is called, decay-copies
-the expressions args... into sh_state. It then checks sh_state to see if there is an operation state awaiting
-completion; if so, it notifies the operation state that the
-results are ready. If any exceptions are thrown, the exception
-is caught and execution::set_error(r,current_exception()) is
-called instead.
-
-
When execution::set_error(r,e) is called, decay-copies e into sh_state. If there is an operation state awaiting completion,
-it then notifies the operation states that the results are ready.
-
-
When execution::set_stopped(r) is called, it then notifies any
-awaiting operation state that the results are ready.
-
-
get_env(r) is an expression e of type ensure-started-env such that execution::get_stop_token(e) is well-formed
-and returns the results of calling get_token() on sh_state's
-stop source.
-
-
r shares ownership of sh_state with s2. After r's
-receiver contract has been completed, it releases its ownership
-of sh_state.
-
-
-
Calls execution::connect(s,r), resulting in an operation state op_state2. op_state2 is saved in sh_state. It then calls execution::start(op_state2).
-
-
When s2 is connected with a receiver out_r of type OutR, it
-returns an operation state object op_state that contains:
-
-
-
An object out_r' of type OutR decay-copied from out_r,
-
-
A reference to sh_state,
-
-
A stop callback of type optional<stop_token_of_t<env_of_t<OutR>>::callback_type<stop-callback-fn>>,
-where stop-callback-fn is an implementation
-defined class type equivalent to the following:
s2 transfers its ownership of sh_state to op_state.
-
-
When execution::start(op_state) is called:
-
-
-
If r's receiver contract has already been satisfied, then let Signal be whichever receiver completion-signal
-was used to complete r's receiver contract ([exec.recv]). Calls Signal(out_r',args2...), where args2... is a
-pack of xvalues referencing the subobjects of sh_state that have
-been saved by the original call to Signal(r,args...) and returns.
-
-
Otherwise, it emplace constructs the stop callback optional with
-the arguments execution::get_stop_token(get_env(out_r')) and stop-callback-fn{stop-src}, where stop-src refers to the stop source of sh_state.
-
-
Then, it checks to see if stop-src.stop_requested() is true. If so, it
-calls execution::set_stopped(out_r').
-
-
Otherwise, it sets sh_state operation state pointer to the
-address of op_state, registering itself as awaiting the result
-of the completion of r.
-
-
-
When r completes it will notify op_state that the result are
-ready. Let Signal be whichever receiver
-completion-signal was used to complete r's receiver contract
-([exec.recv]). op_state's stop callback optional is reset. Then Signal(std::move(out_r'),args2...) is called,
-where args2... is a pack of xvalues referencing the subobjects of sh_state that have been saved by the original call to Signal(r,args...).
-
-
[Note: If sender s2 is destroyed without being connected to a
-receiver, or if it is connected but the operation state is destroyed
-without having been started, then when r's receiver contract
-completes and it releases its shared ownership of sh_state, sh_state will be destroyed and the results of the operation are
-discarded. -- end note]
-
-
-
Given an expression e, let E be decltype((e)). If sender<S,E> is false, the type of tag_invoke(get_sender_traits,s2,e) shall
-be equivalent to dependent_completion_signatures<E>. Otherwise, let Vs... be the set of unique types in the type-list named by value_types_of_t<S,E,set-value-signature,type-list>, and let Es... be the set of types in the type-list named by error_types_of_t<S,E,error-types>, where set-value-signature is the alias template:
Let Bs... be the set of unique types in [Es...,exception_ptr&&]. If sender_traits_t<S,E>::sends_stopped is true, then the type of tag_invoke(get_sender_traits,s2,e) is a class type equivalent to:
If the function selected above does not return a sender that sends xvalue references to values sent by s, propagating the other channels, the program is ill-formed with no diagnostic required.
execution::start_detached is used to eagerly start a sender without the caller needing to manage the lifetimes of any objects.
-
-
The name execution::start_detached denotes a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::sender, execution::start_detached is ill-formed. Otherwise, the expression execution::start_detached(s) is expression-equivalent to:
-
-
-
tag_invoke(execution::start_detached,execution::get_completion_scheduler<execution::set_value_t>(s),s), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above is void.
-
-
-
Otherwise, tag_invoke(execution::start_detached,s), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above is void.
-
-
-
Otherwise:
-
-
-
Constructs a receiver r:
-
-
-
When set_value(r,ts...) is called, it does nothing.
-
-
When set_error(r,e) is called, it calls std::terminate.
-
-
When set_stopped(r) is called, it does nothing.
-
-
-
Calls execution::connect(s,r), resulting in an operation state op_state, then calls execution::start(op_state). The lifetime of op_state lasts until one of the receiver completion-signals of r is called.
-
-
-
If the function selected above does not eagerly start the sender s after connecting it with a receiver which ignores the set_value and set_stopped signals and calls std::terminate on the set_error signal, the program is ill-formed with no diagnostic required.
-
-
10.8.6.2. this_thread::sync_wait[exec.sync_wait]
-
-
-
this_thread::sync_wait and this_thread::sync_wait_with_variant 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.
-
-
For any receiver r created by an implementation of sync_wait and sync_wait_with_variant, the expressions get_scheduler(get_env(r)) and get_delegatee_scheduler(get_env(r)) shall be well-formed. For a receiver
-created by the default implementation of this_thread::sync_wait, these
-expressions shall return a scheduler to the same thread-safe,
-first-in-first-out queue of work such that tasks scheduled to the queue
-execute on the thread of the caller of sync_wait. [Note: The
-scheduler for an instance of execution::run_loop that is a local variable
-within sync_wait is one valid implementation. -- end note]
-
-
The templates sync-wait-type and sync-wait-with-variant-type are used to determine the
-return types of this_thread::sync_wait and this_thread::sync_wait_with_variant. Let sync-wait-env be the type of the expression get_env(r) where r is an instance of the
-receiver created by the default implementation of sync_wait.
The name this_thread::sync_wait denotes a customization point object. For
-some subexpression s, let S be decltype((s)). If execution::sender<S,sync-wait-env> is false,
-or the number of the arguments completion_signatures_of_t<S,sync-wait-env>::value_types passed into the Variant template
-parameter is not 1, this_thread::sync_wait is ill-formed. Otherwise, this_thread::sync_wait is expression-equivalent to:
-
-
-
tag_invoke(this_thread::sync_wait,execution::get_completion_scheduler<execution::set_value_t>(s),s), if this expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above is sync-wait-type<S,sync-wait-env>.
-
-
-
Otherwise, tag_invoke(this_thread::sync_wait,s), if this expression is valid and its type is.
-
-
-
Mandates: The type of the tag_invoke expression above is sync-wait-type<S,sync-wait-env>.
-
-
-
Otherwise:
-
-
-
Constructs a receiver r.
-
-
Calls execution::connect(s,r), resulting in an operation state op_state, then calls execution::start(op_state).
-
-
Blocks the current thread until a receiver completion-signal of r is called. When it is:
-
-
-
If execution::set_value(r,ts...) has been called, returns sync-wait-type<S,sync-wait-env>{decayed-tuple<decltype(ts)...>{ts...}}.
-
-
If execution::set_error(r,e) has been called, let E be the decayed type of e. If E is exception_ptr, calls std::rethrow_exception(e). Otherwise, if the E is error_code, throws system_error(e). Otherwise, throws e.
-
-
If execution::set_stopped(r) has been called, returns sync-wait-type<S,sync-wait-env>{}.
-
-
-
-
-
The name this_thread::sync_wait_with_variant denotes a customization point
-object. For some subexpression s, let S be the type of execution::into_variant(s). If execution::sender<S,sync-wait-env> is false, this_thread::sync_wait_with_variant is ill-formed. Otherwise, this_thread::sync_wait_with_variant is expression-equivalent to:
-
-
-
tag_invoke(this_thread::sync_wait_with_variant,execution::get_completion_scheduler<execution::set_value_t>(s),s), if this expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above is sync-wait-with-variant-type<S,sync-wait-env>.
-
-
-
Otherwise, tag_invoke(this_thread::sync_wait_with_variant,s), if this expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above is sync-wait-with-variant-type<S,sync-wait-env>.
execution::execute is used to create fire-and-forget tasks on a specified scheduler.
-
-
The name execution::execute denotes a customization point object. For some subexpressions sch and f, let Sch be decltype((sch)) and F be decltype((f)). If Sch does not satisfy execution::scheduler or F does not satisfy invocable, execution::execute is ill-formed. Otherwise, execution::execute is expression-equivalent to:
-
-
-
tag_invoke(execution::execute,sch,f), if that expression is valid. If
-the function selected by tag_invoke does not invoke the function f (or an object decay-copied from f) on an execution agent belonging to
-the associated execution context of sch, or if it does not call std::terminate if an error occurs after control is returned to the
-caller, the program is ill-formed with no diagnostic required.
-
-
-
Mandates: The type of the tag_invoke expression above is void.
template<
- class-typeDerived,
- receiverBase=unspecified>// arguments are not associated entities ([lib.tmpl-heads])
- classreceiver_adaptor;
-
-
-
-
receiver_adaptor is used to simplify the implementation of one receiver type in terms of another. It defines tag_invoke overloads that forward to named members if they exist, and to the adapted receiver otherwise.
-
-
This section makes use of the following exposition-only entities:
[Note:receiver_adaptor provides tag_invoke overloads on behalf of
-the derived class Derived, which is incomplete when receiver_adaptor is
-instantiated.]
completion_signatures is used to define a type that implements the nested value_types, error_types, and sends_stopped members that describe the
-ways a sender completes. Its arguments are a flat list of function types
-that describe the signatures of the receiver’s completion-signal operations
-that the sender invokes.
Let ValueFns be a template parameter pack of the function types in Fns whose return types are execution::set_value_t, and let Valuesn be a template parameter pack of the function argument types in the n-th type in ValueFns. Then, given two variadic templates Tuple and Variant, the type completion_signatures<Fns...>::value_types<Tuple,Variant> names the type Variant<Tuple<Values0...>,Tuple<Values1...>,...Tuple<Valuesm-1...>>, where m is the size of the parameter pack ValueFns.
-
-
Let ErrorFns be a template parameter pack of the function types in Fns whose return types are execution::set_error_t, and let Errorn be the function argument type in the n-th type in ErrorFns. Then, given a variadic template Variant, the type completion_signatures<Fns...>::error_types<Variant> names the type Variant<Error0,Error1,...Errorm-1>, where m is the size of the parameter pack ErrorFns.
-
-
completion_signatures<Fns...>::sends_stopped is true if at least one of the types in Fns is execution::set_stopped_t(); otherwise, false.
make_completion_signatures is an alias template used to adapt the
-completion signatures of a sender. It takes a sender, and environment, and
-several other template arguments that apply modifications to the sender’s
-completion signatures to generate a new instantiation of execution::completion_signatures.
-
-
[Example:
-
// Given a sender S and an environment Env, adapt a S’s completion
-// signatures by lvalue-ref qualifying the values, adding an additional
-// exception_ptr error completion if its not already there, and leaving the
-// other signals alone.
-template<class...Args>
- usingmy_set_value_t=execution::set_value_t(add_lvalue_reference_t<Args>...);
-
-usingmy_completion_signals=
- execution::make_completion_signatures<
- S,Env,
- execution::completion_signatures<execution::set_error_t(exception_ptr)>,
- my_set_value_t>;
-
-
-- end example]
-
-
This section makes use of the following exposition-only entities:
AddlSigs shall name an instantiation of the execution::completion_signatures class template.
-
-
SetValue shall name an alias template such that for any template
-parameter pack As..., SetValue<As...> is either ill-formed, void or an
-alias for a function type whose return type is execution::set_value_t.
-
-
SetError shall name an alias template such that for any type Err, SetError<Err> is either ill-formed, void or an alias for a function
-type whose return type is execution::set_error_t.
-
-
Let Vs... be a pack of the non-void types in the type-list named
-by value_types_of_t<Sndr,Env,SetValue,type-list>.
-
-
Let Es... be a pack of the non-void types in the type-list named by error_types_of_t<Sndr,Env,error-list>, where error-list is an
-alias template such that error-list<Ts...> names type-list<SetError<Ts>...>.
-
-
Let Ss... be an empty pack if SendsStopped is false; otherwise, a
-pack containing the single type execution::set_stopped_t().
-
-
Let MoreSigs... be a pack of the template arguments of the execution::completion_signatures instantiation named by AddlSigs.
-
-
If any of the above types are ill-formed, then make_completion_signatures<Sndr,Env,AddlSigs,SetValue,SetDone,SendsStopped> is an alias for dependent_completion_signatures<Env>.
-
-
Otherwise, make_completion_signatures<Sndr,Env,AddlSigs,SetValue,SetDone,SendsStopped> names the type completion_signatures<Sigs...> where Sigs... is the unique set of types in [Vs...,Es...,Ss...,MoreSigs...].
-
-
-
10.11. Execution contexts [exec.ctx]
-
-
-
This section specifies some execution contexts on which work can be scheduled.
-
-
10.11.1. run_loop[exec.run_loop]
-
-
-
A run_loop is an execution context on which work can be scheduled. It maintains a simple, thread-safe first-in-first-out queue of work. Its run() member function removes elements from the queue and executes them in a loop on whatever thread of execution calls run().
-
-
A run_loop instance has an associated count that corresponds to the number of work items that are in its queue. Additionally, a run_loop has an associated state that can be one of starting, running, or finishing.
-
-
Concurrent invocations of the member functions of run_loop, other than run and its destructor, do not introduce data races. The member functions pop_front, push_back, and finish execute atomically.
-
-
[Note: Implementations are encouraged to use an intrusive queue of operation states to hold the work units to make scheduling allocation-free. — end note]
-
classrun_loop{
- // [exec.run_loop.types] Associated types
- classrun-loop-scheduler;// exposition only
- classrun-loop-sender;// exposition only
- structrun-loop-opstate-base{// exposition only
- virtualvoidexecute()=0;
- run_loop*loop_;
- run-loop-opstate-base*next_;
- };
- template<receiver_ofR>
- usingrun-loop-opstate=unspecified;// exposition only
-
- // [exec.run_loop.members] Member functions:
- run-loop-opstate-base*pop_front();// exposition only
- voidpush_back(run-loop-opstate-base*);// exposition only
-
- public:
- // [exec.run_loop.ctor] construct/copy/destroy
- run_loop()noexcept;
- run_loop(run_loop&&)=delete;
- ~run_loop();
-
- // [exec.run_loop.members] Member functions:
- run-loop-schedulerget_scheduler();
- voidrun();
- voidfinish();
-};
-
-
-
10.11.1.1. Associated types [exec.run_loop.types]
-
classrun-loop-scheduler;
-
-
-
-
run-loop-scheduler is an implementation defined type that models the scheduler concept.
-
-
Instances of run-loop-scheduler remain valid until the end of the lifetime of the run_loop instance from which they were obtained.
-
-
Two instances of run-loop-scheduler compare equal if and only if they were obtained from the same run_loop instance.
-
-
Let sch be an expression of type run-loop-scheduler. The expression execution::schedule(sch) is not potentially throwing and has type run-loop-sender.
-
-
classrun-loop-sender;
-
-
-
-
run-loop-sender is an implementation defined type that models the sender_of concept; i.e.,sender_of<run-loop-sender> is true. Additionally, the types reported by its error_types associated type is exception_ptr, and the value of its sends_stopped trait is true.
-
-
An instance of run-loop-sender remains valid until the end of the lifetime of its associated execution::run_loop instance.
-
-
Let s be an expression of type run-loop-sender, let r be an expression such that decltype(r) models the receiver_of concept, and let C be either set_value_t or set_stopped_t. Then:
-
-
-
The expression execution::connect(s,r) has type run-loop-opstate<decay_t<decltype(r)>> and is potentially throwing if and only if the initialiation of decay_t<decltype(r)> from r is potentially throwing.
-
-
The expression get_completion_scheduler<C>(s) is not potentially throwing, has type run-loop-scheduler, and compares equal to the run-loop-scheduler instance from which s was obtained.
-
-
-
template<receiver_ofR>// arguments are not associated entities ([lib.tmpl-heads])
- structrun-loop-opstate;
-
-
-
-
run-loop-opstate<R> inherits unambiguously from run-loop-opstate-base.
-
-
Let o be a non-const lvalue of type run-loop-opstate<R>, and let REC(o) be a non-const lvalue reference to an instance of type R that was initialized with the expression r passed to the invocation of execution::connect that returned o. Then:
-
-
-
The object to which REC(o) refers remains valid for the lifetime of the object to which o refers.
-
-
The type run-loop-opstate<R> overrides run-loop-opstate-base::execute() such that o.execute() is equivalent to the following:
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:
Alias template single-sender-value-type is defined as follows:
-
-
-
If value_types_of_t<S,E,Tuple,Variant> would have the form Variant<Tuple<T>>, then single-sender-value-type<S,E> is an alias for type T.
-
-
Otherwise, if value_types_of_t<S,E,Tuple,Variant> would have the form Variant<Tuple<>> or Variant<>, then single-sender-value-type<S,E> is an alias for type void.
-
-
Otherwise, single-sender-value-type<S,E> is ill-formed.
-
-
-
The type sender-awaitable<S,P> is equivalent to the following:
Let r be an rvalue expression of type awaitable-receiver, let cr be a const lvalue that refers to r, let v be an expression of type result_t, and let err be an arbitrary expression of type Err. Then:
-
-
-
If value_t is void, then execution::set_value(r) is expression-equivalent to (r.result_ptr_->emplace<1>(),r.continuation_.resume()); otherwise, execution::set_value(r,v) is expression-equivalent to (r.result_ptr_->emplace<1>(v),r.continuation_.resume()).
-
-
execution::set_error(r,e) is expression-equivalent to (r.result_ptr_->emplace<2>(AS_EXCEPT_PTR(err)),r.continuation_.resume()), where AS_EXCEPT_PTR(err) is:
-
-
-
err if decay_t<Err> names the same type as exception_ptr,
-
-
Otherwise, make_exception_ptr(system_error(err)) if decay_t<Err> names the same type as error_code,
-
-
Otherwise, make_exception_ptr(err).
-
-
-
execution::set_stopped(r) is expression-equivalent to static_cast<coroutine_handle<>>(r.continuation_.promise().unhandled_stopped()).resume().
-
-
tag_invoke(tag,cr,as...) is expression-equivalent to tag(as_const(cr.continuation_.promise()),as...) for any expression tag whose type satisfies forwarding-receiver-query and for any set of arguments as....
-
-
-
sender-awaitable::sender-awaitable(S&&s,P&p)
-
-
-
Effects: initializes state_ with connect((S&&)s,awaitable-receiver{&result_,coroutine_handle<P>::from_promise(p)}).
as_awaitable is a customization point object. For some subexpressions e and p where p is an lvalue, E names the type decltype((e)) and P names the type decltype((p)), as_awaitable(e,p) is expression-equivalent to the following:
-
-
-
tag_invoke(as_awaitable,e,p) if that expression is well-formed.
-
-
-
Mandates:is-awaitable<A> is true, where A is the type of the tag_invoke expression above.
-
-
-
Otherwise, e if is-awaitable<E> is true.
-
-
Otherwise, sender-awaitable{e,p} if awaitable-sender<E,P> is true.
with_awaitable_senders, when used as the base class of a coroutine promise type, makes senders awaitable in that coroutine type.
-
In addition, it provides a default implementation of unhandled_stopped() such that if a sender completes by calling execution::set_stopped, it is treated as if an uncatchable "stopped" exception were thrown from the await-expression. In practice, the coroutine is never resumed, and the unhandled_stopped of the coroutine caller’s promise type is called.
ISO/IEC JTC1/SC22/WG21 14882: Programming Language — C++
-
Audience:
-
SG1, LEWG
-
-
-
-
-
-
-
-
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 A Unified Executors Proposal for C++ 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 needs 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 of concurrency primitives (std::atomic, std::mutex, std::counting_semaphore, etc) and lower level building blocks (std::thread, etc), we lack a Standard vocabulary and framework for asynchrony and parallelism that C++ programmers desperately need. std::async/std::future/std::promise, 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.
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. A sender describes asynchronous work and sends a signal (value, error, or stopped) 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 std::invocable, and calls the std::invocable on the signal sent by the input sender. The sender returned by then sends the result of that invocation. In this case, the input sender came from schedule, so its void, meaning it won’t send us a value, so our std::invocable takes no parameters. But we return an int, which will be sent to the next recipient.
-
-
Now, we add another operation to the chain, again using § 4.21.2 execution::then. This time, we get sent a value - the int from the previous step. We add 42 to it, and then return the result.
-
-
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 std::optional<std::tuple<...>> with the value sent by the last sender, or an empty std::optional if the last sender sent a stopped signal, or it throws an exception if the last sender sent an error.
This example builds an asynchronous computation of an inclusive scan:
-
-
-
It scans a sequence of doubles (represented as the std::span<constdouble>input) and stores the result in another sequence of doubles (represented as std::span<double>output).
-
-
It takes a scheduler, which specifies what execution context the scan should be launched on.
-
-
It also takes a tile_count parameter that controls the number of execution agents that will be spawned.
-
-
First we need to allocate temporary storage needed for the algorithm, which we’ll do with a std::vector, partials. We need one double of temporary storage for each execution agent we create.
-
-
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 sch, which means the next item in the chain will use sch.
-
-
Senders and sender adaptors support composition via operator|, similar to C++ ranges. We’ll use operator| to attach the next piece of work, which will spawn tile_count execution agents using § 4.21.9 execution::bulk (see § 4.13 Most sender adaptors are pipeable for details).
-
-
Each agent will call a std::invocable, passing it two arguments. The first is the agent’s index (i) in the § 4.21.9 execution::bulk operation, in this case a unique integer in [0,tile_count). The second argument is what the input sender sent - the temporary storage.
-
-
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 std::inclusive_scan over our elements. We store the scan result for our last element, which is the sum of all of our elements, in our temporary storage 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 partials. 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.
-
-
§ 4.21.2 execution::then takes an input sender and an std::invocable and calls the std::invocable with the value sent by the input sender. Inside our std::invocable, we call std::inclusive_scan on partials, which the input senders will send to us.
-
-
Then we return partials, which the next phase will need.
-
-
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 partials to integrate the sums from other tiles into our elements, completing the inclusive scan.
-
-
async_inclusive_scan returns a sender that sends the output std::span<double>. A consumer of the algorithm can chain additional work that uses the scan result. At the point at which async_inclusive_scan returns, the computation may not have completed. In fact, it may not have even started.
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:
-
-
-
async_read 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 a std::span<std::byte>, and a handle to an I/O context. It will asynchronously read into the input buffer, up to the size of the std::span. It returns a sender which will send the number of bytes read once the read completes.
-
-
async_read_array 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 a dynamic_buffer object that owns the data that was sent.
-
-
dynamic_buffer is an aggregate struct that contains a std::unique_ptr<std::byte[]> and a size.
-
-
The first thing we do inside of async_read_array is create a sender that will send a new, empty dynamic_array object using § 4.20.2 execution::just. We can attach more work to the pipeline using operator| composition (see § 4.13 Most sender adaptors are pipeable for details).
-
-
We need the lifetime of this dynamic_array object to last for the entire pipeline. So, we use let_value, which takes an input sender and a std::invocable that must return a sender itself (see § 4.21.4 execution::let_* for details). let_value sends the value from the input sender to the std::invocable. Critically, the lifetime of the sent object will last until the sender returned by the std::invocable completes.
-
-
Inside of the let_valuestd::invocable, we have the rest of our logic. First, we want to initiate an async_read of the buffer size. To do that, we need to send a std::span pointing to buf.size. We can do that with § 4.20.2 execution::just.
Next, we pipe a std::invocable that will be invoked after the async_read completes using § 4.21.2 execution::then.
-
-
That std::invocable gets sent the number of bytes read.
-
-
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 std::span<std::byte> to the storage for the data from the std::invocable. This will be sent to the next recipient in the pipeline.
-
-
And that recipient will be another async_read, which will read the data.
-
-
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 dynamic_buffer object. It will get sent by the sender returned by async_read_array. We can attach more things to that sender to use the data in the buffer.
-
-
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 async_recv() operation for a Windows Socket.
-
structoperation_base:WSAOVERALAPPED{
- usingcompletion_fn=void(operation_base*op,DWORDbytesTransferred,interrorCode)noexcept;
-
- // Assume IOCP event loop will call this when this OVERLAPPED structure is dequeued.
- completion_fn*completed;
-};
-
-template<typenameReceiver>
-structrecv_op:operation_base{
- recv_op(SOCKETs,void*data,size_tlen,Receiverr)
- :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);
- }
-
- friendvoidtag_invoke(std::tag_t<std::execution::start>,recv_op&self)noexcept{
- // Avoid even calling WSARecv() if operation already cancelled
- autost=std::execution::get_stop_token(
- std::execution::get_env(self.receiver));
- if(st.stop_requested()){
- std::execution::set_stopped(std::move(self.receiver));
- return;
- }
-
- // Store and cache result here in case it changes during execution
- constboolstopPossible=st.stop_possible();
- if(!stopPossible){
- self.ready.store(true,std::memory_order_relaxed);
- }
-
- // Launch the operation
- DWORDbytesTransferred=0;
- DWORDflags=0;
- intresult=WSARecv(self.sock,&self.buffer,1,&bytesTransferred,&flags,
- static_cast<WSAOVERLAPPED*>(&self), NULL);
- if(result==SOCKET_ERROR){
- interrorCode=WSAGetLastError();
- if(errorCode!=WSA_IO_PENDING)){
- if(errorCode==WSA_OPERATION_ABORTED){
- std::execution::set_stopped(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();
-
- BOOLok=WSAGetOverlappedResult(self.sock,(WSAOVERLAPPED*)&self,&bytesTransferred,FALSE,&flags);
- if(ok){
- std::execution::set_value(std::move(self.receiver),bytesTransferred);
- }else{
- interrorCode=WSAGetLastError();
- std::execution::set_error(std::move(self.receiver),
- std::error_code(errorCode,std::system_category()));
- }
- }
- }
- }
-
- structcancel_cb{
- recv_op&op;
-
- voidoperator()()noexcept{
- CancelIoEx((HANDLE)op.sock,(OVERLAPPED*)(WSAOVERLAPPED*)&op);
- }
- };
-
- staticvoidon_complete(operation_base*op,DWORDbytesTransferred,interrorCode)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()));
- }
- }
- }
-
- Receiverreceiver;
- SOCKETsock;
- WSABUFbuffer;
- std::optional<typenamestop_callback_type_t<Receiver>
- ::templatecallback_type<cancel_cb>>stopCallback;
- std::atomic<bool>ready{false};
-};
-
-structrecv_sender{
- SOCKETsock;
- void*data;
- size_tlen;
-
- template<typenameReceiver>
- friendrecv_op<Receiver>tag_invoke(std::tag_t<std::execution::connect>
- constrecv_sender&s,
- Receiverr){
- returnrecv_op<Receiver>{s.sock,s.data,s.len,std::move(r)};
- }
-};
-
-recv_senderasync_recv(SOCKETs,void*data,size_tlen){
- returnrecv_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.
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.
In this code, NN::async_read_some and NN::async_write_some are asynchronous socket-based networking APIs that return senders. EX::repeat_effect_until, EX::let_value, and EX::then 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 repeat_effect_until 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.
This code builds a then 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 (set_error and set_stopped), as well as all receiver queries,
-are passed through unchanged.
-
In detail, it does the following:
-
-
-
Defines a receiver in terms of execution::receiver_adaptor that aggregates
-another receiver and an invocable that:
-
-
-
Defines a constrained tag_invoke overload for transforming the value
-channel.
-
-
Defines another constrained overload of tag_invoke that passes all other
-customizations through unchanged.
-
-
The tag_invoke overloads are actually implemented by execution::receiver_adaptor; they dispatch either to named members, as
-shown above with _then_receiver::set_value, or to the adapted receiver.
-
-
Defines a sender that aggregates another sender and the invocable, which defines a tag_invoke customization for std::execution::connect that wraps the incoming receiver in the receiver from (1) and passes it and the incoming sender to std::execution::connect, returning the result. It also defines a tag_invoke customization of get_completion_signatures that declares the sender’s completion signatures when executed within a particular environment.
-
-
1.5.2. retry
-
usingnamespacestd;
-namespaceexec=execution;
-
-template<classFrom,classTo>
-using_decays_to=same_as<decay_t<From>,To>;
-
-// _conv needed so we can emplace construct non-movable types into
-// a std::optional.
-template<invocableF>
- requiresis_nothrow_move_constructible_v<F>
-struct_conv{
- Ff_;
- explicit_conv(Ff)noexcept:f_((F&&)f){}
- operatorinvoke_result_t<F>()&&{
- return((F&&)f_)();
- }
-};
-
-template<classS,classR>
-struct_op;
-
-// pass through all customizations except set_error, which retries the operation.
-template<classS,classR>
-struct_retry_receiver
- :exec::receiver_adaptor<_retry_receiver<S,R>>{
- _op<S,R>*o_;
-
- R&&base()&&noexcept{return(R&&)o_->r_;}
- constR&base()const&noexcept{returno_->r_;}
-
- explicit_retry_receiver(_op<S,R>*o):o_(o){}
-
- voidset_error(auto&&)&&noexcept{
- o_->_retry();// This causes the op to be retried
- }
-};
-
-// Hold the nested operation state in an optional so we can
-// re-construct and re-start it if the operation fails.
-template<classS,classR>
-struct_op{
- Ss_;
- Rr_;
- optional<
- exec::connect_result_t<S&,_retry_receiver<S,R>>>o_;
-
- _op(Ss,Rr):s_((S&&)s),r_((R&&)r),o_{_connect()}{}
- _op(_op&&)=delete;
-
- auto_connect()noexcept{
- return_conv{[this]{
- returnexec::connect(s_,_retry_receiver<S,R>{this});
- }};
- }
- void_retry()noexcepttry{
- o_.emplace(_connect());// potentially throwing
- exec::start(*o_);
- }catch(...){
- exec::set_error((R&&)r_,std::current_exception());
- }
- friendvoidtag_invoke(exec::start_t,_op&o)noexcept{
- exec::start(*o.o_);
- }
-};
-
-template<classS>
-struct_retry_sender{
- Ss_;
- explicit_retry_sender(Ss):s_((S&&)s){}
-
- template<class...Ts>
- using_value_t=
- exec::completion_signatures<exec::set_value_t(Ts...)>;
- template<class>
- using_error_t=exec::completion_signatures<>;
-
- // Declare the signatures with which this sender can complete
- template<classEnv>
- friendautotag_invoke(exec::get_completion_signatures_t,const_retry_sender&,Env)
- ->exec::make_completion_signatures<S&,Env,
- exec::completion_signatures<exec::set_error_t(std::exception_ptr)>,
- _value_t,_error_t>;
-
- template<exec::receiverR>
- friend_op<S,R>tag_invoke(exec::connect_t,_retry_sender&&self,Rr){
- return{(S&&)self.s_,(R&&)r};
- }
-};
-
-template<exec::senderS>
-exec::senderautoretry(Ss){
- return_retry_sender{(S&&)s};
-}
-
-
The retry algorithm takes a multi-shot sender and causes it to repeat on error, passing
-through values and stopped signals. Each time the input sender is restarted, a new receiver
-is connected and the resulting operation state is stored in an optional, which allows us
-to reinitialize it multiple times.
-
This example does the following:
-
-
-
Defines a _conv utility that takes advantage of C++17’s guaranteed copy elision to
-emplace a non-movable type in a std::optional.
-
-
Defines a _retry_receiver 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 set_error, which causes a _retry() function to be called instead.
-
-
Defines an operation state that aggregates the input sender and receiver, and declares
-storage for the nested operation state in an optional. Constructing the operation
-state constructs a _retry_receiver with a pointer to the (under construction) operation
-state and uses it to connect to the aggregated sender.
-
-
Starting the operation state dispatches to start on the inner operation state.
-
-
The _retry() 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.
-
-
After reinitializing the inner operation state, _retry() calls start on it, causing
-the failed operation to be rescheduled.
-
-
Defines a _retry_sender that implements the connect customization point to return
-an operation state constructed from the passed-in sender and receiver.
-
-
_retry_sender also implements the get_completion_signatures customization point to describe the ways this sender may complete when executed in a particular execution context.
-
-
1.6. Examples: Schedulers
-
In this section we look at some schedulers of varying complexity.
The inline scheduler is a trivial scheduler that completes immediately and synchronously on
-the thread that calls std::execution::start on the operation state produced by its sender.
-In other words, start(connect(schedule(inline-scheduler),receiver)) is
-just a fancy way of saying set_value(receiver), with the exception of the fact that start wants to be passed an lvalue.
-
Although not a particularly useful scheduler, it serves to illustrate the basics of
-implementing one. The inline_scheduler:
-
-
-
Customizes execution::schedule to return an instance of the sender type _sender.
-
-
The _sender type models the sender concept and provides the metadata
-needed to describe it as a sender of no values that can send an exception_ptr as an error and that never calls set_stopped. This
-metadata is provided with the help of the execution::completion_signatures utility.
-
-
The _sender type customizes execution::connect to accept a receiver of no
-values. It returns an instance of type _op that holds the receiver by
-value.
-
-
The operation state customizes std::execution::start to call std::execution::set_value on the receiver, passing any exceptions to std::execution::set_error as an 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 std::execution::run_loop.
The single_thread_context 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 execution::run_loop 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 this_thread::sync_wait, 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. Examples: Server theme
-
In this section we look at some examples of how one would use senders to implement an HTTP server. The examples ignore the low-level details of the HTTP server and looks at how senders can be combined to achieve the goals of the project.
-
General application context:
-
-
-
server application that processes images
-
-
execution contexts:
-
-
-
1 dedicated thread for network I/O
-
-
N worker threads used for CPU-intensive work
-
-
M threads for auxiliary I/O
-
-
optional GPU context that may be used on some types of servers
-
-
-
all parts of the applications can be asynchronous
-
-
no locks shall be used in user code
-
-
1.7.1. Composability with execution::let_*
-
Example context:
-
-
-
we are looking at the flow of processing an HTTP request and sending back the response
-
-
show how one can break the (slightly complex) flow into steps with execution::let_* functions
-
-
different phases of processing HTTP requests are broken down into separate concerns
-
-
each part of the processing might use different execution contexts (details not shown in this example)
-
-
error handling is generic, regardless which component fails; we always send the right response to the clients
-
-
Goals:
-
-
-
show how one can break more complex flows into steps with let_* functions
-
-
exemplify the use of let_value, let_error, let_stopped, and just algorithms
-
-
namespaceex=std::execution;
-
-// Returns a sender that yields an http_request object for an incoming request
-ex::senderautoschedule_request_start(read_requests_ctxctx){...}
-// Sends a response back to the client; yields a void signal on success
-ex::senderautosend_response(consthttp_response&resp){...}
-// Validate that the HTTP request is well-formed; forwards the request on success
-ex::senderautovalidate_request(consthttp_request&req){...}
-
-// Handle the request; main application logic
-ex::senderautohandle_request(consthttp_request&req){
- //...
- returnex::just(http_response{200,result_body});
-}
-
-// Transforms server errors into responses to be sent to the client
-ex::senderautoerror_to_response(std::exception_ptrerr){
- try{
- std::rethrow_exception(err);
- }catch(conststd::invalid_argument&e){
- returnex::just(http_response{404,e.what()});
- }catch(conststd::exception&e){
- returnex::just(http_response{500,e.what()});
- }catch(...){
- returnex::just(http_response{500,"Unknown server error"});
- }
-}
-// Transforms cancellation of the server into responses to be sent to the client
-ex::senderautostopped_to_response(){
- returnex::just(http_response{503,"Service temporarily unavailable"});
-}
-//...
-// The whole flow for transforming incoming requests into responses
-ex::senderautosnd=
- // get a sender when a new request comes
- schedule_request_start(the_read_requests_ctx)
- // make sure the request is valid; throw if not
- |ex::let_value(validate_request)
- // process the request in a function that may be using a different execution context
- |ex::let_value(handle_request)
- // If there are errors transform them into proper responses
- |ex::let_error(error_to_response)
- // If the flow is cancelled, send back a proper response
- |ex::let_stopped(stopped_to_response)
- // write the result back to the client
- |ex::let_value(send_response)
- // done
- ;
-// execute the whole flow asynchronously
-ex::start_detached(std::move(snd));
-
- The example shows how one can separate out the concerns for interpreting requests, validating requests, running the main logic for handling the request, generating error responses, handling cancellation and sending the response back to the client.
-They are all different phases in the application, and can be joined together with the let_* functions.
-
All our functions return execution::sender objects, so that they can all generate success, failure and cancellation paths.
-For example, regardless where an error is generated (reading request, validating request or handling the response), we would have one common block to handle the error, and following error flows is easy.
-
Also, because of using execution::sender objects at any step, we might expect any of these steps to be completely asynchronous; the overall flow doesn’t care.
-Regardless of the execution context in which the steps, or part of the steps are executed in, the flow is still the same.
-
1.7.2. Moving between execution contexts with execution::on and execution::transfer
-
Example context:
-
-
-
reading data from the socket before processing the request
-
-
reading of the data is done on the I/O context
-
-
no processing of the data needs to be done on the I/O context
-
-
Goals:
-
-
-
show how one can change the execution context
-
-
exemplify the use of on and transfer algorithms
-
-
namespaceex=std::execution;
-
-size_tlegacy_read_from_socket(intsock,char*buffer,size_tbuffer_len){}
-voidprocess_read_data(constchar*read_data,size_tread_len){}
-//...
-
-// A sender that just calls the legacy read function
-autosnd_read=ex::just(sock,buf,buf_len)|ex::then(legacy_read_from_socket);
-// The entire flow
-autosnd=
- // start by reading data on the I/O thread
- ex::on(io_sched,std::move(snd_read))
- // do the processing on the worker threads pool
- |ex::transfer(work_sched)
- // process the incoming data (on worker threads)
- |ex::then([buf](intread_len){process_read_data(buf,read_len);})
- // done
- ;
-// execute the whole flow asynchronously
-ex::start_detached(std::move(snd));
-
-
The example assume that we need to wrap some legacy code of reading sockets, and handle execution context switching.
-(This style of reading from socket may not be the most efficient one, but it’s working for our purposes.)
-For performance reasons, the reading from the socket needs to be done on the I/O thread, and all the processing needs to happen on a work-specific execution context (i.e., thread pool).
-
Calling execution::on will ensure that the given sender will be started on the given scheduler.
-In our example, snd_read is going to be started on the I/O scheduler.
-This sender will just call the legacy code.
-
The completion signal will be issued in the I/O execution context, so we have to move it to the work thread pool.
-This is achieved with the help of the execution::transfer algorithm.
-The rest of the processing (in our case, the last call to then) will happen in the work thread pool.
-
The reader should notice the difference between execution::on and execution::transfer.
-The execution::on algorithm will ensure that the given sender will start in the specified context, and doesn’t care where the completion signal for that sender is sent.
-The execution::transfer algorithm will not care where the given sender is going to be started, but will ensure that the completion signal of will be transferred to the given context.
-
1.8. What this proposal is not
-
This paper is not a patch on top of A Unified Executors Proposal for C++; 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 A Unified Executors Proposal for C++; 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.9. Design changes from P0443
-
-
-
The executor concept has been removed and all of its proposed functionality
-is now based on schedulers and senders, as per SG1 direction.
-
-
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.
This paper extends the sender traits/typed sender design to support typed
-senders whose value/error types depend on type information provided late via
-the receiver.
-
-
Support for untyped senders is dropped; the typed_sender concept is renamed sender; sender_traits is replaced with completion_signatures_of_t.
-
-
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.
-
-
Some additional utilities are added:
-
-
-
run_loop: An execution context that provides a multi-producer,
-single-consumer, first-in-first-out work queue.
-
-
receiver_adaptor: A utility for algorithm authors for defining one
-receiver type in terms of another.
-
-
completion_signatures and make_completion_signatures:
-Utilities for describing the ways in which a sender can complete in a
-declarative syntax.
-
-
-
1.10. 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.10.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.10.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 Halo: coroutine Heap Allocation eLision Optimization: the joint response, 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.10.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.11. Field experience
-
1.11.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 (with slight variations -- the design of P2300 has evolved due to LEWG feedback), many of this paper’s algorithms (some under different names), and much more besides.
-
Libunifex has several concrete schedulers in addition to the run_loop suggested here (where it is called manual_event_loop). 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.11.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 Simberg 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 let_value are really nice additions.
-
-
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 f2 in the continuation will always be ready here f1.then([](future<T>f2){...}) is enough of a reason to separate the channels. All the other obvious reasons apply as well of course.
-
-
For futures we have a thing called hpx::dataflow which is an optimized version of when_all(...).then(...) which avoids intermediate allocations. With the sender/receiver when_all(...)|then(...) we get that "for free".
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 execution::start_detached, but otherwise implements the concepts, customization points, traits, queries, coroutine integration, sender factories, pipe support, execution::receiver_adaptor, and execution::run_loop.
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.11.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:
start_detached requires its argument to be a void sender (sends no values
-to set_value).
-
-
Enhancements:
-
-
-
Receiver concepts refactored to no longer require an error channel for exception_ptr or a stopped channel.
-
-
sender_of concept and connect customization point additionally require
-that the receiver is capable of receiving all of the sender’s possible
-completions.
-
-
get_completion_signatures is now required to return an instance of either completion_signatures or dependent_completion_signatures.
-
-
make_completion_signatures made more general.
-
-
receiver_adaptor handles get_env as it does the set_* members; that is, receiver_adaptor will look for a member named get_env() in the derived
-class, and if found dispatch the get_env_t tag invoke customization to it.
-
-
just, just_error, just_stopped, and into_variant have been respecified
-as customization point objects instead of functions, following LEWG guidance.
-
-
2.2. R4
-
The changes since R3 are as follows:
-
Fixes:
-
-
-
Fix specification of get_completion_scheduler on the transfer, schedule_from and transfer_when_all algorithms; the completion scheduler cannot be guaranteed
-for set_error.
-
-
The value of sends_stopped for the default sender traits of types that are
-generally awaitable was changed from false to true to acknowledge the
-fact that some coroutine types are generally awaitable and may implement the unhandled_stopped() protocol in their promise types.
-
-
Fix the incorrect use of inline namespaces in the <execution> header.
-
-
Shorten the stable names for the sections.
-
-
sync_wait now handles std::error_code specially by throwing a std::system_error on failure.
-
-
Fix how ADL isolation from class template arguments is specified so it
-doesn’t constrain implmentations.
-
-
Properly expose the tag types in the header <execution> synopsis.
-
-
Enhancements:
-
-
-
Support for "dependently-typed" senders, where the completion signatures -- and
-thus the sender metadata -- depend on the type of the receiver connected
-to it. See the section dependently-typed
-senders below for more information.
-
-
Add a read(query) sender factory for issuing a query
-against a receiver and sending the result through the value channel. (This is
-a useful instance of a dependently-typed sender.)
-
-
Add completion_signatures utility for declaratively defining a typed
-sender’s metadata and a make_completion_signatures utility for adapting
-another sender’s completions in helpful ways.
-
-
Add make_completion_signatures utility for specifying a sender’s completion
-signatures by adapting those of another sender.
-
-
Drop support for untyped senders and rename typed_sender to sender.
-
-
set_done is renamed to set_stopped. All occurances of "done" in
-indentifiers replaced with "stopped"
-
-
Add customization points for controlling the forwarding of scheduler,
-sender, receiver, and environment queries through layers of adaptors;
-specify the behavior of the standard adaptors in terms of the new
-customization points.
-
-
Add get_delegatee_scheduler query to forward a scheduler that can be used
-by algorithms or by the scheduler to delegate work and forward progress.
-
-
Add schedule_result_t alias template.
-
-
More precisely specify the sender algorithms, including precisely what their
-completion signatures are.
-
-
stopped_as_error respecified as a customization point object.
-
-
tag_invoke respecified to improve diagnostics.
-
-
2.2.1. Dependently-typed senders
-
Background:
-
In the sender/receiver model, as with coroutines, contextual information about
-the current execution is most naturally propagated from the consumer to the
-producer. In coroutines, that means information like stop tokens, allocators and
-schedulers are propagated from the calling coroutine to the callee. In
-sender/receiver, that means that that contextual information is associated with
-the receiver and is queried by the sender and/or operation state after the
-sender and the receiver are connect-ed.
-
Problem:
-
The implication of the above is that the sender alone does not have all the
-information about the async computation it will ultimately initiate; some of
-that information is provided late via the receiver. However, the sender_traits mechanism, by which an algorithm can introspect the value and error types the
-sender will propagate, only accepts a sender parameter. It does not take into
-consideration the type information that will come in late via the receiver. The
-effect of this is that some senders cannot be typed senders when they
-otherwise could be.
-
Example:
-
To get concrete, consider the case of the "get_scheduler()" sender: when connect-ed and start-ed, it queries the receiver for its associated
-scheduler and passes it back to the receiver through the value channel. That
-sender’s "value type" is the type of the receiver’s scheduler. What then
-should sender_traits<get_scheduler_sender>::value_types report for the get_scheduler()'s value type? It can’t answer because it doesn’t know.
-
This causes knock-on problems since some important algorithms require a typed
-sender, such as sync_wait. To illustrate the problem, consider the following
-code:
-
namespaceex=std::execution;
-
-ex::senderautotask=
- ex::let_value(
- ex::get_scheduler(),// Fetches scheduler from receiver.
- [](autocurrent_sched){
- // Lauch some nested work on the current scheduler:
- returnex::on(current_sched,nestedwork...);
- });
-
-std::this_thread::sync_wait(std::move(task));
-
-
The code above is attempting to schedule some work onto the sync_wait's run_loop execution context. But let_value only returns a typed sender when
-the input sender is typed. As we explained above, get_scheduler() is not
-typed, so task is likewise not typed. Since task isn’t typed, it cannot be
-passed to sync_wait which is expecting a typed sender. The above code would
-fail to compile.
-
Solution:
-
The solution is conceptually quite simple: extend the sender_traits mechanism
-to optionally accept a receiver in addition to the sender. The algorithms can
-use sender_traits<Sender,Receiver> to inspect the
-async operation’s completion signals. The typed_sender concept would also need
-to take an optional receiver parameter. This is the simplest change, and it
-would solve the immediate problem.
-
Design:
-
Using the receiver type to compute the sender traits turns out to have pitfalls
-in practice. Many receivers make use of that type information in their
-implementation. It is very easy to create cycles in the type system, leading to
-inscrutible errors. The design pursued in R4 is to give receivers an associated environment object -- a bag of key/value pairs -- and to move the contextual
-information (schedulers, etc) out of the receiver and into the environment. The sender_traits template and the typed_sender concept, rather than taking a
-receiver, take an environment. This is a much more robust design.
-
A further refinement of this design would be to separate the receiver and the
-environment entirely, passing then as separate arguments along with the sender to connect. This paper does not propose that change.
-
Impact:
-
This change, apart from increasing the expressive power of the sender/receiver abstraction, has the following impact:
-
-
-
Typed senders become moderately more challenging to write. (The new completion_signatures and make_completion_signatures utilities are added
-to ease this extra burden.)
-
-
Sender adaptor algorithms that previously constrained their sender arguments
-to satisfy the typed_sender concept can no longer do so as the receiver is
-not available yet. This can result in type-checking that is done later, when connect is ultimately called on the resulting sender adaptor.
-
-
Operation states that own receivers that add to or change the environment
-are typically larger by one pointer. It comes with the benefit of far fewer
-indirections to evaluate queries.
-
-
"Has it been implemented?"
-
Yes, the reference implementation, which can be found at
-https://github.com/brycelelbach/wg21_p2300_std_execution, has implemented this
-design as well as some dependently-typed senders to confirm that it works.
-
Implementation experience
-
Although this change has not yet been made in libunifex, the most widely adopted sender/receiver implementation, a similar design can be found in Folly’s coroutine support library. In Folly.Coro, it is possible to await a special awaitable to obtain the current coroutine’s associated scheduler (called an executor in Folly).
-
For instance, the following Folly code grabs the current executor, schedules a task for execution on that executor, and starts the resulting (scheduled) task by enqueueing it for execution.
-
// From Facebook’s Folly open source library:
-template<classT>
-folly::coro::Task<void>CancellableAsyncScope::co_schedule(folly::coro::Task<T>&&task){
- this->add(std::move(task).scheduleOn(co_awaitco_current_executor));
- co_return;
-}
-
-
Facebook relies heavily on this pattern in its coroutine code. But as described
-above, this pattern doesn’t work with R3 of std::execution because of the lack
-of dependently-typed schedulers. The change to sender_traits in R4 rectifies that.
-
Why now?
-
The authors are loathe to make any changes to the design, however small, at this
-stage of the C++23 release cycle. But we feel that, for a relatively minor
-design change -- adding an extra template parameter to sender_traits and typed_sender -- the returns are large enough to justify the change. And there
-is no better time to make this change than as early as possible.
-
One might wonder why this missing feature not been added to sender/receiver
-before now. The designers of sender/receiver have long been aware of the need.
-What was missing was a clean, robust, and simple design for the change, which we
-now have.
-
Drive-by:
-
We took the opportunity to make an additional drive-by change: Rather than
-providing the sender traits via a class template for users to specialize, we
-changed it into a sender query: get_completion_signatures(sender,env). That function’s return type is used as the sender’s traits.
-The authors feel this leads to a more uniform design and gives sender authors a
-straightforward way to make the value/error types dependent on the cv- and
-ref-qualification of the sender if need be.
-
Details:
-
Below are the salient parts of the new support for dependently-typed senders in
-R4:
-
-
-
Receiver queries have been moved from the receiver into a separate environment
-object.
-
-
Receivers have an associated environment. The new get_env CPO retrieves a
-receiver’s environment. If a receiver doesn’t implement get_env, it returns
-an unspecified "empty" environment -- an empty struct.
-
-
sender_traits now takes an optional Env parameter that is used to
-determine the error/value types.
-
-
The primary sender_traits template is replaced with a completion_signatures_of_t alias implemented in terms of a new get_completion_signatures CPO that dispatches
-with tag_invoke. get_completion_signatures takes a sender and an optional
-environment. A sender can customize this to specify its value/error types.
-
-
Support for untyped senders is dropped. The typed_sender concept has been
-renamed to sender and now takes an optional environment.
-
-
The environment argument to the sender concept and the get_completion_signatures CPO defaults to no_env. All environment queries fail (are ill-formed) when
-passed an instance of no_env.
-
-
A type S is required to satisfy sender<S> to be
-considered a sender. If it doesn’t know what types it will complete with
-independent of an environment, it returns an instance of the placeholder
-traits dependent_completion_signatures.
-
-
If a sender satisfies both sender<S> and sender<S,Env>, then the completion signatures
-for the two cannot be different in any way. It is possible for an
-implementation to enforce this statically, but not required.
-
-
All of the algorithms and examples have been updated to work with
-dependently-typed senders.
-
-
2.3. R3
-
The changes since R2 are as follows:
-
Fixes:
-
-
-
Fix specification of the on algorithm to clarify lifetimes of
-intermediate operation states and properly scope the get_scheduler query.
-
-
Fix a memory safety bug in the implementation of connect-awaitable.
-
-
Fix recursive definition of the scheduler concept.
-
-
Enhancements:
-
-
-
Add run_loop execution context.
-
-
Add receiver_adaptor utility to simplify writing receivers.
-
-
Require a scheduler’s sender to model sender_of and provide a completion scheduler.
-
-
Specify the cancellation scope of the when_all algorithm.
-
-
Make as_awaitable a customization point.
-
-
Change connect's handling of awaitables to consider those types that are awaitable owing to customization of as_awaitable.
-
-
Add value_types_of_t and error_types_of_t alias templates; rename stop_token_type_t to stop_token_of_t.
-
-
Add a design rationale for the removal of the possibly eager algorithms.
-
-
Expand the section on field experience.
-
-
2.4. R2
-
The changes since R1 are as follows:
-
-
-
Remove the eagerly executing sender algorithms.
-
-
Extend the execution::connect customization point and the sender_traits<> template to recognize awaitables as typed_senders.
-
-
Add utilities as_awaitable() and with_awaitable_senders<> so a coroutine type can trivially make senders awaitable with a coroutine.
-
-
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 bulk algorithm.
-
-
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 execution::just_error and execution::just_stopped.
-
-
Add sender adaptors execution::stopped_as_optional and execution::stopped_as_error.
-
-
Document more production uses of sender/receiver at scale.
-
-
Various fixes of typos and bugs.
-
-
2.5. 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 unschedule adaptor.
-
-
Various fixes of typos and bugs.
-
-
2.6. R0
-
Initial revision.
-
3. Design - introduction
-
The following three 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 A Unified Executors Proposal for C++: std::execution; however, for brevity, the std:: part of this name is omitted. When you see execution::foo, treat that as std::execution::foo.
-
-
Universal references and explicit calls to std::move/std::forward are omitted in code samples and signatures for simplicity; assume universal references and perfect forwarding unless stated otherwise.
-
-
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 std::invocable 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 std::invocable 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, schedule, 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::schedulerautosch=thread_pool.scheduler();
-execution::senderautosnd=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 execution::schedule API.
-
Future papers will propose additional scheduler concepts that extend scheduler to add other capabilities. For example:
-
-
-
A time_scheduler concept that extends scheduler to support time-based scheduling.
-Such a concept might provide access to schedule_after(sched,duration), schedule_at(sched,time_point) and now(sched) APIs.
-
-
Concepts that extend scheduler to support opening, reading and writing files asynchronously.
-
-
Concepts that extend scheduler to support connecting, sending data and receiving data over the network asynchronously.
-
-
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.
execution::schedulerautosch=thread_pool.scheduler();
-execution::senderautosnd=execution::schedule(sch);
-execution::senderautocont=execution::then(snd,[]{
- std::fstreamfile{"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 execution::connect) senders and return a sender.
-
-
sender consumers, algorithms that take (and potentially execution::connect) senders and do not return a sender.
-
-
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.
-
A Unified Executors Proposal for C++ 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 (Towards C++23 executors: A proposal for an initial set of algorithms) 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 std::thread (which they aren’t).
-
-
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 on sender algorithm from Towards C++23 executors: A proposal for an initial set of algorithms, 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 stopped) 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
-
get_completion_scheduler is a query that retrieves the completion scheduler for a specific completion signal from a sender.
-Calling get_completion_scheduler 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::schedulerautocpu_sched=new_thread_scheduler{};
-execution::schedulerautogpu_sched=cuda::scheduler();
-
-execution::senderautosnd0=execution::schedule(cpu_sched);
-execution::schedulerautocompletion_sch0=
- execution::get_completion_scheduler<execution::set_value_t>(snd0);
-// completion_sch0 is equivalent to cpu_sched
-
-execution::senderautosnd1=execution::then(snd0,[]{
- std::cout<<"I am running on cpu_sched!\n";
-});
-execution::schedulerautocompletion_sch1=
- execution::get_completion_scheduler<execution::set_value_t>(snd1);
-// completion_sch1 is equivalent to cpu_sched
-
-execution::senderautosnd2=execution::transfer(snd1,gpu_sched);
-execution::senderautosnd3=execution::then(snd2,[]{
- std::cout<<"I am running on gpu_sched!\n";
-});
-execution::schedulerautocompletion_sch3=
- execution::get_completion_scheduler<execution::set_value_t>(snd3);
-// completion_sch3 is equivalent to gpu_sched
-
-
4.6. Execution context transitions are explicit
-
A Unified Executors Proposal for C++ 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 execution::schedule, 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 execution::transfer sender adaptor performs a transition from one execution context to another:
-
execution::schedulerautosch1=...;
-execution::schedulerautosch2=...;
-
-execution::senderautosnd1=execution::schedule(sch1);
-execution::senderautothen1=execution::then(snd1,[]{
- std::cout<<"I am running on sch1!\n";
-});
-
-execution::senderautosnd2=execution::transfer(then1,sch2);
-execution::senderautothen2=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 std::unique_ptr that it will be transferring ownership of to the
-operation-state returned by a call to execution::connect so that the operation has access to
-this resource. In such a sender, calling execution::connect 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 execution::connect only has overloads for an rvalue-qualified sender. Callers must pass the sender
-as an rvalue to the call to execution::connect, 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 execution::connect to accept an lvalue reference to the
-sender. Callers can indicate that they want the sender to remain valid after the call to execution::connect by passing an lvalue reference to the sender to call these overloads. Multi-shot senders should also define
-overloads of execution::connect 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 execution::connect.
-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 execution::connect. 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 execution::connect on the input sender, such as in this_thread::sync_wait or execution::start_detached.
-
Some multi-use sender algorithms may require that an input sender be copy-constructible but will only call execution::connect 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 execution::connect 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 split sender adaptor facilitates connecting to a sender multiple times, regardless of whether it is single-shot or multi-shot:
-
autosome_algorithm(execution::senderauto&&input){
- execution::senderautomulti_shot=split(input);
- // "multi_shot" is guaranteed to be multi-shot,
- // regardless of whether "input" was multi-shot or not
-
- returnwhen_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.
-
when_all 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. when_all returns a sender that also does not have an associated scheduler.
-
transfer_when_all 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 when_all. You can think of it as a composition of transfer(when_all(inputs...),scheduler), 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 execution::connect() before calling execution::start(), 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 when_all(ops...) algorithm should cancel other operations as soon as one operation fails
-
-
a first_successful(ops...) algorithm should cancel the other operations as soon as one operation completes successfuly
-
-
a generic timeout(src,duration) algorithm needs to be able to cancel the src operation after the timeout duration has elapsed.
-
-
a stop_when(src,trigger) algorithm should cancel src if trigger completes first and cancel trigger if src completes first
-
-
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:
In this example, if we take the operation returned by query_server_b(query), this operation will
-receive a stop-request when any of the following happens:
-
-
-
first_successful algorithm will send a stop-request if query_server_a(query) completes successfully
-
-
when_all algorithm will send a stop-request if the load_file("some_file.jpg") operation completes with an error or stopped result.
-
-
timeout algorithm will send a stop-request if the operation does not complete within 5 seconds.
-
-
stop_when algorithm will send a stop-request if the user clicks on the "Cancel" button in the user-interface.
-
-
The parent operation consuming the composed_cancellation_example() sends a stop-request
-
-
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.
At a high-level, the facilities proposed by this paper for supporting cancellation include:
-
-
-
Add std::stoppable_token and std::stoppable_token_for concepts that generalise the interface of std::stop_token type to allow other types with different implementation strategies.
-
-
Add std::unstoppable_token concept for detecting whether a stoppable_token can never receive a stop-request.
-
-
Add std::in_place_stop_token, std::in_place_stop_source and std::in_place_stop_callback<CB> types that provide a more efficient implementation of a stop-token for use in structured concurrency situations.
-
-
Add std::never_stop_token for use in places where you never want to issue a stop-request
-
-
Add std::execution::get_stop_token() CPO for querying the stop-token to use for an operation from its receiver’s execution environment.
-
-
Add std::execution::stop_token_of_t<T> for querying the type of a stop-token returned from 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 execution::get_stop_token() CPO.
-This CPO takes a single parameter, which is the execution environment of the receiver passed to execution::connect, and returns a std::stoppable_token that the operation can use to check for stop-requests for that operation.
-
As the caller of execution::connect typically has control over the receiver
-type it passes, it is able to customise the execution::get_env() CPO for that
-receiver to return an execution environment that hooks the execution::get_stop_token() CPO to return a stop-token that the receiver 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’s execution environment does not customise the execution::get_stop_token() CPO then invoking the CPO on that receiver’s
-environment will invoke the default implementation which returns std::never_stop_token. This is a special stoppable_token type that is
-statically known to always return false from the stop_possible() 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 execution::get_stop_token(), 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 execution::start() 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 execution::start() 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 execution::start() implementation.
-
An implementation of execution::start() 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 execution::start 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 execution::start() call (which is still executing).
This paper currently includes the design for cancellation as proposed in Composable cancellation for sender-based async operations - "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 ensure_started 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 execution::connect?" or similarly, "What happens if a call
-to execution::connect is made, but the returned operation state is destroyed
-before execution::start 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 when_all(eager_op_1(),eager_op_2()). Imagine eager_op_1() starts an
-asynchronous operation successfully, but then eager_op_2() throws. For
-lazy senders, that failure happens in the context of the when_all 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 before when_all has been called.
-
It then becomes the responsibility, not of the algorithm, but of the end
-user to handle the exception and ensure that eager_op_1() is joined before
-allowing the exception to propagate. If they fail to do that, they incur
-undefined behavior.
-
-
Detach from the computation - let the operation continue in the background -
-like an implicit call to std::thread::detach(). 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::shared_ptr, to ensure resources are kept alive until the operations
-complete, and can make clean shutdown nigh impossible.
-
-
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 std::terminate if an eager sender is destructed
-without joining it. This is the approach that std::thread destructor
-takes.
-
-
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 std::jthread uses for its
-destructor. It reduces the risk of deadlock but does not eliminate it.
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 execution::start 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 sequence that puts async operations in sequence by starting an operation only
-after the preceding one has completed. In an expression like sequence(a(),then(src,[]{b();}),c()), one my reasonably assume that a(), b() and c() 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.
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 execution::get_stop_token is an std::unstoppable_token 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, get_forward_progress_guarantee, which returns one of the enumerators of a new enum type, forward_progress_guarantee. Each enumerator of forward_progress_guarantee 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 operator|.
-This mechanism is similar to the operator| 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.
-
a|b will pass the sender a as the first argument to the pipeable sender adaptor b. Pipeable sender adaptors support partial application of the parameters after the first. For example, all of the following are equivalent:
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:
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.
-
-
-
execution::when_all and execution::when_all_with_variant: 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::on: 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 to 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 R that is a range of senders, consider the following in a coroutine that returns an async generator type:
This transforms each element of the asynchronous sequence R with the function fn on demand, as the data arrives. The result is a new asynchronous sequence of the transformed values.
-
Now imagine that R is the simple expression views::iota(0)|views::transform(execution::just). 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 [fn(0), fn(1), fn(2), ...].
-
Far more interesting would be if R 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:
- returnread_socket_async(socket,span{buff})
- // For success (partial and full), specify the next action:
- |execution::let_value([](error_codeerr,size_tbytes_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(autosnd:read_socket_async(socket,span{buff})){
- try{
- if(optional<size_t>bytes_read=
- co_awaitexecution::stopped_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 sender 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 await_transform trickery from a promise type to make them awaitable.)
-
For an example, imagine a coroutine type called task<T> that knows nothing
-about senders. It doesn’t implement any of the sender customization points.
-Despite that fact, and despite the fact that the this_thread::sync_wait algorithm is constrained with the sender concept, the following would compile
-and do what the user wants:
-
task<int>doSomeAsyncWork();
-
-intmain(){
- // OK, awaitable types satisfy the requirements for senders:
- autoo=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 execution::with_awaitable_senders 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 retry algorithm:
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 with_awaitable_senders 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 into_variant sender algorithm -- before co_await-ing that sender.
-
4.18. Cancellation of a sender can unwind a stack of coroutines
-
When looking at the sender-based retry 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 execution::set_stopped?
-
When your task type’s promise inherits from with_awaitable_senders, what happens is this: the coroutine behaves as if an uncatchable exception had been thrown from the co_await 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 with_awaitable_senders, or more generally implement a member function called unhandled_stopped, the exception unwinds the chain of coroutines as if an exception were thrown except that it bypasses catch(...) clauses.
-
In order to "catch" this uncatchable stopped exception, one of the calling coroutines in the stack would have to await a sender that maps the stopped channel into either a value or an error. That is achievable with the execution::let_stopped, execution::upon_stopped, execution::stopped_as_optional, or execution::stopped_as_error sender adaptors. For instance, we can use execution::stopped_as_optional to "catch" the stopped signal and map it into an empty optional as shown below:
-
if(autoopt=co_awaitexecution::stopped_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 connect-ing an awaitable and a receiver, the adaptation layer awaits the awaitable within a coroutine that implements unhandled_stopped in its promise type. The effect of this is that an "uncatchable" stopped exception propagates seamlessly out of awaitables, causing execution::set_stopped to be called on the receiver.
-
Obviously, unhandled_stopped is a library extension of the coroutine promise interface. Many promise types will not implement unhandled_stopped. When an uncatchable stopped exception tries to propagate through such a coroutine, it is treated as an unhandled exception and terminate is called. The solution, as described above, is to use a sender adaptor to handle the stopped exception before awaiting it. It goes without saying that any future Standard Library coroutine types ought to implement unhandled_stopped. The author of Add lazy coroutine (coroutine task) type, 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 execution::par, 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.
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 policy while using scheduler to create any work to be run. The expression may be ill-formed if scheduler 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 executing_on 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 void 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.
execution::schedulerautosch1=get_system_thread_pool().scheduler();
-
-execution::senderautosnd1=execution::schedule(sch1);
-// snd1 describes the creation of a new task on the system thread pool
-
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 set_value.
Returns a sender whose value completion scheduler is the provided scheduler, which sends the provided values in the same manner as just.
-
execution::senderautovals=execution::transfer_just(
- get_system_thread_pool().scheduler(),
- 1,2,3
-);
-execution::senderautosnd=execution::then(vals,[](auto...args){
- std::print(args...);
-});
-// when snd is executed, it will print "123"
-
-
This adaptor is included as it greatly simplifies lifting values into senders.
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 set_error. 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 set_error.
-
4.20.5. execution::just_stopped
-
execution::senderautojust_stopped();
-
-
Returns a sender with no completion schedulers, which completes immediately by calling the receiver’s set_stopped.
Returns a sender that reaches into a receiver’s environment and pulls out the current value associated with the customization point denoted by Tag. It then sends the value read back to the receiver through the value channel. For instance, get_scheduler() (with no arguments) is a sender that asks the receiver for the currently suggested scheduler and passes it to the receiver’s set_value completion-signal.
-
This can be useful when scheduling nested dependent work. The following sender pulls the current schduler into the value channel and then schedules more work onto it.
-
execution::senderautotask=
- execution::get_scheduler()
- |execution::let_value([](autosched){
- returnexecution::on(sched,somenestedworkhere);
- });
-
-this_thread::sync_wait(std::move(task));// wait for it to finish
-
-
This code uses the fact that sync_wait associates a scheduler with the receiver that it connects with task. get_scheduler() reads that scheduler out of the receiver, and passes it to let_value's receiver’s set_value function, which in turn passes it to the lambda. That lambda returns a new sender that uses the scheduler to schedule some nested work onto sync_wait's scheduler.
-
4.21. User-facing sender adaptors
-
A sender adaptor is an algorithm that takes one or more senders, which it may execution::connect, as parameters, and returns a sender, whose completion is related to the sender arguments it has received.
execution::schedulerautocpu_sched=get_system_thread_pool().scheduler();
-execution::schedulerautogpu_sched=cuda::scheduler();
-
-execution::senderautocpu_task=execution::schedule(cpu_sched);
-// cpu_task describes the creation of a new task on the system thread pool
-
-execution::senderautogpu_task=execution::transfer(cpu_task,gpu_sched);
-// gpu_task describes the transition of the task graph described by cpu_task to the gpu
-
then 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.
-
then is guaranteed to not begin executing function until the returned sender is started.
-
execution::senderautoinput=get_input();
-execution::senderautosnd=execution::then(input,[](auto...args){
- std::print(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.
upon_error and upon_stopped are similar to then, but where then works with values sent by the input sender, upon_error works with errors, and upon_stopped is invoked when the "stopped" signal is sent.
let_value is very similar to then: 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 then sends exactly what that function ends up returning - let_value requires that the function return a sender, and the sender returned by let_value 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.
-
let_value is guaranteed to not begin executing function until the returned sender is started.
-
let_error and let_stopped are similar to let_value, but where let_value works with values sent by the input sender, let_error works with errors, and let_stopped is invoked when the "stopped" signal is sent.
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.
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.
Returns a sender that maps the value channel from a T to an optional<decay_t<T>>, and maps the stopped channel to a value of an empty optional<decay_t<T>>.
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 function will begin executing until the returned sender is started. Each invocation of function 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 bulk execute with the same guarantee. This allows, for instance, a scheduler to execute all invocations of the function in parallel.
-
The bulk operation is intended to be used at the point where the number of agents to be created is known and provided to bulk via its shape 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, bulk can be combined with additional operations such as let_value to deliver dynamic shape information to the bulk 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.
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.
when_all 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 set_value 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 when_all. It completes inline on the execution context on which the last input sender completes, unless stop is requested before when_all is started, in which case it completes inline within the call to start.
-
when_all_with_variant does the same, but it adapts all the input senders using into_variant, and so it does not constrain the input arguments as when_all does.
execution::schedulerautosched=thread_pool.scheduler();
-
-execution::senderautosends_1=...;
-execution::senderautosends_abc=...;
-
-execution::senderautoboth=execution::when_all(sched,
- sends_1,
- sends_abc
-);
-
-execution::senderautofinal=execution::then(both,[](auto...args){
- std::cout<<std::format("the two args: {}, {}",args...);
-});
-// when final executes, it will print "the two args: 1, abc"
-
Once ensure_started returns, it is known that the provided sender has been connected and start 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 execution::connect() is called, or if execution::connect() is called but the
-returned operation-state is destroyed before execution::start() 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 std::shared_ptr 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 execution::connect, as parameters, and does not return a sender.
this_thread::sync_wait is a sender consumer that submits the work described by the provided sender for execution, similarly to ensure_started, except that it blocks the current std::thread or thread of main 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, sync_wait 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, sync_wait throws that error as an exception, or rethrows the original exception if the error is of type std::exception_ptr.
-
If the provided sender sends the "stopped" signal instead of values, sync_wait returns an empty optional.
-
For an explanation of the requires clause, see § 5.8 All senders are typed. That clause also explains another sender consumer, built on top of sync_wait: sync_wait_with_variant.
-
Note: This function is specified inside std::this_thread, and not inside execution. This is because sync_wait 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 std::this_thread, 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 std::this_fiber::sync_wait would be provided. We also expect that runtimes with execution agents that use different synchronization mechanisms than std::thread's will provide their own flavors of sync_wait 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.
A receiver is a callback that supports more than one channel. In fact, it supports three of them:
-
-
-
set_value, which is the moral equivalent of an operator() or a function call, which signals successful completion of the operation its execution depends on;
-
-
set_error, 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; and
-
-
set_stopped, which signals that the operation completed without succeeding (set_value) and without failing (set_error). 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.
-
-
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 set_value failed with an exception, either set_error or set_stopped 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 std::promise, which provides the first two signals as set_value and set_error, 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.
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: start, 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 execution::ensure_started and this_thread::sync_wait, 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.
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 connect will be fulfilled.
-
execution::senderautosnd=someinputsender;
-execution::receiverautorcv=somereceiver;
-execution::operation_stateautostate=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 then, 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 execution::then(sender,invocable) to be equivalent to:
-
-
-
sender.then(invocable), if that expression is well formed; otherwise
-
-
then(sender,invocable), performed in a context where this call always performs ADL, if that expression is well formed; otherwise
-
-
a default implementation of then, which returns a sender adaptor, and then define the exact semantics of said adaptor.
-
-
However, this definition is problematic. Imagine another sender adaptor, bulk, 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 bulk to have specialized behavior, which invokes a kernel of more than one thread (with its size defined by the call to bulk); therefore, we would like to customize bulk for CUDA senders to achieve this. However, there’s no reason for CUDA kernels to
-necessarily customize the then sender adaptor, as the generic implementation is perfectly sufficient. This creates a problem, though; consider the following snippet:
-
execution::schedulerautocuda_sch=cuda_scheduler{};
-
-execution::senderautoinitial=execution::schedule(cuda_sch);
-// the type of initial is a type defined by the cuda_scheduler
-// let’s call it cuda::schedule_sender<>
-
-execution::senderautonext=execution::then(cuda_sch,[]{return1;});
-// 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::senderautokernel_sender=execution::bulk(next,shape,[](inti){...});
-
-
How can we specialize the bulk sender adaptor for our wrapped schedule_sender? 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):
However, if the input sender is not just a then_sender_adaptor like in the example above, but another sender that overrides bulk 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 execution::<sender-algorithm>(sender,args...), for any given sender algorithm that accepts a sender as its first argument, should be
-equivalent to:
-
-
-
tag_invoke(<sender-algorithm>,get_completion_scheduler<Signal>(sender),sender,args...), if that expression is well-formed; otherwise
-
-
tag_invoke(<sender-algorithm>,sender,args...), if that expression is well-formed; otherwise
-
-
a default implementation, if there exists a default implementation of the given sender algorithm.
-
-
where Signal is one of set_value, set_error, or set_stopped; for most sender algorithms, the completion scheduler for set_value would be used, but for some (like upon_error or let_stopped), one of the others would be used.
-
For sender algorithms which accept concepts other than sender as their first argument, we propose that the customization scheme remains as it has been in A Unified Executors Proposal for C++ so far, except it should also use tag_invoke.
-
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 execution::start 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 execution::transfer 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 transfer 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 transfer.
-
To allow for such customization from both ends, we propose the inclusion of a secondary transitioning sender adaptor, called schedule_from. This adaptor is a form of schedule, 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 transfer, to ensure that both schedulers have a say in how the transitions are made. Any scheduler that specializes transfer(snd,sch) shall ensure that the
-return value of their customization is equivalent to schedule_from(sch,snd2), where snd2 is a successor of snd that sends values equivalent to those sent by snd.
-
The default implementation of transfer(snd,sched) is schedule_from(sched,snd).
-
5.8. All senders are typed
-
All senders must 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 execution::then is hard.
-
The mechanism for this advertisement is similar to the one in A Unified Executors Proposal for C++; the
-way to query the types is through completion_signatures_of_t<S,[Env]>::value_types<tuple_like,variant_like>.
-
completion_signatures_of_t::value_types 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 when_all). 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 std::optional to handle the set_stopped signal. However, this assumes that those values can be represented as a tuple,
-like here:
-
execution::senderautosends_1=...;
-execution::senderautosends_2=...;
-execution::senderautosends_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 value_types of a sender which sends Types... to be as follows:
If senders could only ever send one specific set of values, this would probably need to be the required form of value_types 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) set_value 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, A Unified Executors Proposal for C++ also includes a second template parameter to value_types, one that represents a variant-like type. If we permit such senders, we would almost certainly need to require that the canonical form of value_types 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 Types1..., Types2..., ..., TypesN... to be as follows:
This, however, introduces a couple of complications:
-
-
-
A just(1) sender would also need to follow this structure, so the correct type for storing the value sent by it would be std::variant<std::tuple<int>> 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 where value_types 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.
-
-
As a consequence of (1): because sync_wait needs to store the above type, it can no longer return just a std::tuple<int> for just(1); it has to return std::variant<std::tuple<int>>. 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 of sync_wait must be the same across all sender types.
-
-
One possible solution to (2) above is to place a requirement on sync_wait that it can only accept senders which send only a single set of values, therefore removing the need for std::variant to appear in its API; because of this, we propose to expose both sync_wait, which is a simple, user-friendly version of the sender consumer, but requires that value_types have only one possible variant, and sync_wait_with_variant, which accepts any sender, but returns an optional whose value type is the variant of all the
-possible tuples sent by the input sender:
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 (A Unified Executors Proposal for C++ and Towards C++23 executors: A proposal for an initial set of algorithms) 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: sync_wait. We imagine that if, in the future, C++ was to
-gain fibers support, we would want to also have std::this_fiber::sync_wait, in addition to std::this_thread::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.
In short, instead of using globally reserved names, tag_invoke uses the type of the customization point object itself as the mechanism to find customizations. It globally reserves only a single name - tag_invoke - which itself is used the same way that
-ranges-style customization points are used. All other customization points are defined in terms of tag_invoke. For example, the customization for std::this_thread::sync_wait(s) will call tag_invoke(std::this_thread::sync_wait,s), instead of attempting
-to invoke s.sync_wait(), and then sync_wait(s) if the member call is not valid.
-
Using tag_invoke 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<typenameTag,typename...Args>
-friendautotag_invoke(Tag&&tag,wrapper&self,Args&&...args){
- returnstd::forward<Tag>(tag)(self.subobject,std::forward<Args>(args)...);
-}
-
-// but override one of them with a specific value
-friendautotag_invoke(specific_customization_point_t,wrapper&self){
- returnself.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 A Unified Executors Proposal for C++ 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 tag_invoke, dispatching to manually constructed vtables containing pointers to specialized implementations for the wrapped objects. For an example of such a polymorphic wrapper, see unifex::any_unique (example).
[Editorial: Add the header <execution> to Table 23: C++ library headers [tab:headers.cpp]]
-
In subclause [conforming], after [lib.types.movedfrom], add the following new subclause with suggested stable name [lib.tmpl-heads].
-
-
- 16.4.6.17 Class template-heads
-
-
-
If a class template’s template-head is marked with "arguments are not
-associated entities"", any template arguments do not contribute to the
-associated entities ([basic.lookup.argdep]) of a function call where a
-specialization of the class template is an associated entity. In such a case,
-the class template may be implemented as an alias template referring to a
-templated class, or as a class template where the template arguments
-themselves are templated classes.
-
-
[Example:
-
template<classT>// arguments are not associated entities
-structS{};
-
-namespaceN{
- intf(auto);
- structA{};
-}
-
-intx=f(S<N::A>{});// error: N::f not a candidate
-
-
The template S specified above may be implemented as
Insert this section as a new subclause, between Searchers [func.search] and Class template hash[unord.hash].
-
-
-
-
-
The name std::tag_invoke denotes a customization point object [customization.point.object]. Given subexpressions T and A..., the expression std::tag_invoke(T,A...) is expression-equivalent [defns.expression-equivalent] to tag_invoke(T,A...) if it is a well-formed expression with overload resolution performed in a context in which unqualified lookup for tag_invoke finds only the declaration
-
voidtag_invoke();
-
-
Otherwise, std::tag_invoke(T,A...) is ill-formed.
-
-
[Note: Diagnosable ill-formed cases above result in substitution failure when std::tag_invoke(T,A...) appears in the immediate context of a template instantiation. —end note]
Insert this section as a new subclause between Header <stop_token> synopsis [thread.stoptoken.syn] and Class stop_token[stoptoken].
-
-
-
-
-
The stoppable_token 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, T::callback_type<CB>, 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. The stoppable_token_for concept checks for a stop token type compatible with a given
-callback type. The unstoppable_token concept checks for a stop token type that does not allow stopping.
Let t and u be distinct object of type T. The type T models stoppable_token only if:
-
-
-
All copies of a stoppable_token reference the same logical shared stop state and shall report values consistent with each other.
-
-
If t.stop_possible() evaluates to false then, if u, references the same logical shared stop state, u.stop_possible() shall also subsequently evaluate to false and u.stop_requested() shall also subsequently evaluate to false.
-
-
If t.stop_requested() evaluates to true then, if u, references the same logical shared stop state, u.stop_requested() shall also subsequently evaluate to true and u.stop_possible() shall also subsequently evaluate to true.
-
-
Given a callback-type, CB, and a callback-initializer argument, init, of type Initializer then constructing an instance, cb, of type T::callback_type<CB>, passing t as the first argument and init as the second argument to the constructor, shall,
-if t.stop_possible() is true, construct an instance, callback, of type CB, direct-initialized with init, and register callback with t’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.
-
-
-
If t.stop_requested() is true at the time callback is registered then callback may be invoked immediately inline inside the call to cb’s constructor.
-
-
If callback is invoked then, if u references the same shared stop state as t, an evaluation of u.stop_requested() will be true if the beginning of the invocation of callback strongly-happens-before the evaluation of u.stop_requested().
-
-
If t.stop_possible() evaluates to false then the construction of cb is not required to construct and initialize callback.
-
-
-
Construction of a T::callback_type<CB> instance shall only throw exceptions thrown by the initialization of the CB instance from the value of type Initializer.
-
-
Destruction of the T::callback_type<CB> object, cb, removes callback from the shared stop state such that callback will not be invoked after the destructor returns.
-
-
-
If callback is currently being invoked on another thread then the destructor of cb will block until the invocation of callback returns such that the return from the invocation of callback strongly-happens-before the destruction of callback.
-
-
Destruction of a callback cb shall not block on the completion of the invocation of some other callback registered with the same shared stop state.
-
-
-
-
-
-
9.1.3. Class stop_token[stoptoken]
-
9.1.3.1. General [stoptoken.general]
-
Modify the synopsis of class stop_token in section General [stoptoken.general] as follows:
Insert a new subclause, Class never_stop_token[stoptoken.never], after section Class template stop_callback[stopcallback], as a new subclause of Stop tokens [thread.stoptoken].
-
9.1.4.1. General [stoptoken.never.general]
-
-
-
The class never_stop_token provides an implementation of the unstoppable_token concept. It provides a stop token interface, but also provides static information that a stop is never possible nor requested.
9.1.5. Class in_place_stop_token[stoptoken.inplace]
-
Insert a new subclause, Class in_place_stop_token[stoptoken.inplace], after the section added above, as a new subclause of Stop tokens [thread.stoptoken].
-
9.1.5.1. General [stoptoken.inplace.general]
-
-
-
The class in_place_stop_token provides an interface for querying whether a stop request has been made (stop_requested) or can ever be made (stop_possible) using an associated in_place_stop_source object ([stopsource.inplace]).
-An in_place_stop_token can also be passed to an in_place_stop_callback ([stopcallback.inplace]) constructor to register a callback to be called when a stop request has been made from an associated in_place_stop_source.
9.1.6. Class in_place_stop_source[stopsource.inplace]
-
Insert a new subclause, Class in_place_stop_source[stopsource.inplace], after the section added above, as a new subclause of Stop tokens [thread.stoptoken].
-
9.1.6.1. General [stopsource.inplace.general]
-
-
-
The class in_place_stop_source implements the semantics of making a stop request, without the need for a dynamic allocation of a shared state.
-A stop request made on a in_place_stop_source object is visible to all associated in_place_stop_token ([stoptoken.inplace]) objects.
-Once a stop request has been made it cannot be withdrawn (a subsequent stop request has no effect).
-All uses of in_place_stop_token objects associated with a given in_place_stop_source object must happen before the invocation of the destructor of that in_place_stop_token object.
Returns: true if the stop state inside *this has not yet received a stop request; otherwise, false.
-
-
[[nodiscard]]boolstop_requested()constnoexcept;
-
-
-
-
Returns: true if the stop state inside *this has received a stop request; otherwise, false.
-
-
boolrequest_stop()noexcept;
-
-
-
-
Effects: Atomically determines whether the stop state inside *this has received a stop request, and if not, makes a stop request.
-The determination and making of the stop request are an atomic read-modify-write operation ([intro.races]).
-If the request was made, the callbacks registered by associated in_place_stop_callback objects are synchronously called.
-If an invocation of a callback exits via an exception then terminate is invoked ([except.terminate]).
-
-
Postconditions: stop_possible() is false and stop_requested() is true.
-
-
Returns: true if this call made a stop request; otherwise false.
-
-
9.1.7. Class template in_place_stop_callback[stopcallback.inplace]
-
Insert a new subclause, Class template in_place_stop_callback[stopcallback.inplace], after the section added above, as a new subclause of Stop tokens [thread.stoptoken].
Mandates: in_place_stop_callback is instantiated with an argument for the template parameter Callback that satisfies both invocable and destructible.
-
-
Preconditions: in_place_stop_callback is instantiated with an argument for the template parameter Callback that models both invocable and destructible.
-
-
Recommended practice: Implementation should use the storage of the in_place_stop_callback objects to store the state necessary for their association with an in_place_stop_source object.
-
-
9.1.7.2. Constructors and destructor [stopcallback.inplace.cons]
Constraints: Callback and C satisfy constructible_from<Callback,C>.
-
-
Preconditions: Callback and C model constructible_from<Callback,C>.
-
-
Effects: Initializes callback_ with std::forward<C>(cb).
-If st.stop_requested() is true, then std::forward<Callback>(callback_)() is evaluated in the current thread before the constructor returns.
-Otherwise, if st has an associated in_place_stop_source object, registers the callback with the stop state of the in_place_stop_source that st is associated with such that std::forward<Callback>(callback_)() is evaluated by the first call to request_stop() on an associated in_place_stop_source.
-The in_place_stop_callback object being initialized becomes associated with the in_place_stop_source object that st is associated with, if any.
-
-
Throws: Any exception thrown by the initialization of callback_.
-
-
Remarks: If evaluating std::forward<Callback>(callback_)() exits via an exception, then terminate is invoked ([except.terminate]).
-
-
~in_place_stop_callback();
-
-
-
-
Effects: Unregisters the callback from the stop state of the associated in_place_stop_source object, if any.
-The destructor does not block waiting for the execution of another callback registered by an associated stop_callback.
-If callback_ is concurrently executing on another thread, then the return from the invocation of callback_ strongly happens before ([intro.races]) callback_ is destroyed.
-If callback_ is executing on the current thread, then the destructor does not block ([defns.block]) waiting for the return from the invocation of callback_.
-
-
Remarks: A program has undefined behavior if the invocation of this function does not strongly happen before the beginning of the invocation of the destructor of the associated in_place_stop_source object, if any.
-
-
10. Execution control library [exec]
-
-
-
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.
-
-
-
Table 1: Execution control library summary [tab:execution.summary]
[Note: A large number of execution control primitives are customization point objects. For an object one might define multiple types of customization point objects, for which different rules apply.**** Table 2 shows the types of customization point objects used in the execution control library:
-
-
-
Table 2: Types of customization point objects in the execution control library [tab:execution.cpos]
-
-
-
Customization point object type
-
Purpose
-
Examples
-
-
core
-
provide core execution functionality, and connection between core components
-
connect, start, execute
-
-
completion signals
-
called by senders to announce the completion of the work (success, error, or cancellation)
-
execution::get_scheduler is used to ask an object for its associated scheduler.
-
-
The name execution::get_scheduler denotes a customization point object. For some subexpression r, if the type of r is (possibly cv-qualified) no_env, then execution::get_scheduler(r) is ill-formed. Otherwise, it is expression equivalent to:
-
-
-
tag_invoke(execution::get_scheduler,as_const(r)), if this expression is well formed.
-
-
-
Mandates: The tag_invoke expression above is not
-potentially-throwing and its type satisfies execution::scheduler.
-
-
-
Otherwise, execution::get_scheduler(r) is ill-formed.
-
-
-
execution::get_scheduler() (with no arguments) is expression-equivalent to execution::read(execution::get_scheduler).
execution::get_delegatee_scheduler is used to ask an object for a scheduler that may be used to delegate work to for the purpose of forward progress delegation.
-
-
The name execution::get_delegatee_scheduler denotes a customization point object. For some subexpression r, if the type of r is (possibly cv-qualified) no_env, then execution::get_delegatee_scheduler(r) is ill-formed. Otherwise, it is expression equivalent to:
-
-
-
tag_invoke(execution::get_delegatee_scheduler,as_const(r)), if this expression is well formed.
-
-
-
Mandates: The tag_invoke expression above is not
-potentially-throwing and its type satisfies execution::scheduler.
-
-
-
Otherwise, execution::get_delegatee_scheduler(r) is ill-formed.
-
-
-
execution::get_delegatee_scheduler() (with no arguments) is expression-equivalent to execution::read(execution::get_delegatee_scheduler).
execution::get_allocator is used to ask an object for its associated allocator.
-
-
The name execution::get_allocator denotes a customization point object. For some subexpression r, if the type of r is (possibly cv-qualified) no_env, then execution::get_allocator(r) is ill-formed. Otherwise, it is expression equivalent to:
-
-
-
tag_invoke(execution::get_allocator,as_const(r)), if this expression is well formed.
-
-
-
Mandates: The tag_invoke expression above is not
-potentially-throwing and its type satisfies Allocator.
-
-
-
Otherwise, execution::get_allocator(r) is ill-formed.
-
-
-
execution::get_allocator() (with no arguments) is expression-equivalent to execution::read(execution::get_allocator).
execution::get_stop_token is used to ask an object for an associated stop token.
-
-
The name execution::get_stop_token denotes a customization point object. For some subexpression r, if the type of r is (possibly cv-qualified) no_env, then execution::get_stop_token(r) is ill-formed. Otherwise, it is expression equivalent to:
-
-
-
tag_invoke(execution::get_stop_token,as_const(r)), if this expression is well formed.
-
-
-
Mandates: The tag_invoke expression above is not
-potentially-throwing and its type satisfies stoppable_token.
-
-
-
Otherwise, never_stop_token{}.
-
-
-
execution::get_stop_token() (with no arguments) is expression-equivalent to execution::read(execution::get_stop_token).
-
-
10.4. Execution environments [exec.env]
-
-
-
An execution environment contains state associated with the completion of an asynchronous operation. Every receiver has an associated execution environment, accessible with the get_env receiver query. The state of an execution environment is accessed with customization point objects. An execution environment may respond to any number of these environment queries.
-
-
An environment query is a customization point object that accepts as its first argument an execution environment. For an environment query EQ and an object e of type no_env, the expression EQ(e) shall be ill-formed.
no_env is a special environment used by the sender concept and by the get_completion_signatures customization point when the user has specified no environment argument. [Note: A user may choose to not specify an environment in order to see if a sender knows its completion signatures independent of any particular execution environment. -- end note]
-
-
10.4.2. execution::get_env[exec.get_env]
-
namespaceexec-envs{// exposition only
- structget_env_t;
-}
-inlineconstexprexec-envs::get_env_tget_env{};
-
-
-
-
get_env is a customization point object. For some subexpression r, get_env(r) is expression-equivalent to
-
-
-
tag_invoke(execution::get_env,r) if that expression is well-formed.
-
-
-
Mandates: The decayed type of the above expression is not no_env.
-
-
-
Otherwise, get_env(r) is ill-formed.
-
-
-
If get_env(r) is an lvalue, the object it refers to shall be valid while r is valid.
execution::forwarding_env_query is used to ask a customization point object whether it is a environment query that should be forwarded through environment adaptors.
-
-
The name execution::forwarding_env_query denotes a customization point object. For some subexpression t, execution::forwarding_env_query(t) is expression equivalent to:
-
-
-
tag_invoke(execution::forwarding_env_query,t), contextually converted to bool, if the tag_invoke expression is well formed.
-
-
-
Mandates: The tag_invoke expression is indeed contextually
-convertible to bool, that expression and the contextual conversion
-are not potentially-throwing and are core constant expressions if t is a core constant expression.
-
-
-
Otherwise, true.
-
-
-
10.5. Schedulers [exec.sched]
-
-
-
The scheduler concept defines the requirements of a type that allows for scheduling of work on its associated execution context.
Let S be the type of a scheduler and let E be the type of an execution environment for which sender<schedule_result_t<S>,E> is true. Then sender_of<schedule_result_t<S>,E> shall be true.
-
-
None of a scheduler’s copy constructor, destructor, equality comparison, or swap member functions shall exit via an exception.
-
-
None of these member functions, nor a scheduler type’s schedule function, shall introduce data races as a result of concurrent invocations of those functions from different threads.
-
-
For any two (possibly const) values s1 and s2 of some scheduler type S, s1==s2 shall return true only if both s1 and s2 are handles to the same associated execution context.
-
-
For a given scheduler expression s, the expression execution::get_completion_scheduler<set_value_t>(execution::schedule(s)) shall compare equal to s.
-
-
A scheduler type’s destructor shall not block pending completion of any receivers connected to the sender objects returned from schedule. [Note: The ability to wait for completion of submitted function objects may be provided by the associated execution
-context of the scheduler. —end note]
execution::forwarding_scheduler_query is used to ask a customization point object whether it is a scheduler query that should be forwarded through scheduler adaptors.
-
-
The name execution::forwarding_scheduler_query denotes a customization point object. For some subexpression t, execution::forwarding_scheduler_query(t) is expression equivalent to:
-
-
-
tag_invoke(execution::forwarding_scheduler_query,t), contextually converted to bool, if the tag_invoke expression is well formed.
-
-
-
Mandates: The tag_invoke expression is indeed contextually
-convertible to bool, that expression and the contextual conversion
-are not potentially-throwing and are core constant expressions if t is a core constant expression.
execution::get_forward_progress_guarantee is used to ask a scheduler about the forward progress guarantees of execution agents created by that scheduler.
-
-
The name execution::get_forward_progress_guarantee denotes a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::scheduler, execution::get_forward_progress_guarantee is ill-formed.
-Otherwise, execution::get_forward_progress_guarantee(s) is expression equivalent to:
-
-
-
tag_invoke(execution::get_forward_progress_guarantee,as_const(s)), if this expression is well formed.
-
-
-
Mandates: The tag_invoke expression above is not potentially
-throwing and its type is execution::forward_progress_guarantee.
If execution::get_forward_progress_guarantee(s) for some scheduler s returns execution::forward_progress_guarantee::concurrent, all execution agents created by that scheduler shall provide the concurrent forward progress guarantee. If it returns execution::forward_progress_guarantee::parallel, all execution agents created by that scheduler shall provide at least the parallel forward progress guarantee.
this_thread::execute_may_block_caller is used to ask a scheduler s whether a call execution::execute(s,f) with any invocable f may block the thread where such a call occurs.
-
-
The name this_thread::execute_may_block_caller denotes a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::scheduler, this_thread::execute_may_block_caller is ill-formed. Otherwise, this_thread::execute_may_block_caller(s) is expression equivalent to:
-
-
-
tag_invoke(this_thread::execute_may_block_caller,as_const(s)), if this expression is well formed.
-
-
-
Mandates: The tag_invoke expression above is not potentially
-throwing and its type is bool.
-
-
-
Otherwise, true.
-
-
-
If this_thread::execute_may_block_caller(s) for some scheduler s returns false, no execution::execute(s,f) call with some invocable f shall block the calling thread.
-
-
10.6. Receivers [exec.recv]
-
-
-
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, set_error, and set_stopped. These are
-collectively known as a receiver’s completion-signal operations.
-
-
The receiver concept defines the requirements for a receiver type with an
-unknown set of completion signatures. The receiver_of concept defines the
-requirements for a receiver type with a known set of completion signatures.
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 execution::start has been called on the operation state object that was returned by execution::connect to connect that receiver to a sender.
-
-
Once execution::start 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.
-
-
If execution::set_value exits with an exception, it is still valid to call execution::set_error or execution::set_stopped on the receiver, but it is no longer valid to call execution::set_value on the receiver.
-
-
-
Once one of a receiver’s completion-signal operations has completed non-exceptionally, the receiver contract has been satisfied.
-
-
Receivers have an associated execution environment that is accessible by passing the receiver to execution::get_env. A sender can obtain information about the current execution environment by querying the environment of the receiver to which it is connected. The set of environment queries is extensible and includes:
-
-
-
execution::get_scheduler: used to obtain a suggested scheduler to
-be used when a sender needs to launch additional work. [Note: the
-presence of this query on a receiver’s environment does not bind a
-sender to use its result. --end note]
-
-
execution::get_delegatee_scheduler: used to obtain a delegatee
-scheduler on which an algorithm or scheduler may delegate work for the
-purpose of ensuring forward progress.
-
-
execution::get_allocator: used to obtain a suggested allocator to be used when a sender 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_stop_token: used to obtain the associated stop
-token to be used by a sender 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]
-
-
-
Let r be a receiver, s be a sender, and op_state be an operation state
-resulting from an execution::connect(s,r) call. Let token be a stop
-token resulting from an execution::get_stop_token(execution::get_env(r)) call. token must remain valid at least until a call to a receiver
-completion-signal function of r returns successfully. [Note: this
-means that, unless it knows about further guarantees provided by the
-receiver r, the implementation of op_state should not use token after
-it makes a call to a receiver completion-signal function of r. This also
-implies that any stop callbacks registered on token by the implementation
-of op_state or s must be destroyed before such a call to a receiver
-completion-signal function of r. --end note]
-
-
10.6.1. execution::set_value[exec.set_value]
-
-
-
execution::set_value is used to send a value completion signal to a receiver.
-
-
The name execution::set_value denotes a customization point object. The
-expression execution::set_value(R,Vs...) for some subexpressions R and Vs... is expression-equivalent to:
-
-
-
tag_invoke(execution::set_value,R,Vs...), if that expression is
-valid. If the function selected by tag_invoke does not send the
-value(s) Vs... to the receiver R’s value channel, the behavior of
-calling execution::set_value(R,Vs...) is undefined.
-
-
-
Mandates: The tag_invoke expression above is not potentially
-throwing.
-
-
-
Otherwise, execution::set_value(R,Vs...) is ill-formed.
-
-
-
10.6.2. execution::set_error[exec.set_error]
-
-
-
execution::set_error is used to send a error signal to a receiver.
-
-
The name execution::set_error denotes a customization point object. The expression execution::set_error(R,E) for some subexpressions R and E is expression-equivalent to:
-
-
-
tag_invoke(execution::set_error,R,E), if that expression is valid. If
-the function selected by tag_invoke does not send the error E to the
-receiver R’s error channel, the behavior of calling execution::set_error(R,E) is undefined.
-
-
-
Mandates: The tag_invoke expression above is not potentially
-throwing.
-
-
-
Otherwise, execution::set_error(R,E) is ill-formed.
-
-
-
10.6.3. execution::set_stopped[exec.set_stopped]
-
-
-
execution::set_stopped is used to send a stopped signal to a receiver.
-
-
The name execution::set_stopped denotes a customization point object. The expression execution::set_stopped(R) for some subexpression R is expression-equivalent to:
-
-
-
tag_invoke(execution::set_stopped,R), if that expression is valid. If the function selected by tag_invoke does not signal the receiver R’s stopped channel, the behavior of calling execution::set_stopped(R) is undefined.
-
-
-
Mandates: The tag_invoke expression above is not potentially
-throwing.
-
-
-
Otherwise, execution::set_stopped(R) is ill-formed.
execution::forwarding_receiver_query is used to ask a customization point object whether it is a receiver query that should be forwarded through receiver adaptors.
-
-
The name execution::forwarding_receiver_query denotes a customization point object. For some subexpression t, execution::forwarding_receiver_query(t) is expression equivalent to:
-
-
-
tag_invoke(execution::forwarding_receiver_query,t), contextually converted to bool, if the tag_invoke expression is well formed.
-
-
-
Mandates: The tag_invoke expression is indeed contextually
-convertible to bool, that expression and the contextual conversion
-are not potentially-throwing and are core constant expressions if t is a core constant expression.
-
-
-
Otherwise, false if the type of t is one of set_value_t, set_error_t, or set_stopped_t.
-
-
Otherwise, true.
-
-
-
[Note: Currently the only standard receiver query is execution::get_env -- end note]
-
-
10.7. Operation states [exec.op_state]
-
-
-
The operation_state concept defines the requirements for an operation state type, which allows for starting the execution of work.
Any operation state types defined by the implementation are non-movable types.
-
-
10.7.1. execution::start[exec.op_state.start]
-
-
-
execution::start is used to start work represented by an operation state object.
-
-
The name execution::start denotes a customization point object. The expression execution::start(O) for some lvalue subexpression O is expression-equivalent to:
-
-
-
tag_invoke(execution::start,O), if that expression is valid. If the function selected by tag_invoke does not start the work represented by the operation state O, the behavior of calling execution::start(O) is undefined.
-
-
-
Mandates: The tag_invoke expression above is not potentially
-throwing.
-
-
-
Otherwise, execution::start(O) is ill-formed.
-
-
-
The caller of execution::start(O) must guarantee that the lifetime of the operation state object O extends at least until one of the receiver completion-signal functions of a receiver R passed into the execution::connect call that produced O 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]
-
-
10.8. Senders [exec.snd]
-
-
-
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 sender concept defines the requirements for a sender type. The sender_to concept defines the requirements for a sender type capable of being connected with a specific receiver type.
The alias template completion_signatures_of_t is used to query a sender type for facts associated with the signals it sends.
-
-
completion_signatures_of_t also recognizes awaitables as senders. For this clause ([exec]):
-
-
-
An awaitable is an expression that would be well-formed as the operand of a co_await expression within a given context.
-
-
For any type T, is-awaitable<T> is 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 await_transform. For a coroutine promise type P, is-awaitable<T,P> is 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 a such that decltype((a)) is type A, await-result-type<A> is an alias for decltype(e), where e is a's await-resume expression ([expr.await]) within the context of a coroutine whose promise type does not define a member await_transform. For a coroutine promise type P, await-result-type<A,P> is an alias for decltype(e), where e is a's await-resume expression ([expr.await]) within the context of a coroutine whose promise type is P.
-
-
-
For types S and E, the type completion_signatures_of_t<S,E> is an
-alias for decltype(get_completion_signatures(declval<S>(),declval<E>())) if that expression is well-formed and names a type other than no-completion-signatures. Otherwise, it is ill-formed.
-
-
execution::get_completion_signatures is a customization point object. Let s be an expression such that decltype((s)) is S, and let e be an
-expression such that decltype((e)) is E. Then get_completion_signatures(s) is expression-equivalent to get_completion_signatures(s,no_env{}) and get_completion_signatures(s,e) is expression-equivalent to:
-
-
-
tag_invoke_result_t<get_completion_signatures_t,S,E>{} if that expression is well-formed,
-
-
-
Mandates:is-instance-of<Sigs,completion_signatures> or is-instance-of<Sigs,dependent_completion_signatures>, where Sigs names the type tag_invoke_result_t<get_completion_signatures_t,S,E>.
-
-
-
Otherwise, if remove_cvref_t<S>::completion_signatures is well-formed
-and names a type, then a value-initialized prvalue of type remove_cvref_t<S>::completion_signatures,
-
-
-
Mandates:is-instance-of<Sigs,completion_signatures> or is-instance-of<Sigs,dependent_completion_signatures>, where Sigs names the type remove_cvref_t<S>::completion_signatures.
-
-
-
Otherwise, if is-awaitable<S> is true, then
-
-
-
If await-result-type<S> is cvvoid then a prvalue of a type equivalent to:
The exposition-only type variant-or-empty<Ts...> is
- defined as follows:
-
-
-
If sizeof...(Ts) is greater than zero, variant-or-empty<Ts...> names the type variant<Us...> where Us... is the pack decay_t<Ts>... with duplicate types removed.
-
-
Otherwise, variant-or-empty<Ts...> names an implementation defined class type equivalent to the following:
Let r be an rvalue receiver of type R, and let S be the type of a
-sender. If value_types_of_t<S,env_of_t<R>,Tuple,Variant> is well
-formed, it shall name the type Variant<Tuple<Args0...>,Tuple<Args1...>,...,Tuple<ArgsN...>>>, where the type packs Args0 through ArgsN are the packs of types the sender S passes as arguments to execution::set_value (besides the receiver object).
-Such a sender S shall not odr-use ([basic.def.odr]) execution::set_value(r,args...), where decltype(args)... is not one of the type packs Args0... through ArgsN... (ignoring differences in
-rvalue-reference qualification).
-
-
Let r be an rvalue receiver of type R, and let S be the type of a
-sender. If error_types_of_t<S,env_of_t<R>,Variant> is well formed, it
-shall name the type Variant<E0,E1,...,EN>, where the types E0 through EN are the types the sender S passes as arguments to execution::set_error (besides the receiver object). Such a sender S shall not odr-use execution::set_error(r,e), where decltype(e) is not one of the types E0 through EN (ignoring differences in rvalue-reference qualification).
-
-
Let r be an rvalue receiver of type R, and let S be the type of a
-sender. If completion_signatures_of_t<S,env_of_t<R>>::sends_stopped is well formed and false, such a sender S shall not odr-use execution::set_stopped(r).
-
-
Let S be the type of a sender, let E be the type of an execution
-environment other than execution::no_env such that sender<S,E> is true. Let Tuple, Variant1, and Variant2 be variadic alias templates
-or class templates such that following types are well-formed:
-
-
-
value_types_of_t<S,no_env,Tuple,Variant1>
-
-
error_types_of_t<S,no_env,Variant2>
-
-
then the following shall also be true:
-
-
-
value_types_of_t<S,E,Tuple,Variant1> shall also be well-formed and shall
- name the same type as value_types_of_t<S,no_env,Tuple,Variant1>,
-
-
error_types_of_t<S,E,Variant2> shall also be well-formed and shall
- name the same type as error_types_of_t<S,no_env,Variant2>, and
-
-
completion_signatures_of_t<S,E>::sends_stopped shall have the same
- value as completion_signatures_of_t<S,no_env>::sends_stopped.
-
-
-
[Note: The types Argsi... and Ei... captured in value_types and error_types can appear in any order.
- For example, a sender that can yield, in case of an error, either exception_ptr or error_code can have error_types be either Variant<exception_ptr,error_code> or Variant<error_code,exception_ptr>. --end note]
template<classE>// arguments are not associated entities ([lib.tmpl-heads])
- structdependent_completion_signatures{};
-
-
-
-
dependent_completion_signatures is a placeholder completion signatures
-descriptor that can be returned from get_completion_signatures to report
-that a type might be a sender within a particular execution environment, but
-it isn’t a sender in an arbitrary execution environment.
-
-
When used as the return type of a customization of get_completion_signatures, the template argument E shall be the
-unqualified type of the second argument.
-
-
10.8.2. execution::connect[exec.connect]
-
-
-
execution::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.
-
-
The name execution::connect denotes a customization point object. For some subexpressions s and r, let S be decltype((s)) and R be decltype((r)), and let S' and R' be the decayed types of S and R, respectively. If R does not satisfy execution::receiver, execution::connect(s,r) is ill-formed. Otherwise, the expression execution::connect(s,r) is expression-equivalent to:
-
-
-
tag_invoke(execution::connect,s,r), if the constraints below are satisfied. If the function selected by tag_invoke does not return an operation state for which execution::start starts work described by s, the behavior of calling execution::connect(s,r) is undefined.
Mandates: The type of the tag_invoke expression above satisfies operation_state.
-
-
-
Otherwise, connect-awaitable(s,r) if is-awaitable<S,connect-awaitable-promise> is true and that expression is valid, where connect-awaitable is a coroutine equivalent to the following:
where connect-awaitable-promise is the promise type of connect-awaitable, and where connect-awaitable suspends at the initial suspends point ([dcl.fct.def.coroutine]), and:
-
-
-
set-value-expr first evaluates co_awaitstd::move(s), then suspends the coroutine and evaluates execution::set_value(std::move(r)) if await-result-type<S,connect-awaitable-promise> is cvvoid; otherwise, it evaluates auto&&res=co_awaitstd::move(s), then suspends the coroutine and evaluates execution::set_value(std::move(r),std::forward<decltype(res)>(res)).
-
If the call to execution::set_value exits with an exception, the coroutine is resumed and the exception is immediately propagated in the context of the coroutine.
-
[Note: If the call to execution::set_value exits normally, then the connect-awaitable coroutine is never resumed. --end note]
-
-
set-error-expr first suspends the coroutine and then executes execution::set_error(std::move(r),std::move(ep)).
-
[Note: The connect-awaitable coroutine is never resumed after the call to execution::set_error. --end note]
-
-
operation-state-task is a type that models operation_state. Its execution::start resumes the connect-awaitable coroutine, advancing it past the initial suspend point.
-
-
Let p be an lvalue reference to the promise of the connect-awaitable coroutine, let b be a const lvalue reference to the receiver r. Then tag_invoke(tag,p,as...) is expression-equivalent to tag(b,as...) for any set of arguments as... and for any tag whose type satisfies forwarding-receiver-query.
-
-
The expression p.unhandled_stopped() is expression-equivalent to (execution::set_stopped(std::move(r)),noop_coroutine()).
-
-
For some expression e, the expression p.await_transform(e) is expression-equivalent to tag_invoke(as_awaitable,e,p) if that expression is well-formed; otherwise, it is expression-equivalent to e.
-
-
Let Res be await-result-type<S,connect-awaitable-promise>, and let Vs... be an empty parameter pack if Res is cvvoid, or a pack containing the single type Res otherwise. The operand of the requires-clause of connect-awaitable is equivalent to receiver_of<R,Sigs> where Sigs names the type:
Standard sender types shall always expose an rvalue-qualified overload of a customization of execution::connect. Standard sender types shall only expose an lvalue-qualified overload of a customization of execution::connect if they are copyable.
execution::forwarding_sender_query is used to ask a customization point object whether it is a sender query that should be forwarded through sender adaptors.
-
-
The name execution::forwarding_sender_query denotes a customization point object. For some subexpression t, execution::forwarding_sender_query(t) is expression equivalent to:
-
-
-
tag_invoke(execution::forwarding_sender_query,t) contextually converted to bool, if the tag_invoke expression is well formed.
-
-
-
Mandates: The tag_invoke expression is indeed contextually
-convertible to bool, that expression and the contextual conversion
-are not potentially-throwing and are core constant expressions if t is a core constant expression.
execution::get_completion_scheduler is used to ask a sender object for the completion scheduler for one of its signals.
-
-
The name execution::get_completion_scheduler denotes a customization point object template. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::sender, execution::get_completion_scheduler<CPO>(s) is ill-formed for all template arguments CPO. If the template
-argument CPO in execution::get_completion_scheduler<CPO> is not one of execution::set_value_t, execution::set_error_t, or execution::set_stopped_t, execution::get_completion_scheduler<CPO> is ill-formed. Otherwise, execution::get_completion_scheduler<CPO>(s) is expression-equivalent to:
-
-
-
tag_invoke(execution::get_completion_scheduler<CPO>,as_const(s)) if this expression is well formed.
-
-
-
Mandates: The tag_invoke expression above is not potentially throwing and its type satisfies execution::scheduler.
-
-
-
Otherwise, execution::get_completion_scheduler<CPO>(s) is ill-formed.
-
-
-
If, for some sender s and customization point object CPO, execution::get_completion_scheduler<decltype(CPO)>(s) is well-formed and results in a scheduler sch, and the sender s invokes CPO(r,args...), for some receiver r which has been connected to s, with additional arguments args..., on an execution agent which does not belong to the associated execution context of sch, the behavior is undefined.
-
-
The expression execution::forwarding_sender_query(get_completion_scheduler<CPO>) shall be a prvalue core constant expression of type bool with value true. It shall not be potentially-throwing. CPO shall be one of set_value_t, set_error_t or set_stopped_t.
-
-
10.8.4. Sender factories [exec.factories]
-
10.8.4.1. General [exec.factories.general]
-
-
-
Subclause [exec.factories] defines sender factories, which are utilities that return senders without accepting senders as arguments.
-
-
10.8.4.2. execution::schedule[exec.schedule]
-
-
-
execution::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.
-
-
The name execution::schedule denotes a customization point object. For some subexpression s, the expression execution::schedule(s) is expression-equivalent to:
-
-
-
tag_invoke(execution::schedule,s), if that expression is valid. If the function selected by tag_invoke does not return a sender whose set_value completion scheduler is equivalent to s, the behavior of calling execution::schedule(s) is undefined.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
execution::just is used to create a sender that propagates a set of values to a connected receiver. execution::just_error is used to create a sender that propagates an error to a connected receiver. execution::just_stopped is used to create a sender that propagates a stopped signal to a connected receiver.
The name execution::just denotes a customization point object. For some subexpressions vs..., let Vs... be decltype((vs)). If any type V in Vs does not satisfy movable-value, execution::just(vs...) is ill-formed.
-Otherwise, execution::just(vs...) is expression-equivalent to just-sender<set_value_t,decay_t<Ts>...>(vs...).
-
-
The name execution::just_error denotes a customization point object. For some subexpression err, let Err be decltype((err)). If Err does not satisfy movable-value, execution::just_error(err) is ill-formed.
-Otherwise, execution::just_error(err) is expression-equivalent to just-sender<set_error_t,decay_t<Err>>(err).
-
-
Then name execution::just_stopped denotes a customization point object. execution::just_stopped is expression-equivalent to just-sender<set_stopped_t>().
execution::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.
-
-
The name execution::transfer_just denotes a customization point object. For some subexpressions s and vs..., let S be decltype((s)) and Vs... be decltype((vs)). If S does not satisfy execution::scheduler, or any type V in Vs does not
-satisfy movable-value, execution::transfer_just(s,vs...) is ill-formed. Otherwise, execution::transfer_just(s,vs...) is expression-equivalent to:
-
-
-
tag_invoke(execution::transfer_just,s,vs...), if that expression is
-valid. If the function selected by tag_invoke does not return a sender
-whose set_value completion scheduler is equivalent to s and sends
-values equivalent to auto(vs)... to a receiver connected to it, the
-behavior of calling execution::transfer_just(s,vs...) is undefined.
-
-
-
Mandates:execution::sender_of<R,no_env,decltype(auto(vs))...>, where R is the type of the tag_invoke expression above.
execution::read is used to create a sender that retrieves a value from the receiver’s associated environment and sends it back to the receiver through the value channel.
-
-
execution::read is a customization point object of an unspecified class type equivalent to:
-
template<classTag>
- structread-sender;// exposition only
-
-structread-t{// exposition only
- template<classTag>
- read-sender<Tag>operator()(Tag)constnoexcept{return{};}
-};
-
-
-
read-sender is an exposition only class template equivalent to:
-
template<classTag>
- structread-sender{// exposition only
- template<classR>
- structoperation-state{// exposition only
- Rr_;// exposition only
-
- friendvoidtag_invoke(start_t,operation-state&s)noexcept{
- TRY-SET-VALUE(std::move(s.r_),auto(Tag{}(get_env(s.r_))));
- }
- };
-
- template<receiverR>
- friendoperation-state<decay_t<R>>tag_invoke(connect_t,read-sender,R&&r){
- return{std::forward<R>(r)};
- }
-
- template<classEnv>
- friendautotag_invoke(get_completion_signatures_t,read-sender,Env)
- ->dependent_completion_signatures<Env>;// not defined
-
- template<classEnv>
- requirescallable<Tag,Env>
- friendautotag_invoke(get_completion_signatures_t,read-sender,Env)
- ->completion_signatures<
- set_value_t(call-result-t<Tag,Env>),set_error_t(exception_ptr)>;// not defined
-
- template<classEnv>
- requiresnothrow-callable<Tag,Env>
- friendautotag_invoke(get_completion_signatures_t,read-sender,Env)
- ->completion_signatures<set_value_t(call-result-t<Tag,Env>)>;// not defined
- };
-
-
where TRY-SET-VALUE(r,e), for two subexpressions r and e,
-is equivalent to:
if e is potentially throwing; or execution::set_value(r,e) otherwise.
-
-
10.8.5. Sender adaptors [exec.adapt]
-
10.8.5.1. General [exec.adapt.general]
-
-
-
Subclause [exec.adapt] 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 execution::connect, and execution::start is called on the resulting operation state. This requirement applies to any function that is selected by the implementation of the sender adaptor.
-
-
A type T is a forwarding sender query if it is the type of a customization point object that models forwarding-sender-query. Unless otherwise specified, all sender adaptors that accept a single sender argument return sender objects that propagate forwarding sender queries to that single sender argument. This requirement applies to any function that is selected by the implementation of the
-sender adaptor.
-
-
A type T is a forwarding receiver query if it is the type of a customization point object that models forwarding-receiver-query. Unless otherwise specified, whenever a sender adaptor constructs a receiver that it passes to another sender’s connect, that receiver shall propagate forwarding receiver queries to a receiver accepted as an argument of execution::connect. This requirements
-applies to any sender returned from a function that is selected by the implementation of such sender adaptor.
-
-
For any sender type, receiver type, operation state type, execution environment type, or coroutine promise type that is part of the implementation of any sender adaptor in this subclause and that is a class template, the template arguments do not contribute to the associated entities ([basic.lookup.argdep]) of a function call where a specialization of the class template is an associated entity.
If the specification of a sender adaptor requires that the implementation of the get_completion_signatures customization point adds the signature set_error_t(exception_ptr) as an additional signature, but a customization of that sender adaptor never
-calls the exception_ptr overload of set_error, that customization is allowed to omit the set_error_t(exception_ptr) additional signature from its implementation of the get_completion_signatures sender query.
A pipeable sender adaptor closure object is a function object that accepts one or more sender arguments and returns a sender. For a sender adaptor closure object C and an expression S such that decltype((S)) models sender, the following
-expressions are equivalent and yield a sender:
-
C(S)
-S|C
-
-
Given an additional pipeable sender adaptor closure object D, the expression C|D produces another pipeable sender adaptor closure object E:
-
E is a perfect forwarding call wrapper ([func.require]) with the following properties:
-
-
-
Its target object is an object d of type decay_t<decltype((D))> direct-non-list-initialized with D.
-
-
It has one bound argument entity, an object c of type decay_t<decltype((C))> direct-non-list-initialized with C.
-
-
Its call pattern is d(c(arg)), where arg is the argument used in a function call expression of E.
-
-
The expression C|D is well-formed if and only if the initializations of the state entities of E are all well-formed.
-
-
An object t of type T is a pipeable sender adaptor closure object if T models derived_from<sender_adaptor_closure<T>>, T has no other base
-classes of type sender_adaptor_closure<U> for any other type U, and T does not model sender.
-
-
The template parameter D for sender_adaptor_closure may be an incomplete type. Before any expression of type cvD appears as
-an operand to the | operator, D shall be complete and model derived_from<sender_adaptor_closure<D>>. The behavior of an expression involving an
-object of type cvD as an operand to the | operator is undefined if overload resolution selects a program-defined operator| function.
-
-
A pipeable sender adaptor object is a customization point object that accepts a sender as its first argument and returns a 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 adaptor accepts more than one argument, then let s be an expression such that decltype((s)) models sender,
-let args... be arguments such that adaptor(s,args...) is a well-formed expression as specified in the rest of this subclause
-([exec.adapt.objects]), and let BoundArgs be a pack that denotes decay_t<decltype((args))>.... The expression adaptor(args...) produces a pipeable sender adaptor closure object f that is a perfect forwarding call wrapper with the following properties:
-
-
-
Its target object is a copy of adaptor.
-
-
Its bound argument entities bound_args consist of objects of types BoundArgs... direct-non-list-initialized with std::forward<decltype((args))>(args)..., respectively.
-
-
Its call pattern is adaptor(r,bound_args...), where r is the argument used in a function call expression of f.
-
-
The expression adaptor(args...) is well-formed if and only if the initializations of the bound argument entities of the result, as specified above,
- are all well-formed.
-
-
10.8.5.3. execution::on[exec.on]
-
-
-
execution::on is used to adapt a sender into a sender that will start the input sender on an execution agent belonging to a specific execution context.
-
-
Let replace-scheduler(e,sch) be an expression denoting an object e' such that execution::get_scheduler(e) returns a copy of sch, and tag_invoke(tag,e',args...) is expression-equivalent to tag(e,args...) for all arguments args... and for all tag whose type satisfies forwarding-env-query and is not execution::get_scheduler_t.
-
-
The name execution::on denotes a customization point object. For some subexpressions sch and s, let Sch be decltype((sch)) and S be decltype((s)). If Sch does not satisfy execution::scheduler, or S does not satisfy execution::sender, execution::on is ill-formed. Otherwise, the expression execution::on(sch,s) is expression-equivalent to:
-
-
-
tag_invoke(execution::on,sch,s), if that expression is valid. If the function selected above does not return a sender which starts s on an execution agent of the associated execution context of sch when
-started, the behavior of calling execution::on(sch,s) is undefined.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, constructs a sender s1. When s1 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r such that:
-
-
-
When execution::set_value(r) is called, it calls execution::connect(s,r2), where r2 is as specified below, which results in op_state3. It calls execution::start(op_state3). If any of these throws an exception, it calls execution::set_error on out_r, passing current_exception() as the second argument.
-
-
execution::set_error(r,e) is expression-equivalent to execution::set_error(out_r,e).
-
-
execution::set_stopped(r) is expression-equivalent to execution::set_stopped(out_r).
-
-
execution::get_env(r) is expression-equivalent to execution::get_env(out_r).
-
-
-
Calls execution::schedule(sch), which results in s2. It then calls execution::connect(s2,r), resulting in op_state2.
-
-
op_state2 is wrapped by a new operation state, op_state1, that is returned to the caller.
-
-
r2 is a receiver that wraps a reference to out_r and forwards all
-receiver completion-signals to it. In addition, execution::get_env(r2) returns replace-scheduler(e,sch).
-
-
When execution::start is called on op_state1, it calls execution::start on op_state2.
-
-
The lifetime of op_state2, once constructed, lasts until either op_state3 is constructed or op_state1 is destroyed, whichever comes first. The lifetime of op_state3, once constructed, lasts until op_state1 is destroyed.
-
-
-
Given subexpressions s1 and e, where s1 is a sender returned from on or a copy of such, let S1 be decltype((s1)).
-Let E' be decltype((replace-scheduler(e,sch))).
-Then the type of tag_invoke(get_completion_signatures,s1,e) shall be:
where no-value-completions<As...> names the type completion_signatures<> for any set of types As....
-
-
-
10.8.5.4. execution::transfer[exec.transfer]
-
-
-
execution::transfer is used to adapt a sender into a sender with a different associated set_value completion scheduler. [Note: it results in a transition between different execution contexts when executed. --end note]
-
-
The name execution::transfer denotes a customization point object. For some subexpressions sch and s, let Sch be decltype((sch)) and S be decltype((s)). If Sch does not satisfy execution::scheduler, or S does not satisfy execution::sender, execution::transfer is ill-formed. Otherwise, the expression execution::transfer(s,sch) is expression-equivalent to:
-
-
-
tag_invoke(execution::transfer,get_completion_scheduler<set_value_t>(s),s,sch), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, tag_invoke(execution::transfer,s,sch), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies 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 execution::schedule_from(sch,s2), where s2 is a sender which
-sends values equivalent to those sent by s, the behavior of calling execution::transfer(s,sch) is undefined.
-
-
Senders returned from execution::transfer shall not propagate the sender queries get_completion_scheduler<CPO> to an input sender. They will implement get_completion_scheduler<CPO>, where CPO is one of set_value_t and set_stopped_t; this query returns a scheduler equivalent to the sch argument from those queries. The get_completion_scheduler<set_error_t> is not implemented, as the scheduler cannot be guaranteed in case an error is thrown while trying to schedule work on the given scheduler object.
execution::schedule_from is used to schedule work dependent on the completion of a sender onto a scheduler’s associated execution context. [Note: schedule_from is not meant to be used in user code; it is used in the implementation of transfer. -end note]
-
-
The name execution::schedule_from denotes a customization point object. For some subexpressions sch and s, let Sch be decltype((sch)) and S be decltype((s)). If Sch does not satisfy execution::scheduler, or S does not satisfy execution::sender, execution::schedule_from is ill-formed. Otherwise, the expression execution::schedule_from(sch,s) is expression-equivalent to:
-
-
-
tag_invoke(execution::schedule_from,sch,s), if that expression is valid. If the function selected by tag_invoke does not return a sender which completes on an execution agent belonging to the associated
-execution context of sch and sends signals equivalent to those sent by s, the behavior of calling execution::schedule_from(sch,s) is undefined.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r such that when a receiver completion-signal Signal(r,args...) is called, it decay-copies args... into op_state (see below) as args'... and constructs a receiver r2 such that:
-
-
-
When execution::set_value(r2) is called, it calls Signal(out_r,std::move(args')...).
-
-
execution::set_error(r2,e) is expression-equivalent to execution::set_error(out_r,e).
-
-
execution::set_stopped(r2) is expression-equivalent to execution::set_stopped(out_r).
-
-
It then calls execution::schedule(sch), resulting in a sender s3. It then calls execution::connect(s3,r2), resulting in an operation state op_state3. It then calls execution::start(op_state3). If any of these throws an exception,
-it catches it and calls execution::set_error(out_r,current_exception()). If any of these expressions would be ill-formed, then Signal(r,args...) is ill-formed.
-
-
Calls execution::connect(s,r) resulting in an operation state op_state2. If this expression would be ill-formed, execution::connect(s2,out_r) is ill-formed.
-
-
Returns an operation state op_state that contains op_state2. When execution::start(op_state) is called, calls execution::start(op_state2). The lifetime of op_state3 ends when op_state is destroyed.
-
-
-
Given subexpressions s2 and e, where s2 is a sender returned from schedule_from or a copy of such, let S2 be decltype((s2)) and let E be decltype((e)). Then the type of tag_invoke(get_completion_signatures,s2,e) shall be:
-
make_completion_signatures<
- copy_cvref_t<S2,S>,
- E,
- make_completion_signatures<
- schedule_result_t<Sch>,
- E,
- completion_signatures<set_error_t(exception_ptr)>,
- no-value-completions>>;
-
-
where no-value-completions<As...> names the type completion_signatures<> for any set of types As....
-
-
-
Senders returned from execution::schedule_from shall not propagate the sender queries get_completion_scheduler<CPO> to an input sender. They will implement get_completion_scheduler<CPO>, where CPO is one of set_value_t and set_stopped_t; this query returns a scheduler equivalent to the sch argument from those queries. The get_completion_scheduler<set_error_t> is not implemented, as the scheduler cannot be guaranteed in case an error is thrown while trying to schedule work on the given scheduler object.
-
-
10.8.5.6. execution::then[exec.then]
-
-
-
execution::then is used to attach an invocable as a continuation for the successful completion of the input sender.
-
-
The name execution::then denotes a customization point object. For some
-subexpressions s and f, let S be decltype((s)), let F be the
-decayed type of f, and let f' be an xvalue refering to an object
-decay-copied from f. If S does not satisfy execution::sender, or F does not model movable-value, execution::then is
-ill-formed. Otherwise, the expression execution::then(s,f) is
-expression-equivalent to:
-
-
-
tag_invoke(execution::then,get_completion_scheduler<set_value_t>(s),s,f), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, tag_invoke(execution::then,s,f), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r such that:
-
-
-
When execution::set_value(r,args...) is called, let v be the
-expression invoke(f',args...). If decltype(v) is void,
-calls execution::set_value(out_r); otherwise, it calls execution::set_value(out_r,v). If any of these throw an
-exception, it catches it and calls execution::set_error(out_r,current_exception()). If any of these expressions would be
-ill-formed, the expression execution::set_value(r,args...) is
-ill-formed.
-
-
execution::set_error(r,e) is expression-equivalent to execution::set_error(out_r,e).
-
-
execution::set_stopped(r) is expression-equivalent to execution::set_stopped(out_r).
-
-
-
Returns an expression equivalent to execution::connect(s,r).
-
-
Let compl-sig-t<Tag,Args...> name the type Tag() if Args... is a template paramter pack containing the
-single type void; otherwise, Tag(Args...). Given
-subexpressions s2 and e where s2 is a sender returned from then or a copy of such, let S2 be decltype((s2)) and let E be decltype((e)). The type of tag_invoke(get_completion_signatures,s2,e) shall be equivalent
-to:
and set-error-signature is an alias for completion_signatures<set_error_t(exception_ptr)> if any of the types
-in the type-list named by value_types_of_t<copy_cvref_t<S2,S>,E,potentially-throwing,type-list> are true_type; otherwise, completion_signatures<>, where potentially-throwing is the template alias:
If the function selected above does not return a sender that invokes f with the result of the set_value signal of s, passing the return value as the value to any connected receivers, and propagates the other completion-signals sent by s, the behavior of calling execution::then(s,f) is undefined.
-
-
10.8.5.7. execution::upon_error[exec.upon_error]
-
-
-
execution::upon_error is used to attach an invocable as a continuation for the unsuccessful completion of the input sender.
-
-
The name execution::upon_error denotes a customization point object. For
-some subexpressions s and f, let S be decltype((s)), let F be the
-decayed type of f, and let f' be an xvalue refering to an object
-decay-copied from f. If S does not satisfy execution::sender, or F does not model movable-value, execution::upon_error is
-ill-formed. Otherwise, the expression execution::upon_error(s,f) is
-expression-equivalent to:
-
-
-
tag_invoke(execution::upon_error,get_completion_scheduler<set_error_t>(s),s,f), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, tag_invoke(execution::upon_error,s,f), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r such that:
-
-
-
execution::set_value(r,args...) is expression-equivalent to execution::set_value(out_r,args...).
-
-
When execution::set_error(r,e) is called, let v be the
-expression invoke(f',e). If decltype(v) is void, calls execution::set_value(out_r); otherwise, it calls execution::set_value(out_r,v). If any of these throw an
-exception, it catches it and calls execution::set_error(out_r,current_exception()). If any of these expressions would be
-ill-formed, the expression execution::set_error(r,e) is
-ill-formed.
-
-
execution::set_stopped(r) is expression-equivalent to execution::set_stopped(out_r).
-
-
-
Returns an expression equivalent to execution::connect(s,r).
-
-
Let compl-sig-t<Tag,Args...> name the type Tag() if Args... is a template paramter pack containing the
-single type void; otherwise, Tag(Args...). Given
-subexpressions s2 and e where s2 is a sender returned from upon_error or a copy of such, let S2 be decltype((s2)) and let E be decltype((e)). The type of tag_invoke(get_completion_signatures,s2,e) shall be equivalent
-to:
and set-error-signature is an alias for completion_signatures<set_error_t(exception_ptr)> if any of the types
-in the type-list named by error_types_of_t<copy_cvref_t<S2,S>,E,potentially-throwing> are true_type; otherwise, completion_signatures<>, where potentially-throwing is the template alias:
If the function selected above does not return a sender which invokes f with the result of the set_error signal of s, passing the return value as the value to any connected receivers, and propagates the other completion-signals sent by s, the behavior of calling execution::upon_error(s,f) is undefined.
execution::upon_stopped is used to attach an invocable as a continuation for the completion of the input sender using the "stopped" channel.
-
-
The name execution::upon_stopped denotes a customization point object. For
-some subexpressions s and f, let S be decltype((s)), let F be the
-decayed type of f, and let f' be an xvalue refering to an object
-decay-copied from f. If S does not satisfy execution::sender, or F does not model both movable-value and invocable, execution::upon_stopped is ill-formed. Otherwise, the expression execution::upon_stopped(s,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::upon_stopped,get_completion_scheduler<set_stopped_t>(s),s,f), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, tag_invoke(execution::upon_stopped,s,f), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r such that:
-
-
-
execution::set_value(r,args...) is expression-equivalent to execution::set_value(out_r,args...).
-
-
execution::set_error(r,e) is expression-equivalent to execution::set_error(out_r,e).
-
-
When execution::set_stopped(r) is called, let v be the
-expression invoke(f'). If v has type void, calls execution::set_value(out_r); otherwise, calls execution::set_value(out_r,v). If any of these throw an
-exception, it catches it and calls execution::set_error(out_r,current_exception()). If any of these expressions would be
-ill-formed, the expression execution::set_stopped(r) is
-ill-formed.
-
-
-
Returns an expression equivalent to execution::connect(s,r).
-
-
Let compl-sig-t<Tag,Args...> name the type Tag() if Args... is a template paramter pack containing the
-single type void; otherwise, Tag(Args...). Given
-subexpressions s2 and e where s2 is a sender returned from upon_stopped or a copy of such, let S2 be decltype((s2)) and let E be decltype((e)). The type of tag_invoke(get_completion_signatures,s2,e) shall be equivalent
-to:
where set-stopped-completions names the type completion_signatures<compl-sig-t<set_value_t,invoke_result_t<F>>, and set-error-signature names the type completion_signatures<set_error_t(exception_ptr)> if is_nothrow_invocable_v<F> is true, or completion_signatures<> otherwise.
-
-
-
If the function selected above does not return a sender which invokes f when s completes by calling set_stopped, passing the return value as the value to any connected receivers, and propagates the other completion-signals sent by s, the behavior of calling execution::upon_stopped(s,f) is undefined.
execution::let_value is used to insert continuations creating more work dependent on the results of their input senders into a sender chain. execution::let_error is used to insert continuations creating more work dependent on the error of its input senders into a sender chain. execution::let_stopped is used to insert continuations creating more work dependent on the stopped signal of its input senders into a sender chain.
-
-
The names execution::let_value, execution::let_error, and execution::let_stopped denote a customization point object.
-Let the expression let-cpo be one of execution::let_value, execution::let_error, or execution::let_stopped.
-For some subexpressions s and f, let S be decltype((s)), let F be the decayed type of f, and let f' be an xvalue that refers to an object decay-copied from f.
-If S does not satisfy execution::sender, the expression let-cpo(s,f) is ill-formed.
-If F does not satisfy invocable, the expression execution::let_stopped(s,f) is ill-formed.
-Otherwise, the expression let-cpo(s,f) is expression-equivalent to:
-
-
-
tag_invoke(let-cpo,get_completion_scheduler<set_value_t>(s),s,f), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, tag_invoke(let-cpo,s,f), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, given a receiver out_r and an lvalue out_r' refering to an object decay-copied from out_r.
-
-
-
For execution::let_value, let set-cpo be execution::set_value.
-For execution::let_error, let set-cpo be execution::set_error.
-For execution::let_stopped, let set-cpo be execution::set_stopped.
-Let signal be one of execution::set_value, execution::set_error, or execution::set_stopped.
-
-
Let r be an rvalue of a receiver type R such that:
-
-
-
When set-cpo(r,args...) is called, the receiver r decay-copies args... into op_state2 as args'..., then calls invoke(f',args'...), resulting in a sender s3.
-It then calls execution::connect(s3,std::move(out_r')), resulting in an operation state op_state3. op_state3 is saved as a part of op_state2.
-It then calls execution::start(op_state3).
-If any of these throws an exception, it catches it and calls execution::set_error(std::move(out_r'),current_exception()).
-If any of these expressions would be ill-formed, set-cpo(r,args...) is ill-formed.
-
-
signal(r,args...) is expression-equivalent to signal(std::move(out_r'),args...), when signal is different from set-cpo.
-
-
-
let-cpo(s,f) returns a sender s2 such that:
-
-
-
If the expression execution::connect(s,r) is ill-formed, execution::connect(s2,out_r) is ill-formed.
-
-
Otherwise, let op_state2 be the result of execution::connect(s,r). execution::connect(s2,out_r) returns an operation state op_state that stores op_state2. execution::start(op_state) is expression-equivalent to execution::start(op_state2).
-
-
-
Given subexpressions s2 and e, where s2 is a sender returned
-from let-cpo(s,f) or a copy of such, let S2 be decltype((s2)), let E be decltype((e)), and let S' be copy_cvref_t<S2,S>. Then the type of tag_invoke(get_completion_signatures,s2,e) is specified as
-follows:
-
-
-
If sender<S',E> is false, the type of tag_invoke(get_completion_signatures,s2,e) shall be equivalent
-to dependent_completion_signatures<E>.
-
-
Otherwise, let Sigs... be the set of template arguments of the completion_signatures specialization named by completion_signatures_of_t<S',E>,
-let Sigs2... be the set of function types in Sigs... whose return type
-is set-cpo, and let Rest... be the set of function types
-in Sigs... but not Sigs2....
-
-
For each Sig2i in Sigs2..., let Vsi... be the set of function
-arguments in Sig2i and let S3i be invoke_result_t<F,decay_t<Vsi>&...>. If S3i is ill-formed, or if sender<S3i,E> is not satisfied,
-then the type of tag_invoke(get_completion_signatures,s2,e) shall be equivalent to dependent_completion_signatures<E>.
-
-
Otherwise, let Sigs3i... be the
-set of template arguments of the completion_signatures specialization named by completion_signatures_of_t<S3i,E>. Then the type of tag_invoke(get_completion_signatures,s2,e) shall be equivalent to completion_signatures<Sigs30...,Sigs31...,...Sigs3n-1...,Rest...,set_error_t(exception_ptr)>, where n is sizeof...(Sigs2).
-
-
-
-
If let-cpo(s,f) does not return a sender that invokes f when set-cpo is called, and makes its completion dependent on the completion of a sender returned by f, and propagates the other completion-signals sent by s, the behavior of calling let-cpo(s,f) is undefined.
-
-
10.8.5.10. execution::bulk[exec.bulk]
-
-
-
execution::bulk is used to run a task repeatedly for every index in an index space.
-
-
The name execution::bulk denotes a customization point object. For some
-subexpressions s, shape, and f, let S be decltype((s)), Shape be decltype((shape)), and F be decltype((f)). If S does not satisfy execution::sender or Shape does not satisfy integral, execution::bulk is ill-formed. Otherwise, the expression execution::bulk(s,shape,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::bulk,get_completion_scheduler<set_value_t>(s),s,shape,f), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, tag_invoke(execution::bulk,s,shape,f), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r:
-
-
-
When execution::set_value(r,args...) is called, calls f(i,args...) for each i of type Shape from 0 to shape, then calls execution::set_value(out_r,args...). If any of these throws an exception, it catches it and calls execution::set_error(out_r,current_exception()).
-
-
When execution::set_error(r,e) is called, calls execution::set_error(out_r,e).
-
-
When execution::set_stopped(r) is called, calls execution::set_stopped(out_r,e).
-
-
-
Calls execution::connect(s,r), which results in an operation state op_state2.
-
-
Returns an operation state op_state that contains op_state2. When execution::start(op_state) is called, calls execution::start(op_state2).
-
-
Given subexpressions s2 and e where s2 is a sender returned
-from bulk or a copy of such, let S2 be decltype((s2)), let E be decltype((e)), let S' be copy_cvref_t<S2,S> and let nothrow-callable be the alias template:
If any of the types in the type-list named by value_types_of_t<S',E,nothrow-callable,type-list> are false_type, then the type of tag_invoke(get_completion_signatures,s2,e) shall be
-equivalent to:
Otherwise, the type of tag_invoke(get_completion_signatures,s2,e) shall be equivalent to completion_signatures_of_t<S',E>.
-
-
-
-
If the function selected above does not return a sender which invokes f(i,args...) for each i of type Shape from 0 to shape when
-the input sender sends values args..., or does not propagate the
-values of the signals sent by the input sender to a connected receiver,
-the behavior of calling execution::bulk(s,shape,f) is undefined.
-
-
-
10.8.5.11. execution::split[exec.split]
-
-
-
execution::split is used to adapt an arbitrary sender into a sender that can be connected multiple times.
-
-
Let split-env be the type of an execution environment such that, given an instance e, the expression get_stop_token(e) is well formed and has type stop_token.
-
-
The name execution::split denotes a customization point object. For some
-subexpression s, let S be decltype((s)). If execution::sender<S,split-env> is false, execution::split is ill-formed. Otherwise, the expression execution::split(s) is expression-equivalent to:
-
-
-
tag_invoke(execution::split,get_completion_scheduler<set_value_t>(s),s),
-if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, tag_invoke(execution::split,s), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, constructs a sender s2, which:
-
-
-
Creates an object sh_state that contains a stop_source, a list of
-pointers to operation states awaiting the completion of s, and that
-also reserves space for storing:
-
-
-
the operation state that results from connecting s with r described below, and
-
-
the sets of values and errors with which s may complete, with
-the addition of exception_ptr.
-
-
-
Constructs a receiver r such that:
-
-
-
When execution::set_value(r,args...) is called, decay-copies
-the expressions args... into sh_state. It then notifies all
-the operation states in sh_state's list of operation states
-that the results are ready. If any exceptions are thrown, the
-exception is caught and execution::set_error(r,current_exception()) is called instead.
-
-
When execution::set_error(r,e) is called, decay-copies e into sh_state. It then notifies the operation states in sh_state's list of operation states that the results are ready.
-
-
When execution::set_stopped(r) is called, it then notifies the
-operation states in sh_state's list of operation states that
-the results are ready.
-
-
get_env(r) is an expression e of type split-env such that execution::get_stop_token(e) is well-formed
-and returns the results of calling get_token() on sh_state's
-stop source.
-
-
-
Calls execution::connect(s,r), resulting in an operation state op_state2. op_state2 is saved in sh_state.
-
-
When s2 is connected with a receiver out_r of type OutR, it
-returns an operation state object op_state that contains:
-
-
-
An object out_r' of type OutR decay-copied from out_r,
-
-
A reference to sh_state,
-
-
A stop callback of type optional<stop_token_of_t<env_of_t<OutR>>::callback_type<stop-callback-fn>>,
-where stop-callback-fn is an implementation
-defined class type equivalent to the following:
If r's receiver contract has already been satisfied, then let Signal be whichever receiver completion-signal
-was used to complete r’s receiver contract ([exec.recv]). Calls Signal(out_r',args2...), where args2... is a
-pack of const lvalues referencing the subobjects of sh_state that have
-been saved by the original call to Signal(r,args...) and returns.
-
-
Otherwise, it emplace constructs the stop callback optional with
-the arguments execution::get_stop_token(get_env(out_r')) and stop-callback-fn{stop-src}, where stop-src refers to the stop source of sh_state.
-
-
Then, it checks to see if stop-src.stop_requested() is true. If so, it
-calls execution::set_stopped(out_r').
-
-
Otherwise, it adds a pointer to op_state to the list of
-operation states in sh_state and calls execution::start(op_state2) if this would be the first such
-invocation.
-
-
-
When r completes it will notify op_state that the result are
-ready. Let Signal be whichever receiver
-completion-signal was used to complete r's receiver contract
-([exec.recv]). op_state's stop callback optional is reset. Then Signal(std::move(out_r'),args2...) is called,
-where args2... is a pack of const lvalues referencing the subobjects of sh_state that have been saved by the original call to Signal(r,args...).
-
-
Ownership of sh_state is shared by s2 and by every op_state that results from connecting s2 to a receiver.
-
-
Given subexpressions s2 and e where s2 is a sender returned
-from split or a copy of such, let S2 be decltype((s2)) and let E be decltype((e)). The type of tag_invoke(get_completion_signatures,s2,e) shall be equivalent
-to:
- Does not expose the sender queries get_completion_scheduler
- .
-
-
-
-
If the function selected above does not return a sender which sends
-references to values sent by s, propagating the other channels, the
-behavior of calling execution::split(s) is undefined.
-
-
-
10.8.5.12. execution::when_all[exec.when_all]
-
-
-
execution::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_with_variant 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.
-
-
The name execution::when_all denotes a customization point object. For some subexpressions si..., let Si... be decltype((si)).... The expression execution::when_all(si...) is ill-formed if any of the following is true:
-
-
-
If the number of subexpressions si... is 0, or
-
-
If any type Si does not satisfy execution::sender.
-
-
Otherwise, the expression execution::when_all(si...) is expression-equivalent to:
-
-
-
tag_invoke(execution::when_all,si...), if
-that expression is valid. If the function selected by tag_invoke does
-not return a sender that sends a concatenation of values sent by si... when they all complete with set_value, the behavior of calling execution::when_all(si...) is undefined.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, constructs a sender w of type W. When w is connected
-with some receiver out_r of type OutR, it returns an operation state op_state specified as below:
-
-
-
For each sender si, constructs a receiver ri such that:
-
-
-
If execution::set_value(ri,ti...) is called for every ri, op_state's associated stop callback optional is reset and execution::set_value(out_r,t0...,t1...,...,tn-1...) is called, where n the number of subexpressions in si....
-
-
Otherwise, execution::set_error or execution::set_stopped was called for at least one receiver ri. If the first such to complete did so with the call execution::set_error(ri,e), request_stop is called on op_state's associated stop source. When all child operations have completed, op_state's associated stop callback optional is reset and execution::set_error(out_r,e) is called.
-
-
Otherwise, request_stop is called on op_state's associated stop source. When all child operations have completed, op_state's associated stop callback optional is reset and execution::set_stopped(out_r) is called.
-
-
For each receiver ri, get_env(ri) is an expression e such that execution::get_stop_token(e) is well-formed and returns the results of calling get_token() on op_state's associated stop source, and for which tag_invoke(tag,e,args...) is expression-equivalent to tag(get_env(out_r),args...) for all arguments args... and all tag whose type satisfies forwarding-env-query and is not get_stop_token_t.
-
-
-
For each sender si, calls execution::connect(si,ri), resulting in operation states child_opi.
-
-
Returns an operation state op_state that contains:
-
-
-
Each operation state child_opi,
-
-
A stop source of type in_place_stop_source,
-
-
A stop callback of type optional<stop_token_of_t<env_of_t<OutR>>::callback_type<stop-callback-fn>>, where stop-callback-fn is an implementation defined class type equivalent to the following:
Emplace constructs the stop callback optional with the arguments execution::get_stop_token(get_env(out_r)) and stop-callback-fn{stop-src}, where stop-src refers to the stop source of op_state.
-
-
Then, it checks to see if stop-src.stop_requested() is true. If so, it calls execution::set_stopped(out_r).
-
-
Otherwise, calls execution::start(child_opi) for each child_opi.
-
-
-
Given subexpressions s2 and e where s2 is a sender returned
-from when_all or a copy of such, let S2 be decltype((s2)), let E be decltype((e)), and let Ss... be the decayed types of the
-arguments to the when_all expression that created s2. If the
-decayed type of e is no_env, let WE be no_env; otherwise,
-let WE be a type such that stop_token_of_t<WE> is in_place_stop_token and tag_invoke_result_t<Tag,WE,As...> names the type, if any, of call-result-t<Tag,E,As...> for all types As... and all types Tag besides get_stop_token_t. The type of tag_invoke(get_completion_signatures,s2,e) shall be as follows:
-
-
-
For each type Si in Ss..., let S'i name the type copy_cvref_t<S2,Si>. If for
-any type S'i, the type completion_signatures_of_t<S'i,WE> names a type other than an instantiation of completion_signatures, the type of tag_invoke(get_completion_signatures,s2,e) shall be dependent_completion_signatures<E>.
-
-
Otherwise, for each type S'i, let Sigsi... be the set of template
-arguments in the instantiation of completion_signatures named
-by completion_signatures_of_t<S'i,WE>, and let Ci be the
-count of function types in Sigsi... for which the return
-type is set_value_t. If any Ci is two or greater, then the
-type of tag_invoke(get_completion_signatures,s2,e) shall be dependent_completion_signatures<E>.
-
-
Otherwise, let Sigs2i... be the set of
-function types in Sigsi... whose
-return types are notset_value_t, and let Ws... be
-the unique set of types in [Sigs20...,Sigs21...,...Sigs2n-1...,set_stopped_t()], where n is sizeof...(Ss). If any Ci is 0, then the type of tag_invoke(get_completion_signatures,s2,e) shall be completion_signatures<Ws...>.
-
-
Otherwise, let Vi... be the
-function argument types of the single type in Sigsi... for which the return
-type is set_value_t. Then the type of tag_invoke(get_completion_signatures,s2,e) shall be completion_signatures<Ws...,set_value_t(decay_t<V0>&&...,decay_t<V1>&&...,...decay_t<Vn-1>&&...)>.
-
-
-
-
-
The name execution::when_all_with_variant denotes a customization point object. For some subexpressions s..., let S be decltype((s)). If any type Si in S... does not satisfy execution::sender, execution::when_all_with_variant is ill-formed. Otherwise, the expression execution::when_all_with_variant(s...) is expression-equivalent to:
-
-
-
tag_invoke(execution::when_all_with_variant,s...), if that expression
-is valid. If the function selected by tag_invoke does not return a
-sender that, when connected with a receiver of type R, sends the types into-variant-type<S,env_of_t<R>>... when they
-all complete with set_value, the behavior of calling execution::when_all(si...) is undefined.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
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
-that only send a single set of values each, while also making sure that they
-complete on the specified scheduler. execution::transfer_when_all_with_variant 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]
-
-
The name execution::transfer_when_all denotes a customization point object. For some subexpressions sch and s..., let Sch be decltype(sch) and S be decltype((s)). If Sch does not satisfy scheduler, or any type Si in S... does not satisfy execution::sender, execution::transfer_when_all is ill-formed. Otherwise, the expression execution::transfer_when_all(sch,s...) is expression-equivalent to:
-
-
-
tag_invoke(execution::transfer_when_all,sch,s...), if that expression
-is valid. If the function selected by tag_invoke does not return a
-sender that sends a concatenation of values sent by s... when they all
-complete with set_value, 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 of sch, the behavior of
-calling execution::transfer_when_all(sch,s...) is undefined.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
The name execution::transfer_when_all_with_variant denotes a customization
-point object. For some subexpressions sch and s..., let Sch be decltype((sch)) and let S be decltype((s)). If any type Si in S... does not satisfy execution::sender, execution::transfer_when_all_with_variant is
-ill-formed. Otherwise, the expression execution::transfer_when_all_with_variant(sch,s...) is expression-equivalent
-to:
-
-
-
tag_invoke(execution::transfer_when_all_with_variant,s...), if that
-expression is valid. If the function selected by tag_invoke does not
-return a sender that, when connected with a receiver of type R, sends
-the types into-variant-type<S,env_of_t<R>>... when they all complete with set_value, the behavior
-of calling execution::transfer_when_all_with_variant(sch,s...) is undefined.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
Senders returned from execution::transfer_when_all shall not propagate the sender queries get_completion_scheduler<CPO> to input senders. They will implement get_completion_scheduler<CPO>, where CPO is one of set_value_t and set_stopped_t; this query returns a scheduler equivalent to the sch argument from those queries. The get_completion_scheduler<set_error_t> is not implemented, as the scheduler cannot be guaranteed in case an error is thrown while trying to schedule work on the given scheduler object.
execution::into_variant can be used to turn a sender which sends multiple sets of values into a sender which sends a variant of all of those sets of values.
-
-
The template into-variant-type is used to compute the type sent by a sender returned from execution::into_variant.
execution::into_variant is a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::sender, execution::into_variant(s) is ill-formed. Otherwise, execution::into_variant(s) returns
-a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r:
-
-
-
If execution::set_value(r,ts...) is called, calls execution::set_value(out_r,into-variant-type<S,env_of_t<decltype((r))>>(decayed-tuple<decltype(ts)...>(ts...))). If this expression throws an exception, calls execution::set_error(out_r,current_exception()).
-
-
execution::set_error(r,e) is expression-equivalent to execution::set_error(out_r,e).
-
-
execution::set_stopped(r) is expression-equivalent to execution::set_stopped(out_r).
-
-
-
Calls execution::connect(s,r), resulting in an operation state op_state2.
-
-
Returns an operation state op_state that contains op_state2. When execution::start(op_state) is called, calls execution::start(op_state2).
-
-
Given subexpressions s2 and e, where s2 is a sender returned from into_variant or a copy of such, let S2 be decltype((s2)) and E be decltype((e)).
-Let into-variant-set-value be the class template:
Let INTO-VARIANT-ERROR-SIGNATURES(S,E) be completion_signatures<set_error_t(exception_ptr)> if any of the types in the type-list named by value_types_of_t<S,E,into-variant-is-nothrow<S,E>::templateapply,type-list> are false_type; otherwise, completion_signatures<>.
-
The type of tag_invoke(get_completion_signatures_t{},s2,e)) shall be equivalent to:
execution::stopped_as_optional is used to handle a stopped 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 stopped, reporting cancellation by completing with an empty optional.
-
-
The name execution::stopped_as_optional denotes a customization point object. For some subexpression s, let S be decltype((s)). Let get-env-sender be an expression such that, when it is connected with a receiver r, start on the resulting operation state completes immediately by calling execution::set_value(r,get_env(r)). The expression execution::stopped_as_optional(s) is expression-equivalent to:
execution::stopped_as_error is used to handle a stopped signal by mapping it into the error channel as a custom exception type. The result is a sender that never completes with stopped, reporting cancellation by completing with an error.
-
-
The name execution::stopped_as_error denotes a customization point object. For some subexpressions s and e, let S be decltype((s)) and let E be decltype((e)). If the type S does not satisfy sender or if the type E doesn’t satisfy movable-value, execution::stopped_as_error(s,e) is ill-formed. Otherwise, the expression execution::stopped_as_error(s,e) is expression-equivalent to:
execution::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.
-
-
Let ensure-started-env be the type of an execution
-environment such that, given an instance e, the expression get_stop_token(e) is well formed and has type stop_token.
-
-
The name execution::ensure_started denotes a customization point object.
-For some subexpression s, let S be decltype((s)). If execution::sender<S,ensure-started-env> is false, execution::ensure_started(s) is ill-formed. Otherwise, the
-expression execution::ensure_started(s) is expression-equivalent to:
-
-
-
tag_invoke(execution::ensure_started,get_completion_scheduler<set_value_t>(s),s), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, tag_invoke(execution::ensure_started,s), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, constructs a sender s2, which:
-
-
-
Creates an object sh_state that contains a stop_source, an
-initially null pointer to an operation state awaitaing completion,
-and that also reserves space for storing:
-
-
-
the operation state that results from connecting s with r described below, and
-
-
the sets of values and errors with which s may complete, with
-the addition of exception_ptr.
-
-
s2 shares ownership of sh_state with r described below.
-
-
Constructs a receiver r such that:
-
-
-
When execution::set_value(r,args...) is called, decay-copies
-the expressions args... into sh_state. It then checks sh_state to see if there is an operation state awaiting
-completion; if so, it notifies the operation state that the
-results are ready. If any exceptions are thrown, the exception
-is caught and execution::set_error(r,current_exception()) is
-called instead.
-
-
When execution::set_error(r,e) is called, decay-copies e into sh_state. If there is an operation state awaiting completion,
-it then notifies the operation states that the results are ready.
-
-
When execution::set_stopped(r) is called, it then notifies any
-awaiting operation state that the results are ready.
-
-
get_env(r) is an expression e of type ensure-started-env such that execution::get_stop_token(e) is well-formed
-and returns the results of calling get_token() on sh_state's
-stop source.
-
-
r shares ownership of sh_state with s2. After r's
-receiver contract has been completed, it releases its ownership
-of sh_state.
-
-
-
Calls execution::connect(s,r), resulting in an operation state op_state2. op_state2 is saved in sh_state. It then calls execution::start(op_state2).
-
-
When s2 is connected with a receiver out_r of type OutR, it
-returns an operation state object op_state that contains:
-
-
-
An object out_r' of type OutR decay-copied from out_r,
-
-
A reference to sh_state,
-
-
A stop callback of type optional<stop_token_of_t<env_of_t<OutR>>::callback_type<stop-callback-fn>>,
-where stop-callback-fn is an implementation
-defined class type equivalent to the following:
s2 transfers its ownership of sh_state to op_state.
-
-
When execution::start(op_state) is called:
-
-
-
If r's receiver contract has already been satisfied, then let Signal be whichever receiver completion-signal
-was used to complete r's receiver contract ([exec.recv]). Calls Signal(out_r',args2...), where args2... is a
-pack of xvalues referencing the subobjects of sh_state that have
-been saved by the original call to Signal(r,args...) and returns.
-
-
Otherwise, it emplace constructs the stop callback optional with
-the arguments execution::get_stop_token(get_env(out_r')) and stop-callback-fn{stop-src}, where stop-src refers to the stop source of sh_state.
-
-
Then, it checks to see if stop-src.stop_requested() is true. If so, it
-calls execution::set_stopped(out_r').
-
-
Otherwise, it sets sh_state operation state pointer to the
-address of op_state, registering itself as awaiting the result
-of the completion of r.
-
-
-
When r completes it will notify op_state that the result are
-ready. Let Signal be whichever receiver
-completion-signal was used to complete r's receiver contract
-([exec.recv]). op_state's stop callback optional is reset. Then Signal(std::move(out_r'),args2...) is called,
-where args2... is a pack of xvalues referencing the subobjects of sh_state that have been saved by the original call to Signal(r,args...).
-
-
[Note: If sender s2 is destroyed without being connected to a
-receiver, or if it is connected but the operation state is destroyed
-without having been started, then when r's receiver contract
-completes and it releases its shared ownership of sh_state, sh_state will be destroyed and the results of the operation are
-discarded. -- end note]
-
-
-
Given subexpressions s2 and e where s2 is a sender returned
-from ensure_started or a copy of such, let S2 be decltype((s2)) and let E be decltype((e)). The type of tag_invoke(get_completion_signatures,s2,e) shall be equivalent
-to:
If the function selected above does not return a sender that sends xvalue
-references to values sent by s, propagating the other channels, the
-behavior of calling execution::ensure_started(s) is undefined.
execution::start_detached is used to eagerly start a sender without the caller needing to manage the lifetimes of any objects.
-
-
The name execution::start_detached denotes a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::sender, execution::start_detached is ill-formed. Otherwise, the expression execution::start_detached(s) is expression-equivalent to:
-
-
-
tag_invoke(execution::start_detached,execution::get_completion_scheduler<execution::set_value_t>(s),s), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above is void.
-
-
-
Otherwise, tag_invoke(execution::start_detached,s), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above is void.
-
-
-
Otherwise:
-
-
-
Let R be the type of a receiver, let r be an rvalue of type R, and let cr be a
-lvalue reference to constR such that:
-
-
-
The expression set_value(r) is not potentially throwing and has no effect,
-
-
For any subexpression e, the expression set_error(r,e) is expression-equivalent
-to terminate(),
-
-
The expression set_stopped(r) is not potentially throwing and has no effect, and
-
-
The expression get_env(cr) is expression-equivalent to empty-env{}.
-
-
-
Calls execution::connect(s,r), resulting in an operation state op_state, then calls execution::start(op_state). The lifetime of op_state lasts until one of the receiver completion-signals of r is called.
-
-
-
If the function selected above does not eagerly start the sender s after
-connecting it with a receiver which ignores the set_value and set_stopped signals and calls terminate() on the set_error signal,
-the behavior of calling execution::start_detached(s) is undefined.
-
-
10.8.6.2. this_thread::sync_wait[exec.sync_wait]
-
-
-
this_thread::sync_wait and this_thread::sync_wait_with_variant 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.
-
-
For any receiver r created by an implementation of sync_wait and sync_wait_with_variant, the expressions get_scheduler(get_env(r)) and get_delegatee_scheduler(get_env(r)) shall be well-formed. For a receiver
-created by the default implementation of this_thread::sync_wait, these
-expressions shall return a scheduler to the same thread-safe,
-first-in-first-out queue of work such that tasks scheduled to the queue
-execute on the thread of the caller of sync_wait. [Note: The
-scheduler for an instance of execution::run_loop that is a local variable
-within sync_wait is one valid implementation. -- end note]
-
-
The templates sync-wait-type and sync-wait-with-variant-type are used to determine the
-return types of this_thread::sync_wait and this_thread::sync_wait_with_variant. Let sync-wait-env be the type of the expression get_env(r) where r is an instance of the
-receiver created by the default implementation of sync_wait.
The name this_thread::sync_wait denotes a customization point object. For
-some subexpression s, let S be decltype((s)). If execution::sender<S,sync-wait-env> is false,
-or the number of the arguments completion_signatures_of_t<S,sync-wait-env>::value_types passed into the Variant template
-parameter is not 1, this_thread::sync_wait is ill-formed. Otherwise, this_thread::sync_wait is expression-equivalent to:
-
-
-
tag_invoke(this_thread::sync_wait,execution::get_completion_scheduler<execution::set_value_t>(s),s), if this expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above is sync-wait-type<S,sync-wait-env>.
-
-
-
Otherwise, tag_invoke(this_thread::sync_wait,s), if this expression is valid and its type is.
-
-
-
Mandates: The type of the tag_invoke expression above is sync-wait-type<S,sync-wait-env>.
-
-
-
Otherwise:
-
-
-
Constructs a receiver r.
-
-
Calls execution::connect(s,r), resulting in an operation state op_state, then calls execution::start(op_state).
-
-
Blocks the current thread until a receiver completion-signal of r is called. When it is:
-
-
-
If execution::set_value(r,ts...) has been called, returns sync-wait-type<S,sync-wait-env>{decayed-tuple<decltype(ts)...>{ts...}}. If that expression exits exceptionally, the exception is propagated to the caller of sync_wait.
-
-
If execution::set_error(r,e) has been called, let E be the decayed type of e. If E is exception_ptr, calls std::rethrow_exception(e). Otherwise, if the E is error_code, throws system_error(e). Otherwise, throws e.
-
-
If execution::set_stopped(r) has been called, returns sync-wait-type<S,sync-wait-env>{}.
-
-
-
-
-
The name this_thread::sync_wait_with_variant denotes a customization point
-object. For some subexpression s, let S be the type of execution::into_variant(s). If execution::sender<S,sync-wait-env> is false, this_thread::sync_wait_with_variant is ill-formed. Otherwise, this_thread::sync_wait_with_variant is expression-equivalent to:
-
-
-
tag_invoke(this_thread::sync_wait_with_variant,execution::get_completion_scheduler<execution::set_value_t>(s),s), if this expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above is sync-wait-with-variant-type<S,sync-wait-env>.
-
-
-
Otherwise, tag_invoke(this_thread::sync_wait_with_variant,s), if this expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above is sync-wait-with-variant-type<S,sync-wait-env>.
execution::execute is used to create fire-and-forget tasks on a specified scheduler.
-
-
The name execution::execute denotes a customization point object. For some subexpressions sch and f, let Sch be decltype((sch)) and F be decltype((f)). If Sch does not satisfy execution::scheduler or F does not satisfy invocable, execution::execute is ill-formed. Otherwise, execution::execute is expression-equivalent to:
-
-
-
tag_invoke(execution::execute,sch,f), if that expression is valid. If
-the function selected by tag_invoke does not invoke the function f (or an object decay-copied from f) on an execution agent belonging to
-the associated execution context of sch, or if it does not call std::terminate if an error occurs after control is returned to the
-caller, the behavior of calling execution::execute is undefined.
-
-
-
Mandates: The type of the tag_invoke expression above is void.
template<
- class-typeDerived,
- receiverBase=unspecified>// arguments are not associated entities ([lib.tmpl-heads])
- classreceiver_adaptor;
-
-
-
-
receiver_adaptor is used to simplify the implementation of one receiver type in terms of another. It defines tag_invoke overloads that forward to named members if they exist, and to the adapted receiver otherwise.
-
-
If Base is an alias for the unspecified default template argument, then:
-
-
-
Let HAS-BASE be false, and
-
-
Let GET-BASE(d) be d.base().
-
-
otherwise, let:
-
-
-
Let HAS-BASE be true, and
-
-
Let GET-BASE(d) be c-style-cast<receiver_adaptor<Derived,Base>>(d).base().
-
-
Let BASE-TYPE(D) be the type of GET-BASE(declval<D>()).
-
-
receiver_adaptor<Derived,Base> is equivalent to the following:
[Note:receiver_adaptor provides tag_invoke overloads on behalf of
-the derived class Derived, which is incomplete when receiver_adaptor is
-instantiated.]
Let SET-VALUE be the expression std::move(self).set_value(std::forward<As>(as)...).
-
-
Constraints: Either SET-VALUE is a valid expression or typenameDerived::set_value denotes a type and callable<set_value_t,BASE-TYPE(Derived),As...> is true.
-
-
Mandates:SET-VALUE, if that expression is valid, is not potentially throwing.
Let SET-ERROR be the expression std::move(self).set_error(std::forward<E>(e)).
-
-
Constraints: Either SET-ERROR is a valid expression or typenameDerived::set_error denotes a type and callable<set_error_t,BASE-TYPE(Derived),E> is true.
-
-
Mandates:SET-ERROR, if that expression is valid, is not potentially throwing.
Let SET-STOPPED be the expression std::move(self).set_stopped().
-
-
Constraints: Either SET-STOPPED is a valid expression or typenameDerived::set_stopped denotes a type and callable<set_stopped_t,BASE-TYPE(Derived)> is true.
-
-
Mandates:SET-STOPPED, if that expression is valid, is not potentially throwing.
-
-
Effects: Equivalent to:
-
-
-
If SET-STOPPED is a valid expression, SET-STOPPED;
Constraints: Either self.get_env() is a valid expression or typenameDerived::get_env denotes a type and callable<get_env_t,BASE-TYPE(constDerived&)> is true.
-
-
Effects: Equivalent to:
-
-
-
If self.get_env() is a valid expression, self.get_env();
-
-
Otherwise, execution::get_env(GET-BASE(self)).
-
-
-
Remarks: The expression in the noexcept clause is:
-
-
-
If self.get_env() is a valid expression, noexcept(self.get_env());
completion_signatures is used to describe the completion signals of a receiver that
-a sender may invoke. Its template argument list is a list of function types corresponding
-to the signatures of the receiver’s completion signals.
-
-
[Example:
-
classmy_sender{
- usingcompletion_signatures=
- execution::completion_signatures<
- execution::set_value_t(),
- execution::set_value_t(int,float),
- execution::set_error_t(exception_ptr),
- execution::set_error_t(error_code),
- execution::set_stopped_t()>;
-};
-
-// Declares my_sender to be a sender that can complete by calling
-// one of the following for a receiver expression R:
-// execution::set_value(R)
-// execution::set_value(R, int{...}, float{...})
-// execution::set_error(R, exception_ptr{...})
-// execution::set_error(R, error_code{...})
-// execution::set_stopped(R)
-
-
-- end example]
-
-
This section makes use of the following exposition-only concept:
Let Fns... be a template parameter pack of the arguments of the completion_signatures instantiation named by completion_signatures_of_t<S,E>, let ValueFns be a
-template parameter pack of the function types in Fns whose return types
-are execution::set_value_t, and let Valuesn be a template parameter
-pack of the function argument types in the n-th type
-in ValueFns. Then, given two variadic templates Tuple and Variant, the type value_types_of_t<S,E,Tuple,Variant> names the type Variant<Tuple<Values0...>,Tuple<Values1...>,...Tuple<Valuesm-1...>>, where m is the size of the parameter pack ValueFns.
Let Fns... be a template parameter pack of the arguments of the completion_signatures instantiation named by completion_signatures_of_t<S,E>, let ErrorFns be a
-template parameter pack of the function types in Fns whose return types
-are execution::set_error_t, and let Errorn be the function argument
-type in the n-th type in ErrorFns. Then, given a variadic template Variant, the type error_types_of_t<S,E,Variant> names the type Variant<Error0,Error1,...Errorm-1>, where m is
-the size of the parameter pack ErrorFns.
Let Fns... be a template parameter pack of the arguments of the completion_signatures instantiation named by completion_signatures_of_t<S,E>. sends_stopped<S,E> is true if at
-least one of the types in Fns is execution::set_stopped_t();
-otherwise, false.
make_completion_signatures is an alias template used to adapt the
-completion signatures of a sender. It takes a sender, and environment, and
-several other template arguments that apply modifications to the sender’s
-completion signatures to generate a new instantiation of execution::completion_signatures.
-
-
[Example:
-
// Given a sender S and an environment Env, adapt a S’s completion
-// signatures by lvalue-ref qualifying the values, adding an additional
-// exception_ptr error completion if its not already there, and leaving the
-// other signals alone.
-template<class...Args>
- usingmy_set_value_t=
- execution::completion_signatures<
- execution::set_value_t(add_lvalue_reference_t<Args>...)>;
-
-usingmy_completion_signals=
- execution::make_completion_signatures<
- S,Env,
- execution::completion_signatures<execution::set_error_t(exception_ptr)>,
- my_set_value_t>;
-
-
-- end example]
-
-
This section makes use of the following exposition-only entities:
SetValue shall name an alias template such that for any template
-parameter pack As..., the type SetValue<As...> is either ill-formed
-or else valid-completion-signatures<SetValue<As...>,E> is satisfied.
-
-
SetError shall name an alias template such that for any type Err, SetError<Err> is either ill-formed or else valid-completion-signatures<SetError<Err>,E> is satisfied.
-
-
Then:
-
-
-
Let Vs... be a pack of the types in the type-list named
-by value_types_of_t<Sndr,Env,SetValue,type-list>.
-
-
Let Es... be a pack of the types in the type-list named by error_types_of_t<Sndr,Env,error-list>, where error-list is an
-alias template such that error-list<Ts...> names type-list<SetError<Ts>...>.
-
-
Let Ss name the type completion_signatures<> if sends_stopped<Sndr,Env> is false; otherwise, SetStopped.
-
-
Then:
-
-
-
If any of the above types are ill-formed, then make_completion_signatures<Sndr,Env,AddlSigs,SetValue,SetError,SetStopped> is ill-formed,
-
-
Otherwise, if any type in [AddlSigs,Vs...,Es...,Ss] is not an
-instantiation of completion_signatures, then make_completion_signatures<Sndr,Env,AddlSigs,SetValue,SetError,SetStopped> is an alias for dependent_completion_signatures<no_env>,
-
-
Otherwise, make_completion_signatures<Sndr,Env,AddlSigs,SetValue,SetError,SetStopped> names the type completion_signatures<Sigs...> where Sigs... is the unique set of types in all the template arguments
-of all the completion_signatures instantiations in [AddlSigs,Vs...,Es...,Ss].
-
-
-
10.11. Execution contexts [exec.ctx]
-
-
-
This section specifies some execution contexts on which work can be scheduled.
-
-
10.11.1. run_loop[exec.run_loop]
-
-
-
A run_loop is an execution context on which work can be scheduled. It maintains a simple, thread-safe first-in-first-out queue of work. Its run() member function removes elements from the queue and executes them in a loop on whatever thread of execution calls run().
-
-
A run_loop instance has an associated count that corresponds to the number of work items that are in its queue. Additionally, a run_loop has an associated state that can be one of starting, running, or finishing.
-
-
Concurrent invocations of the member functions of run_loop, other than run and its destructor, do not introduce data races. The member functions pop_front, push_back, and finish execute atomically.
-
-
[Note: Implementations are encouraged to use an intrusive queue of operation states to hold the work units to make scheduling allocation-free. — end note]
-
classrun_loop{
- // [exec.run_loop.types] Associated types
- classrun-loop-scheduler;// exposition only
- classrun-loop-sender;// exposition only
- structrun-loop-opstate-base{// exposition only
- virtualvoidexecute()=0;
- run_loop*loop_;
- run-loop-opstate-base*next_;
- };
- template<receiver_ofR>
- usingrun-loop-opstate=unspecified;// exposition only
-
- // [exec.run_loop.members] Member functions:
- run-loop-opstate-base*pop_front();// exposition only
- voidpush_back(run-loop-opstate-base*);// exposition only
-
- public:
- // [exec.run_loop.ctor] construct/copy/destroy
- run_loop()noexcept;
- run_loop(run_loop&&)=delete;
- ~run_loop();
-
- // [exec.run_loop.members] Member functions:
- run-loop-schedulerget_scheduler();
- voidrun();
- voidfinish();
-};
-
-
-
10.11.1.1. Associated types [exec.run_loop.types]
-
classrun-loop-scheduler;
-
-
-
-
run-loop-scheduler is an implementation defined type that models the scheduler concept.
-
-
Instances of run-loop-scheduler remain valid until the end of the lifetime of the run_loop instance from which they were obtained.
-
-
Two instances of run-loop-scheduler compare equal if and only if they were obtained from the same run_loop instance.
-
-
Let sch be an expression of type run-loop-scheduler. The expression execution::schedule(sch) is not potentially throwing and has type run-loop-sender.
-
-
classrun-loop-sender;
-
-
-
-
run-loop-sender is an implementation defined type that models the sender_of concept; i.e.,sender_of<run-loop-sender> is true. Additionally, the types reported by its error_types associated type is exception_ptr, and the value of its sends_stopped trait is true.
-
-
An instance of run-loop-sender remains valid until the end of the lifetime of its associated execution::run_loop instance.
-
-
Let s be an expression of type run-loop-sender, let r be an expression such that decltype(r) models the receiver_of concept, and let C be either set_value_t or set_stopped_t. Then:
-
-
-
The expression execution::connect(s,r) has type run-loop-opstate<decay_t<decltype(r)>> and is potentially throwing if and only if the initialiation of decay_t<decltype(r)> from r is potentially throwing.
-
-
The expression get_completion_scheduler<C>(s) is not potentially throwing, has type run-loop-scheduler, and compares equal to the run-loop-scheduler instance from which s was obtained.
-
-
-
template<receiver_ofR>// arguments are not associated entities ([lib.tmpl-heads])
- structrun-loop-opstate;
-
-
-
-
run-loop-opstate<R> inherits unambiguously from run-loop-opstate-base.
-
-
Let o be a non-const lvalue of type run-loop-opstate<R>, and let REC(o) be a non-const lvalue reference to an instance of type R that was initialized with the expression r passed to the invocation of execution::connect that returned o. Then:
-
-
-
The object to which REC(o) refers remains valid for the lifetime of the object to which o refers.
-
-
The type run-loop-opstate<R> overrides run-loop-opstate-base::execute() such that o.execute() is equivalent to the following:
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:
Alias template single-sender-value-type is defined as follows:
-
-
-
If value_types_of_t<S,E,Tuple,Variant> would have the form Variant<Tuple<T>>, then single-sender-value-type<S,E> is an alias for type T.
-
-
Otherwise, if value_types_of_t<S,E,Tuple,Variant> would have the form Variant<Tuple<>> or Variant<>, then single-sender-value-type<S,E> is an alias for type void.
-
-
Otherwise, single-sender-value-type<S,E> is ill-formed.
-
-
-
The type sender-awaitable<S,P> is equivalent to the following:
Let r be an rvalue expression of type awaitable-receiver, let cr be a const lvalue that refers to r, let vs... be an arbitrary function parameter pack of types Vs..., and let err be an arbitrary expression of type Err. Then:
-
-
-
If constructible_from<result_t,Vs...> is satisfied, the expression execution::set_value(r,vs...) is not potentially throwing and is equivalent to:
err if decay_t<Err> names the same type as exception_ptr,
-
-
Otherwise, make_exception_ptr(system_error(err)) if decay_t<Err> names the same type as error_code,
-
-
Otherwise, make_exception_ptr(err).
-
-
-
The expression execution::set_stopped(r) is not potentially throwing and is equivalent to static_cast<coroutine_handle<>>(r.continuation_.promise().unhandled_stopped()).resume().
-
-
tag_invoke(tag,cr,as...) is expression-equivalent to tag(as_const(cr.continuation_.promise()),as...) for any expression tag whose type satisfies forwarding-receiver-query and for any set of arguments as....
-
-
-
sender-awaitable::sender-awaitable(S&&s,P&p)
-
-
-
Effects: initializes state_ with connect(std::forward<S>(s),awaitable-receiver{&result_,coroutine_handle<P>::from_promise(p)}).
as_awaitable is a customization point object. For some subexpressions e and p where p is an lvalue, E names the type decltype((e)) and P names the type decltype((p)), as_awaitable(e,p) is expression-equivalent to the following:
-
-
-
tag_invoke(as_awaitable,e,p) if that expression is well-formed.
-
-
-
Mandates:is-awaitable<A> is true, where A is the type of the tag_invoke expression above.
-
-
-
Otherwise, e if is-awaitable<E> is true.
-
-
Otherwise, sender-awaitable{e,p} if awaitable-sender<E,P> is true.
with_awaitable_senders, when used as the base class of a coroutine promise type, makes senders awaitable in that coroutine type.
-
In addition, it provides a default implementation of unhandled_stopped() such that if a sender completes by calling execution::set_stopped, it is treated as if an uncatchable "stopped" exception were thrown from the await-expression. In practice, the coroutine is never resumed, and the unhandled_stopped of the coroutine caller’s promise type is called.
ISO/IEC JTC1/SC22/WG21 14882: Programming Language — C++
-
Audience:
-
SG1, LEWG
-
-
-
-
-
-
-
-
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 A Unified Executors Proposal for C++ 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 needs 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 of concurrency primitives (std::atomic, std::mutex, std::counting_semaphore, etc) and lower level building blocks (std::thread, etc), we lack a Standard vocabulary and framework for asynchrony and parallelism that C++ programmers desperately need. std::async/std::future/std::promise, 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.
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. A sender describes asynchronous work and sends a signal (value, error, or stopped) 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 std::invocable, and calls the std::invocable on the signal sent by the input sender. The sender returned by then sends the result of that invocation. In this case, the input sender came from schedule, so its void, meaning it won’t send us a value, so our std::invocable takes no parameters. But we return an int, which will be sent to the next recipient.
-
-
Now, we add another operation to the chain, again using § 4.21.2 execution::then. This time, we get sent a value - the int from the previous step. We add 42 to it, and then return the result.
-
-
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 std::optional<std::tuple<...>> with the value sent by the last sender, or an empty std::optional if the last sender sent a stopped signal, or it throws an exception if the last sender sent an error.
This example builds an asynchronous computation of an inclusive scan:
-
-
-
It scans a sequence of doubles (represented as the std::span<constdouble>input) and stores the result in another sequence of doubles (represented as std::span<double>output).
-
-
It takes a scheduler, which specifies what execution context the scan should be launched on.
-
-
It also takes a tile_count parameter that controls the number of execution agents that will be spawned.
-
-
First we need to allocate temporary storage needed for the algorithm, which we’ll do with a std::vector, partials. We need one double of temporary storage for each execution agent we create.
-
-
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 sch, which means the next item in the chain will use sch.
-
-
Senders and sender adaptors support composition via operator|, similar to C++ ranges. We’ll use operator| to attach the next piece of work, which will spawn tile_count execution agents using § 4.21.9 execution::bulk (see § 4.13 Most sender adaptors are pipeable for details).
-
-
Each agent will call a std::invocable, passing it two arguments. The first is the agent’s index (i) in the § 4.21.9 execution::bulk operation, in this case a unique integer in [0,tile_count). The second argument is what the input sender sent - the temporary storage.
-
-
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 std::inclusive_scan over our elements. We store the scan result for our last element, which is the sum of all of our elements, in our temporary storage 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 partials. 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.
-
-
§ 4.21.2 execution::then takes an input sender and an std::invocable and calls the std::invocable with the value sent by the input sender. Inside our std::invocable, we call std::inclusive_scan on partials, which the input senders will send to us.
-
-
Then we return partials, which the next phase will need.
-
-
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 partials to integrate the sums from other tiles into our elements, completing the inclusive scan.
-
-
async_inclusive_scan returns a sender that sends the output std::span<double>. A consumer of the algorithm can chain additional work that uses the scan result. At the point at which async_inclusive_scan returns, the computation may not have completed. In fact, it may not have even started.
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:
-
-
-
async_read 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 a std::span<std::byte>, and a handle to an I/O context. It will asynchronously read into the input buffer, up to the size of the std::span. It returns a sender which will send the number of bytes read once the read completes.
-
-
async_read_array 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 a dynamic_buffer object that owns the data that was sent.
-
-
dynamic_buffer is an aggregate struct that contains a std::unique_ptr<std::byte[]> and a size.
-
-
The first thing we do inside of async_read_array is create a sender that will send a new, empty dynamic_array object using § 4.20.2 execution::just. We can attach more work to the pipeline using operator| composition (see § 4.13 Most sender adaptors are pipeable for details).
-
-
We need the lifetime of this dynamic_array object to last for the entire pipeline. So, we use let_value, which takes an input sender and a std::invocable that must return a sender itself (see § 4.21.4 execution::let_* for details). let_value sends the value from the input sender to the std::invocable. Critically, the lifetime of the sent object will last until the sender returned by the std::invocable completes.
-
-
Inside of the let_valuestd::invocable, we have the rest of our logic. First, we want to initiate an async_read of the buffer size. To do that, we need to send a std::span pointing to buf.size. We can do that with § 4.20.2 execution::just.
Next, we pipe a std::invocable that will be invoked after the async_read completes using § 4.21.2 execution::then.
-
-
That std::invocable gets sent the number of bytes read.
-
-
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 std::span<std::byte> to the storage for the data from the std::invocable. This will be sent to the next recipient in the pipeline.
-
-
And that recipient will be another async_read, which will read the data.
-
-
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 dynamic_buffer object. It will get sent by the sender returned by async_read_array. We can attach more things to that sender to use the data in the buffer.
-
-
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 async_recv() operation for a Windows Socket.
-
structoperation_base:WSAOVERALAPPED{
- usingcompletion_fn=void(operation_base*op,DWORDbytesTransferred,interrorCode)noexcept;
-
- // Assume IOCP event loop will call this when this OVERLAPPED structure is dequeued.
- completion_fn*completed;
-};
-
-template<typenameReceiver>
-structrecv_op:operation_base{
- recv_op(SOCKETs,void*data,size_tlen,Receiverr)
- :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);
- }
-
- friendvoidtag_invoke(std::tag_t<std::execution::start>,recv_op&self)noexcept{
- // Avoid even calling WSARecv() if operation already cancelled
- autost=std::execution::get_stop_token(
- std::execution::get_env(self.receiver));
- if(st.stop_requested()){
- std::execution::set_stopped(std::move(self.receiver));
- return;
- }
-
- // Store and cache result here in case it changes during execution
- constboolstopPossible=st.stop_possible();
- if(!stopPossible){
- self.ready.store(true,std::memory_order_relaxed);
- }
-
- // Launch the operation
- DWORDbytesTransferred=0;
- DWORDflags=0;
- intresult=WSARecv(self.sock,&self.buffer,1,&bytesTransferred,&flags,
- static_cast<WSAOVERLAPPED*>(&self), NULL);
- if(result==SOCKET_ERROR){
- interrorCode=WSAGetLastError();
- if(errorCode!=WSA_IO_PENDING)){
- if(errorCode==WSA_OPERATION_ABORTED){
- std::execution::set_stopped(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();
-
- BOOLok=WSAGetOverlappedResult(self.sock,(WSAOVERLAPPED*)&self,&bytesTransferred,FALSE,&flags);
- if(ok){
- std::execution::set_value(std::move(self.receiver),bytesTransferred);
- }else{
- interrorCode=WSAGetLastError();
- std::execution::set_error(std::move(self.receiver),
- std::error_code(errorCode,std::system_category()));
- }
- }
- }
- }
-
- structcancel_cb{
- recv_op&op;
-
- voidoperator()()noexcept{
- CancelIoEx((HANDLE)op.sock,(OVERLAPPED*)(WSAOVERLAPPED*)&op);
- }
- };
-
- staticvoidon_complete(operation_base*op,DWORDbytesTransferred,interrorCode)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()));
- }
- }
- }
-
- Receiverreceiver;
- SOCKETsock;
- WSABUFbuffer;
- std::optional<typenamestop_callback_type_t<Receiver>
- ::templatecallback_type<cancel_cb>>stopCallback;
- std::atomic<bool>ready{false};
-};
-
-structrecv_sender{
- SOCKETsock;
- void*data;
- size_tlen;
-
- template<typenameReceiver>
- friendrecv_op<Receiver>tag_invoke(std::tag_t<std::execution::connect>
- constrecv_sender&s,
- Receiverr){
- returnrecv_op<Receiver>{s.sock,s.data,s.len,std::move(r)};
- }
-};
-
-recv_senderasync_recv(SOCKETs,void*data,size_tlen){
- returnrecv_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.
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.
In this code, NN::async_read_some and NN::async_write_some are asynchronous socket-based networking APIs that return senders. EX::repeat_effect_until, EX::let_value, and EX::then 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 repeat_effect_until 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.
This code builds a then 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 (set_error and set_stopped), as well as all receiver queries,
-are passed through unchanged.
-
In detail, it does the following:
-
-
-
Defines a receiver in terms of execution::receiver_adaptor that aggregates
-another receiver and an invocable that:
-
-
-
Defines a constrained tag_invoke overload for transforming the value
-channel.
-
-
Defines another constrained overload of tag_invoke that passes all other
-customizations through unchanged.
-
-
The tag_invoke overloads are actually implemented by execution::receiver_adaptor; they dispatch either to named members, as
-shown above with _then_receiver::set_value, or to the adapted receiver.
-
-
Defines a sender that aggregates another sender and the invocable, which defines a tag_invoke customization for std::execution::connect that wraps the incoming receiver in the receiver from (1) and passes it and the incoming sender to std::execution::connect, returning the result. It also defines a tag_invoke customization of get_completion_signatures that declares the sender’s completion signatures when executed within a particular environment.
-
-
1.5.2. retry
-
usingnamespacestd;
-namespaceexec=execution;
-
-template<classFrom,classTo>
-concept_decays_to=same_as<decay_t<From>,To>;
-
-// _conv needed so we can emplace construct non-movable types into
-// a std::optional.
-template<invocableF>
- requiresis_nothrow_move_constructible_v<F>
-struct_conv{
- Ff_;
- explicit_conv(Ff)noexcept:f_((F&&)f){}
- operatorinvoke_result_t<F>()&&{
- return((F&&)f_)();
- }
-};
-
-template<classS,classR>
-struct_op;
-
-// pass through all customizations except set_error, which retries the operation.
-template<classS,classR>
-struct_retry_receiver
- :exec::receiver_adaptor<_retry_receiver<S,R>>{
- _op<S,R>*o_;
-
- R&&base()&&noexcept{return(R&&)o_->r_;}
- constR&base()const&noexcept{returno_->r_;}
-
- explicit_retry_receiver(_op<S,R>*o):o_(o){}
-
- voidset_error(auto&&)&&noexcept{
- o_->_retry();// This causes the op to be retried
- }
-};
-
-// Hold the nested operation state in an optional so we can
-// re-construct and re-start it if the operation fails.
-template<classS,classR>
-struct_op{
- Ss_;
- Rr_;
- optional<
- exec::connect_result_t<S&,_retry_receiver<S,R>>>o_;
-
- _op(Ss,Rr):s_((S&&)s),r_((R&&)r),o_{_connect()}{}
- _op(_op&&)=delete;
-
- auto_connect()noexcept{
- return_conv{[this]{
- returnexec::connect(s_,_retry_receiver<S,R>{this});
- }};
- }
- void_retry()noexcepttry{
- o_.emplace(_connect());// potentially throwing
- exec::start(*o_);
- }catch(...){
- exec::set_error((R&&)r_,std::current_exception());
- }
- friendvoidtag_invoke(exec::start_t,_op&o)noexcept{
- exec::start(*o.o_);
- }
-};
-
-template<classS>
-struct_retry_sender{
- Ss_;
- explicit_retry_sender(Ss):s_((S&&)s){}
-
- template<class...Ts>
- using_value_t=
- exec::completion_signatures<exec::set_value_t(Ts...)>;
- template<class>
- using_error_t=exec::completion_signatures<>;
-
- // Declare the signatures with which this sender can complete
- template<classEnv>
- friendautotag_invoke(exec::get_completion_signatures_t,const_retry_sender&,Env)
- ->exec::make_completion_signatures<S&,Env,
- exec::completion_signatures<exec::set_error_t(std::exception_ptr)>,
- _value_t,_error_t>;
-
- template<exec::receiverR>
- friend_op<S,R>tag_invoke(exec::connect_t,_retry_sender&&self,Rr){
- return{(S&&)self.s_,(R&&)r};
- }
-
- frienddecltype(auto)tag_invoke(exec::get_attrs_t,const_retry_sender&self)noexcept{
- returnget_attrs(self.s_);
- }
-};
-
-template<exec::senderS>
-exec::senderautoretry(Ss){
- return_retry_sender{(S&&)s};
-}
-
-
The retry algorithm takes a multi-shot sender and causes it to repeat on error, passing
-through values and stopped signals. Each time the input sender is restarted, a new receiver
-is connected and the resulting operation state is stored in an optional, which allows us
-to reinitialize it multiple times.
-
This example does the following:
-
-
-
Defines a _conv utility that takes advantage of C++17’s guaranteed copy elision to
-emplace a non-movable type in a std::optional.
-
-
Defines a _retry_receiver 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 set_error, which causes a _retry() function to be called instead.
-
-
Defines an operation state that aggregates the input sender and receiver, and declares
-storage for the nested operation state in an optional. Constructing the operation
-state constructs a _retry_receiver with a pointer to the (under construction) operation
-state and uses it to connect to the aggregated sender.
-
-
Starting the operation state dispatches to start on the inner operation state.
-
-
The _retry() 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.
-
-
After reinitializing the inner operation state, _retry() calls start on it, causing
-the failed operation to be rescheduled.
-
-
Defines a _retry_sender that implements the connect customization point to return
-an operation state constructed from the passed-in sender and receiver.
-
-
_retry_sender also implements the get_completion_signatures customization point to describe the ways this sender may complete when executed in a particular execution context.
-
-
1.6. Examples: Schedulers
-
In this section we look at some schedulers of varying complexity.
The inline scheduler is a trivial scheduler that completes immediately and synchronously on
-the thread that calls std::execution::start on the operation state produced by its sender.
-In other words, start(connect(schedule(inline-scheduler),receiver)) is
-just a fancy way of saying set_value(receiver), with the exception of the fact that start wants to be passed an lvalue.
-
Although not a particularly useful scheduler, it serves to illustrate the basics of
-implementing one. The inline_scheduler:
-
-
-
Customizes execution::schedule to return an instance of the sender type _sender.
-
-
The _sender type models the sender concept and provides the metadata
-needed to describe it as a sender of no values that can send an exception_ptr as an error and that never calls set_stopped. This
-metadata is provided with the help of the execution::completion_signatures utility.
-
-
The _sender type customizes execution::connect to accept a receiver of no
-values. It returns an instance of type _op that holds the receiver by
-value.
-
-
The operation state customizes std::execution::start to call std::execution::set_value on the receiver, passing any exceptions to std::execution::set_error as an 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 std::execution::run_loop.
The single_thread_context 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 execution::run_loop 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 this_thread::sync_wait, 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. Examples: Server theme
-
In this section we look at some examples of how one would use senders to implement an HTTP server. The examples ignore the low-level details of the HTTP server and looks at how senders can be combined to achieve the goals of the project.
-
General application context:
-
-
-
server application that processes images
-
-
execution contexts:
-
-
-
1 dedicated thread for network I/O
-
-
N worker threads used for CPU-intensive work
-
-
M threads for auxiliary I/O
-
-
optional GPU context that may be used on some types of servers
-
-
-
all parts of the applications can be asynchronous
-
-
no locks shall be used in user code
-
-
1.7.1. Composability with execution::let_*
-
Example context:
-
-
-
we are looking at the flow of processing an HTTP request and sending back the response
-
-
show how one can break the (slightly complex) flow into steps with execution::let_* functions
-
-
different phases of processing HTTP requests are broken down into separate concerns
-
-
each part of the processing might use different execution contexts (details not shown in this example)
-
-
error handling is generic, regardless which component fails; we always send the right response to the clients
-
-
Goals:
-
-
-
show how one can break more complex flows into steps with let_* functions
-
-
exemplify the use of let_value, let_error, let_stopped, and just algorithms
-
-
namespaceex=std::execution;
-
-// Returns a sender that yields an http_request object for an incoming request
-ex::senderautoschedule_request_start(read_requests_ctxctx){...}
-// Sends a response back to the client; yields a void signal on success
-ex::senderautosend_response(consthttp_response&resp){...}
-// Validate that the HTTP request is well-formed; forwards the request on success
-ex::senderautovalidate_request(consthttp_request&req){...}
-
-// Handle the request; main application logic
-ex::senderautohandle_request(consthttp_request&req){
- //...
- returnex::just(http_response{200,result_body});
-}
-
-// Transforms server errors into responses to be sent to the client
-ex::senderautoerror_to_response(std::exception_ptrerr){
- try{
- std::rethrow_exception(err);
- }catch(conststd::invalid_argument&e){
- returnex::just(http_response{404,e.what()});
- }catch(conststd::exception&e){
- returnex::just(http_response{500,e.what()});
- }catch(...){
- returnex::just(http_response{500,"Unknown server error"});
- }
-}
-// Transforms cancellation of the server into responses to be sent to the client
-ex::senderautostopped_to_response(){
- returnex::just(http_response{503,"Service temporarily unavailable"});
-}
-//...
-// The whole flow for transforming incoming requests into responses
-ex::senderautosnd=
- // get a sender when a new request comes
- schedule_request_start(the_read_requests_ctx)
- // make sure the request is valid; throw if not
- |ex::let_value(validate_request)
- // process the request in a function that may be using a different execution context
- |ex::let_value(handle_request)
- // If there are errors transform them into proper responses
- |ex::let_error(error_to_response)
- // If the flow is cancelled, send back a proper response
- |ex::let_stopped(stopped_to_response)
- // write the result back to the client
- |ex::let_value(send_response)
- // done
- ;
-// execute the whole flow asynchronously
-ex::start_detached(std::move(snd));
-
- The example shows how one can separate out the concerns for interpreting requests, validating requests, running the main logic for handling the request, generating error responses, handling cancellation and sending the response back to the client.
-They are all different phases in the application, and can be joined together with the let_* functions.
-
All our functions return execution::sender objects, so that they can all generate success, failure and cancellation paths.
-For example, regardless where an error is generated (reading request, validating request or handling the response), we would have one common block to handle the error, and following error flows is easy.
-
Also, because of using execution::sender objects at any step, we might expect any of these steps to be completely asynchronous; the overall flow doesn’t care.
-Regardless of the execution context in which the steps, or part of the steps are executed in, the flow is still the same.
-
1.7.2. Moving between execution contexts with execution::on and execution::transfer
-
Example context:
-
-
-
reading data from the socket before processing the request
-
-
reading of the data is done on the I/O context
-
-
no processing of the data needs to be done on the I/O context
-
-
Goals:
-
-
-
show how one can change the execution context
-
-
exemplify the use of on and transfer algorithms
-
-
namespaceex=std::execution;
-
-size_tlegacy_read_from_socket(intsock,char*buffer,size_tbuffer_len){}
-voidprocess_read_data(constchar*read_data,size_tread_len){}
-//...
-
-// A sender that just calls the legacy read function
-autosnd_read=ex::just(sock,buf,buf_len)|ex::then(legacy_read_from_socket);
-// The entire flow
-autosnd=
- // start by reading data on the I/O thread
- ex::on(io_sched,std::move(snd_read))
- // do the processing on the worker threads pool
- |ex::transfer(work_sched)
- // process the incoming data (on worker threads)
- |ex::then([buf](intread_len){process_read_data(buf,read_len);})
- // done
- ;
-// execute the whole flow asynchronously
-ex::start_detached(std::move(snd));
-
-
The example assume that we need to wrap some legacy code of reading sockets, and handle execution context switching.
-(This style of reading from socket may not be the most efficient one, but it’s working for our purposes.)
-For performance reasons, the reading from the socket needs to be done on the I/O thread, and all the processing needs to happen on a work-specific execution context (i.e., thread pool).
-
Calling execution::on will ensure that the given sender will be started on the given scheduler.
-In our example, snd_read is going to be started on the I/O scheduler.
-This sender will just call the legacy code.
-
The completion signal will be issued in the I/O execution context, so we have to move it to the work thread pool.
-This is achieved with the help of the execution::transfer algorithm.
-The rest of the processing (in our case, the last call to then) will happen in the work thread pool.
-
The reader should notice the difference between execution::on and execution::transfer.
-The execution::on algorithm will ensure that the given sender will start in the specified context, and doesn’t care where the completion signal for that sender is sent.
-The execution::transfer algorithm will not care where the given sender is going to be started, but will ensure that the completion signal of will be transferred to the given context.
-
1.8. What this proposal is not
-
This paper is not a patch on top of A Unified Executors Proposal for C++; 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 A Unified Executors Proposal for C++; 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.9. Design changes from P0443
-
-
-
The executor concept has been removed and all of its proposed functionality
-is now based on schedulers and senders, as per SG1 direction.
-
-
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.
This paper extends the sender traits/typed sender design to support typed
-senders whose value/error types depend on type information provided late via
-the receiver.
-
-
Support for untyped senders is dropped; the typed_sender concept is renamed sender; sender_traits is replaced with completion_signatures_of_t.
-
-
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.
-
-
Some additional utilities are added:
-
-
-
run_loop: An execution context that provides a multi-producer,
-single-consumer, first-in-first-out work queue.
-
-
receiver_adaptor: A utility for algorithm authors for defining one
-receiver type in terms of another.
-
-
completion_signatures and make_completion_signatures:
-Utilities for describing the ways in which a sender can complete in a
-declarative syntax.
-
-
-
1.10. 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.10.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.10.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 Halo: coroutine Heap Allocation eLision Optimization: the joint response, 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.10.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.11. Field experience
-
1.11.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 (with slight variations -- the design of P2300 has evolved due to LEWG feedback), many of this paper’s algorithms (some under different names), and much more besides.
-
Libunifex has several concrete schedulers in addition to the run_loop suggested here (where it is called manual_event_loop). 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.11.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 Simberg 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 let_value are really nice additions.
-
-
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 f2 in the continuation will always be ready here f1.then([](future<T>f2){...}) is enough of a reason to separate the channels. All the other obvious reasons apply as well of course.
-
-
For futures we have a thing called hpx::dataflow which is an optimized version of when_all(...).then(...) which avoids intermediate allocations. With the sender/receiver when_all(...)|then(...) we get that "for free".
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 complete 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. It is
-fit for broad use and for contribution to libc++.
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.11.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:
Fix typo in the specification of in_place_stop_source about the relative
-lifetimes of the tokens and the source that produced them.
-
-
get_completion_signatures tests for awaitability with a promise type
-similar to the one used by connect for the sake of consistency.
-
-
A coroutine promise type is an environment provider (that is, it implements get_env()) rather than being directly queryable. The previous draft was
-inconsistent about that.
-
-
Enhancements:
-
-
-
Sender queries are moved into a separate queryable "attributes" object
-that is accessed by passing the sender to get_attrs() (see below). The sender concept is reexpressed to require get_attrs() and separated
-from a new sender_in<Snd,Env> concept for checking whether a type is
-a sender within a particular execution environment.
-
-
The placeholder types no_env and dependent_completion_signatures<> are no longer needed and are dropped.
-
-
ensure_started and split are changed to persist the result of
-calling get_attrs() on the input sender.
-
-
Reorder constraints of the scheduler and receiver concepts to avoid constraint recursion
-when used in tandem with poorly-constrained, implicitly convertible types.
-
-
Re-express the sender_of concept to be more ergonomic and general.
-
-
Make the specification of the alias templates value_types_of_t and error_types_of_t, and the variable template sends_done more concise by
-expressing them in terms of a new exposition-only alias template gather-signatures.
-
-
2.1.1. Environments and attributes
-
In earlier revisions, receivers, senders, and schedulers all were directly
-queryable. In R4, receiver queries were moved into a separate "environment"
-object, obtainable from a receiver with a get_env accessor. In R6, the
-sender queries are given similar treatment, relocating to a "attributes"
-object obtainable from a sender with a get_attrs accessor. This was done
-to solve a number of design problems with the split and ensure_started algorithms; _e.g._, see NVIDIA/stdexec#466.
-
Schedulers, however, remain directly queryable. As lightweight handles
-that are required to be movable and copyable, there is little reason to
-want to dispose of a scheduler and yet persist the scheduler’s queries.
-
This revision also makes operation states directly queryable, even though
-there isn’t yet a use for such. Some early prototypes of cooperative bulk
-parallel sender algorithms done at NVIDIA suggest the utility of
-forwardable operation state queries. The authors chose to make opstates
-directly queryable since the opstate object is itself required to be kept
-alive for the duration of asynchronous operation.
-
2.2. R5
-
The changes since R4 are as follows:
-
Fixes:
-
-
-
start_detached requires its argument to be a void sender (sends no values
-to set_value).
-
-
Enhancements:
-
-
-
Receiver concepts refactored to no longer require an error channel for exception_ptr or a stopped channel.
-
-
sender_of concept and connect customization point additionally require
-that the receiver is capable of receiving all of the sender’s possible
-completions.
-
-
get_completion_signatures is now required to return an instance of either completion_signatures or dependent_completion_signatures.
-
-
make_completion_signatures made more general.
-
-
receiver_adaptor handles get_env as it does the set_* members; that is, receiver_adaptor will look for a member named get_env() in the derived
-class, and if found dispatch the get_env_t tag invoke customization to it.
-
-
just, just_error, just_stopped, and into_variant have been respecified
-as customization point objects instead of functions, following LEWG guidance.
-
-
2.3. R4
-
The changes since R3 are as follows:
-
Fixes:
-
-
-
Fix specification of get_completion_scheduler on the transfer, schedule_from and transfer_when_all algorithms; the completion scheduler cannot be guaranteed
-for set_error.
-
-
The value of sends_stopped for the default sender traits of types that are
-generally awaitable was changed from false to true to acknowledge the
-fact that some coroutine types are generally awaitable and may implement the unhandled_stopped() protocol in their promise types.
-
-
Fix the incorrect use of inline namespaces in the <execution> header.
-
-
Shorten the stable names for the sections.
-
-
sync_wait now handles std::error_code specially by throwing a std::system_error on failure.
-
-
Fix how ADL isolation from class template arguments is specified so it
-doesn’t constrain implmentations.
-
-
Properly expose the tag types in the header <execution> synopsis.
-
-
Enhancements:
-
-
-
Support for "dependently-typed" senders, where the completion signatures -- and
-thus the sender metadata -- depend on the type of the receiver connected
-to it. See the section dependently-typed
-senders below for more information.
-
-
Add a read(query) sender factory for issuing a query
-against a receiver and sending the result through the value channel. (This is
-a useful instance of a dependently-typed sender.)
-
-
Add completion_signatures utility for declaratively defining a typed
-sender’s metadata and a make_completion_signatures utility for adapting
-another sender’s completions in helpful ways.
-
-
Add make_completion_signatures utility for specifying a sender’s completion
-signatures by adapting those of another sender.
-
-
Drop support for untyped senders and rename typed_sender to sender.
-
-
set_done is renamed to set_stopped. All occurances of "done" in
-indentifiers replaced with "stopped"
-
-
Add customization points for controlling the forwarding of scheduler,
-sender, receiver, and environment queries through layers of adaptors;
-specify the behavior of the standard adaptors in terms of the new
-customization points.
-
-
Add get_delegatee_scheduler query to forward a scheduler that can be used
-by algorithms or by the scheduler to delegate work and forward progress.
-
-
Add schedule_result_t alias template.
-
-
More precisely specify the sender algorithms, including precisely what their
-completion signatures are.
-
-
stopped_as_error respecified as a customization point object.
-
-
tag_invoke respecified to improve diagnostics.
-
-
2.3.1. Dependently-typed senders
-
Background:
-
In the sender/receiver model, as with coroutines, contextual information about
-the current execution is most naturally propagated from the consumer to the
-producer. In coroutines, that means information like stop tokens, allocators and
-schedulers are propagated from the calling coroutine to the callee. In
-sender/receiver, that means that that contextual information is associated with
-the receiver and is queried by the sender and/or operation state after the
-sender and the receiver are connect-ed.
-
Problem:
-
The implication of the above is that the sender alone does not have all the
-information about the async computation it will ultimately initiate; some of
-that information is provided late via the receiver. However, the sender_traits mechanism, by which an algorithm can introspect the value and error types the
-sender will propagate, only accepts a sender parameter. It does not take into
-consideration the type information that will come in late via the receiver. The
-effect of this is that some senders cannot be typed senders when they
-otherwise could be.
-
Example:
-
To get concrete, consider the case of the "get_scheduler()" sender: when connect-ed and start-ed, it queries the receiver for its associated
-scheduler and passes it back to the receiver through the value channel. That
-sender’s "value type" is the type of the receiver’s scheduler. What then
-should sender_traits<get_scheduler_sender>::value_types report for the get_scheduler()'s value type? It can’t answer because it doesn’t know.
-
This causes knock-on problems since some important algorithms require a typed
-sender, such as sync_wait. To illustrate the problem, consider the following
-code:
-
namespaceex=std::execution;
-
-ex::senderautotask=
- ex::let_value(
- ex::get_scheduler(),// Fetches scheduler from receiver.
- [](autocurrent_sched){
- // Lauch some nested work on the current scheduler:
- returnex::on(current_sched,nestedwork...);
- });
-
-std::this_thread::sync_wait(std::move(task));
-
-
The code above is attempting to schedule some work onto the sync_wait's run_loop execution context. But let_value only returns a typed sender when
-the input sender is typed. As we explained above, get_scheduler() is not
-typed, so task is likewise not typed. Since task isn’t typed, it cannot be
-passed to sync_wait which is expecting a typed sender. The above code would
-fail to compile.
-
Solution:
-
The solution is conceptually quite simple: extend the sender_traits mechanism
-to optionally accept a receiver in addition to the sender. The algorithms can
-use sender_traits<Sender,Receiver> to inspect the
-async operation’s completion signals. The typed_sender concept would also need
-to take an optional receiver parameter. This is the simplest change, and it
-would solve the immediate problem.
-
Design:
-
Using the receiver type to compute the sender traits turns out to have pitfalls
-in practice. Many receivers make use of that type information in their
-implementation. It is very easy to create cycles in the type system, leading to
-inscrutible errors. The design pursued in R4 is to give receivers an associated environment object -- a bag of key/value pairs -- and to move the contextual
-information (schedulers, etc) out of the receiver and into the environment. The sender_traits template and the typed_sender concept, rather than taking a
-receiver, take an environment. This is a much more robust design.
-
A further refinement of this design would be to separate the receiver and the
-environment entirely, passing then as separate arguments along with the sender to connect. This paper does not propose that change.
-
Impact:
-
This change, apart from increasing the expressive power of the sender/receiver abstraction, has the following impact:
-
-
-
Typed senders become moderately more challenging to write. (The new completion_signatures and make_completion_signatures utilities are added
-to ease this extra burden.)
-
-
Sender adaptor algorithms that previously constrained their sender arguments
-to satisfy the typed_sender concept can no longer do so as the receiver is
-not available yet. This can result in type-checking that is done later, when connect is ultimately called on the resulting sender adaptor.
-
-
Operation states that own receivers that add to or change the environment
-are typically larger by one pointer. It comes with the benefit of far fewer
-indirections to evaluate queries.
-
-
"Has it been implemented?"
-
Yes, the reference implementation, which can be found at
-https://github.com/NVIDIA/stdexec, has implemented this
-design as well as some dependently-typed senders to confirm that it works.
-
Implementation experience
-
Although this change has not yet been made in libunifex, the most widely adopted sender/receiver implementation, a similar design can be found in Folly’s coroutine support library. In Folly.Coro, it is possible to await a special awaitable to obtain the current coroutine’s associated scheduler (called an executor in Folly).
-
For instance, the following Folly code grabs the current executor, schedules a task for execution on that executor, and starts the resulting (scheduled) task by enqueueing it for execution.
-
// From Facebook’s Folly open source library:
-template<classT>
-folly::coro::Task<void>CancellableAsyncScope::co_schedule(folly::coro::Task<T>&&task){
- this->add(std::move(task).scheduleOn(co_awaitco_current_executor));
- co_return;
-}
-
-
Facebook relies heavily on this pattern in its coroutine code. But as described
-above, this pattern doesn’t work with R3 of std::execution because of the lack
-of dependently-typed schedulers. The change to sender_traits in R4 rectifies that.
-
Why now?
-
The authors are loathe to make any changes to the design, however small, at this
-stage of the C++23 release cycle. But we feel that, for a relatively minor
-design change -- adding an extra template parameter to sender_traits and typed_sender -- the returns are large enough to justify the change. And there
-is no better time to make this change than as early as possible.
-
One might wonder why this missing feature not been added to sender/receiver
-before now. The designers of sender/receiver have long been aware of the need.
-What was missing was a clean, robust, and simple design for the change, which we
-now have.
-
Drive-by:
-
We took the opportunity to make an additional drive-by change: Rather than
-providing the sender traits via a class template for users to specialize, we
-changed it into a sender query: get_completion_signatures(sender,env). That function’s return type is used as the sender’s traits.
-The authors feel this leads to a more uniform design and gives sender authors a
-straightforward way to make the value/error types dependent on the cv- and
-ref-qualification of the sender if need be.
-
Details:
-
Below are the salient parts of the new support for dependently-typed senders in
-R4:
-
-
-
Receiver queries have been moved from the receiver into a separate environment
-object.
-
-
Receivers have an associated environment. The new get_env CPO retrieves a
-receiver’s environment. If a receiver doesn’t implement get_env, it returns
-an unspecified "empty" environment -- an empty struct.
-
-
sender_traits now takes an optional Env parameter that is used to
-determine the error/value types.
-
-
The primary sender_traits template is replaced with a completion_signatures_of_t alias implemented in terms of a new get_completion_signatures CPO that dispatches
-with tag_invoke. get_completion_signatures takes a sender and an optional
-environment. A sender can customize this to specify its value/error types.
-
-
Support for untyped senders is dropped. The typed_sender concept has been
-renamed to sender and now takes an optional environment.
-
-
The environment argument to the sender concept and the get_completion_signatures CPO defaults to no_env. All environment queries fail (are ill-formed) when
-passed an instance of no_env.
-
-
A type S is required to satisfy sender<S> to be
-considered a sender. If it doesn’t know what types it will complete with
-independent of an environment, it returns an instance of the placeholder
-traits dependent_completion_signatures.
-
-
If a sender satisfies both sender<S> and sender<S,Env>, then the completion signatures
-for the two cannot be different in any way. It is possible for an
-implementation to enforce this statically, but not required.
-
-
All of the algorithms and examples have been updated to work with
-dependently-typed senders.
-
-
2.4. R3
-
The changes since R2 are as follows:
-
Fixes:
-
-
-
Fix specification of the on algorithm to clarify lifetimes of
-intermediate operation states and properly scope the get_scheduler query.
-
-
Fix a memory safety bug in the implementation of connect-awaitable.
-
-
Fix recursive definition of the scheduler concept.
-
-
Enhancements:
-
-
-
Add run_loop execution context.
-
-
Add receiver_adaptor utility to simplify writing receivers.
-
-
Require a scheduler’s sender to model sender_of and provide a completion scheduler.
-
-
Specify the cancellation scope of the when_all algorithm.
-
-
Make as_awaitable a customization point.
-
-
Change connect's handling of awaitables to consider those types that are awaitable owing to customization of as_awaitable.
-
-
Add value_types_of_t and error_types_of_t alias templates; rename stop_token_type_t to stop_token_of_t.
-
-
Add a design rationale for the removal of the possibly eager algorithms.
-
-
Expand the section on field experience.
-
-
2.5. R2
-
The changes since R1 are as follows:
-
-
-
Remove the eagerly executing sender algorithms.
-
-
Extend the execution::connect customization point and the sender_traits<> template to recognize awaitables as typed_senders.
-
-
Add utilities as_awaitable() and with_awaitable_senders<> so a coroutine type can trivially make senders awaitable with a coroutine.
-
-
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 bulk algorithm.
-
-
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 execution::just_error and execution::just_stopped.
-
-
Add sender adaptors execution::stopped_as_optional and execution::stopped_as_error.
-
-
Document more production uses of sender/receiver at scale.
-
-
Various fixes of typos and bugs.
-
-
2.6. 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 unschedule adaptor.
-
-
Various fixes of typos and bugs.
-
-
2.7. R0
-
Initial revision.
-
3. Design - introduction
-
The following three 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 A Unified Executors Proposal for C++: std::execution; however, for brevity, the std:: part of this name is omitted. When you see execution::foo, treat that as std::execution::foo.
-
-
Universal references and explicit calls to std::move/std::forward are omitted in code samples and signatures for simplicity; assume universal references and perfect forwarding unless stated otherwise.
-
-
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 std::invocable 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 std::invocable 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, schedule, 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::schedulerautosch=thread_pool.scheduler();
-execution::senderautosnd=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 execution::schedule API.
-
Future papers will propose additional scheduler concepts that extend scheduler to add other capabilities. For example:
-
-
-
A time_scheduler concept that extends scheduler to support time-based scheduling.
-Such a concept might provide access to schedule_after(sched,duration), schedule_at(sched,time_point) and now(sched) APIs.
-
-
Concepts that extend scheduler to support opening, reading and writing files asynchronously.
-
-
Concepts that extend scheduler to support connecting, sending data and receiving data over the network asynchronously.
-
-
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.
execution::schedulerautosch=thread_pool.scheduler();
-execution::senderautosnd=execution::schedule(sch);
-execution::senderautocont=execution::then(snd,[]{
- std::fstreamfile{"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 execution::connect) senders and return a sender.
-
-
sender consumers, algorithms that take (and potentially execution::connect) senders and do not return a sender.
-
-
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.
-
A Unified Executors Proposal for C++ 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 (Towards C++23 executors: A proposal for an initial set of algorithms) 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 std::thread (which they aren’t).
-
-
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 on sender algorithm from Towards C++23 executors: A proposal for an initial set of algorithms, 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 stopped) 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
-
get_completion_scheduler is a query that retrieves the completion scheduler for a specific completion signal from a sender’s attributes.
-For a sender that lacks a completion scheduler attribute for a given signal, calling get_completion_scheduler 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::schedulerautocpu_sched=new_thread_scheduler{};
-execution::schedulerautogpu_sched=cuda::scheduler();
-
-execution::senderautosnd0=execution::schedule(cpu_sched);
-execution::schedulerautocompletion_sch0=
- execution::get_completion_scheduler<execution::set_value_t>(get_attrs(snd0));
-// completion_sch0 is equivalent to cpu_sched
-
-execution::senderautosnd1=execution::then(snd0,[]{
- std::cout<<"I am running on cpu_sched!\n";
-});
-execution::schedulerautocompletion_sch1=
- execution::get_completion_scheduler<execution::set_value_t>(get_attrs(snd1));
-// completion_sch1 is equivalent to cpu_sched
-
-execution::senderautosnd2=execution::transfer(snd1,gpu_sched);
-execution::senderautosnd3=execution::then(snd2,[]{
- std::cout<<"I am running on gpu_sched!\n";
-});
-execution::schedulerautocompletion_sch3=
- execution::get_completion_scheduler<execution::set_value_t>(get_attrs(snd3));
-// completion_sch3 is equivalent to gpu_sched
-
-
4.6. Execution context transitions are explicit
-
A Unified Executors Proposal for C++ 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 execution::schedule, 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 execution::transfer sender adaptor performs a transition from one execution context to another:
-
execution::schedulerautosch1=...;
-execution::schedulerautosch2=...;
-
-execution::senderautosnd1=execution::schedule(sch1);
-execution::senderautothen1=execution::then(snd1,[]{
- std::cout<<"I am running on sch1!\n";
-});
-
-execution::senderautosnd2=execution::transfer(then1,sch2);
-execution::senderautothen2=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 std::unique_ptr that it will be transferring ownership of to the
-operation-state returned by a call to execution::connect so that the operation has access to
-this resource. In such a sender, calling execution::connect 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 execution::connect only has overloads for an rvalue-qualified sender. Callers must pass the sender
-as an rvalue to the call to execution::connect, 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 execution::connect to accept an lvalue reference to the
-sender. Callers can indicate that they want the sender to remain valid after the call to execution::connect by passing an lvalue reference to the sender to call these overloads. Multi-shot senders should also define
-overloads of execution::connect 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 execution::connect.
-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 execution::connect. 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 execution::connect on the input sender, such as in this_thread::sync_wait or execution::start_detached.
-
Some multi-use sender algorithms may require that an input sender be copy-constructible but will only call execution::connect 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 execution::connect 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 split sender adaptor facilitates connecting to a sender multiple times, regardless of whether it is single-shot or multi-shot:
-
autosome_algorithm(execution::senderauto&&input){
- execution::senderautomulti_shot=split(input);
- // "multi_shot" is guaranteed to be multi-shot,
- // regardless of whether "input" was multi-shot or not
-
- returnwhen_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.
-
when_all 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. when_all returns a sender that also does not have an associated scheduler.
-
transfer_when_all 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 when_all. You can think of it as a composition of transfer(when_all(inputs...),scheduler), 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 execution::connect() before calling execution::start(), 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 when_all(ops...) algorithm should cancel other operations as soon as one operation fails
-
-
a first_successful(ops...) algorithm should cancel the other operations as soon as one operation completes successfuly
-
-
a generic timeout(src,duration) algorithm needs to be able to cancel the src operation after the timeout duration has elapsed.
-
-
a stop_when(src,trigger) algorithm should cancel src if trigger completes first and cancel trigger if src completes first
-
-
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:
In this example, if we take the operation returned by query_server_b(query), this operation will
-receive a stop-request when any of the following happens:
-
-
-
first_successful algorithm will send a stop-request if query_server_a(query) completes successfully
-
-
when_all algorithm will send a stop-request if the load_file("some_file.jpg") operation completes with an error or stopped result.
-
-
timeout algorithm will send a stop-request if the operation does not complete within 5 seconds.
-
-
stop_when algorithm will send a stop-request if the user clicks on the "Cancel" button in the user-interface.
-
-
The parent operation consuming the composed_cancellation_example() sends a stop-request
-
-
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.
At a high-level, the facilities proposed by this paper for supporting cancellation include:
-
-
-
Add std::stoppable_token and std::stoppable_token_for concepts that generalise the interface of std::stop_token type to allow other types with different implementation strategies.
-
-
Add std::unstoppable_token concept for detecting whether a stoppable_token can never receive a stop-request.
-
-
Add std::in_place_stop_token, std::in_place_stop_source and std::in_place_stop_callback<CB> types that provide a more efficient implementation of a stop-token for use in structured concurrency situations.
-
-
Add std::never_stop_token for use in places where you never want to issue a stop-request
-
-
Add std::execution::get_stop_token() CPO for querying the stop-token to use for an operation from its receiver’s execution environment.
-
-
Add std::execution::stop_token_of_t<T> for querying the type of a stop-token returned from 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 execution::get_stop_token() CPO.
-This CPO takes a single parameter, which is the execution environment of the receiver passed to execution::connect, and returns a std::stoppable_token that the operation can use to check for stop-requests for that operation.
-
As the caller of execution::connect typically has control over the receiver
-type it passes, it is able to customise the execution::get_env() CPO for that
-receiver to return an execution environment that hooks the execution::get_stop_token() CPO to return a stop-token that the receiver 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’s execution environment does not customise the execution::get_stop_token() CPO then invoking the CPO on that receiver’s
-environment will invoke the default implementation which returns std::never_stop_token. This is a special stoppable_token type that is
-statically known to always return false from the stop_possible() 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 execution::get_stop_token(), 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 execution::start() 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 execution::start() 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 execution::start() implementation.
-
An implementation of execution::start() 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 execution::start 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 execution::start() call (which is still executing).
This paper currently includes the design for cancellation as proposed in Composable cancellation for sender-based async operations - "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 ensure_started 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 execution::connect?" or similarly, "What happens if a call
-to execution::connect is made, but the returned operation state is destroyed
-before execution::start 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 when_all(eager_op_1(),eager_op_2()). Imagine eager_op_1() starts an
-asynchronous operation successfully, but then eager_op_2() throws. For
-lazy senders, that failure happens in the context of the when_all 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 before when_all has been called.
-
It then becomes the responsibility, not of the algorithm, but of the end
-user to handle the exception and ensure that eager_op_1() is joined before
-allowing the exception to propagate. If they fail to do that, they incur
-undefined behavior.
-
-
Detach from the computation - let the operation continue in the background -
-like an implicit call to std::thread::detach(). 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::shared_ptr, to ensure resources are kept alive until the operations
-complete, and can make clean shutdown nigh impossible.
-
-
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 std::terminate if an eager sender is destructed
-without joining it. This is the approach that std::thread destructor
-takes.
-
-
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 std::jthread uses for its
-destructor. It reduces the risk of deadlock but does not eliminate it.
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 execution::start 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 sequence that puts async operations in sequence by starting an operation only
-after the preceding one has completed. In an expression like sequence(a(),then(src,[]{b();}),c()), one my reasonably assume that a(), b() and c() 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.
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 execution::get_stop_token is an std::unstoppable_token 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, get_forward_progress_guarantee, which returns one of the enumerators of a new enum type, forward_progress_guarantee. Each enumerator of forward_progress_guarantee 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 operator|.
-This mechanism is similar to the operator| 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.
-
a|b will pass the sender a as the first argument to the pipeable sender adaptor b. Pipeable sender adaptors support partial application of the parameters after the first. For example, all of the following are equivalent:
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:
Certain sender adaptors are not 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.
-
-
-
execution::when_all and execution::when_all_with_variant: 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::on: 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 to 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 R that is a range of senders, consider the following in a coroutine that returns an async generator type:
This transforms each element of the asynchronous sequence R with the function fn on demand, as the data arrives. The result is a new asynchronous sequence of the transformed values.
-
Now imagine that R is the simple expression views::iota(0)|views::transform(execution::just). 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 [fn(0), fn(1), fn(2), ...].
-
Far more interesting would be if R 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:
- returnread_socket_async(socket,span{buff})
- // For success (partial and full), specify the next action:
- |execution::let_value([](error_codeerr,size_tbytes_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(autosnd:read_socket_async(socket,span{buff})){
- try{
- if(optional<size_t>bytes_read=
- co_awaitexecution::stopped_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 sender 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 await_transform trickery from a promise type to make them awaitable.)
-
For an example, imagine a coroutine type called task<T> that knows nothing
-about senders. It doesn’t implement any of the sender customization points.
-Despite that fact, and despite the fact that the this_thread::sync_wait algorithm is constrained with the sender concept, the following would compile
-and do what the user wants:
-
task<int>doSomeAsyncWork();
-
-intmain(){
- // OK, awaitable types satisfy the requirements for senders:
- autoo=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 execution::with_awaitable_senders 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 retry algorithm:
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 with_awaitable_senders 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 into_variant sender algorithm -- before co_await-ing that sender.
-
4.18. Cancellation of a sender can unwind a stack of coroutines
-
When looking at the sender-based retry 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 execution::set_stopped?
-
When your task type’s promise inherits from with_awaitable_senders, what happens is this: the coroutine behaves as if an uncatchable exception had been thrown from the co_await 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 with_awaitable_senders, or more generally implement a member function called unhandled_stopped, the exception unwinds the chain of coroutines as if an exception were thrown except that it bypasses catch(...) clauses.
-
In order to "catch" this uncatchable stopped exception, one of the calling coroutines in the stack would have to await a sender that maps the stopped channel into either a value or an error. That is achievable with the execution::let_stopped, execution::upon_stopped, execution::stopped_as_optional, or execution::stopped_as_error sender adaptors. For instance, we can use execution::stopped_as_optional to "catch" the stopped signal and map it into an empty optional as shown below:
-
if(autoopt=co_awaitexecution::stopped_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 connect-ing an awaitable and a receiver, the adaptation layer awaits the awaitable within a coroutine that implements unhandled_stopped in its promise type. The effect of this is that an "uncatchable" stopped exception propagates seamlessly out of awaitables, causing execution::set_stopped to be called on the receiver.
-
Obviously, unhandled_stopped is a library extension of the coroutine promise interface. Many promise types will not implement unhandled_stopped. When an uncatchable stopped exception tries to propagate through such a coroutine, it is treated as an unhandled exception and terminate is called. The solution, as described above, is to use a sender adaptor to handle the stopped exception before awaiting it. It goes without saying that any future Standard Library coroutine types ought to implement unhandled_stopped. The author of Add lazy coroutine (coroutine task) type, 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 execution::par, 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.
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 policy while using scheduler to create any work to be run. The expression may be ill-formed if scheduler 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 executing_on 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 void 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.
execution::schedulerautosch1=get_system_thread_pool().scheduler();
-
-execution::senderautosnd1=execution::schedule(sch1);
-// snd1 describes the creation of a new task on the system thread pool
-
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 set_value.
Returns a sender whose value completion scheduler is the provided scheduler, which sends the provided values in the same manner as just.
-
execution::senderautovals=execution::transfer_just(
- get_system_thread_pool().scheduler(),
- 1,2,3
-);
-execution::senderautosnd=execution::then(vals,[](auto...args){
- std::print(args...);
-});
-// when snd is executed, it will print "123"
-
-
This adaptor is included as it greatly simplifies lifting values into senders.
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 set_error. 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 set_error.
-
4.20.5. execution::just_stopped
-
execution::senderautojust_stopped();
-
-
Returns a sender with no completion schedulers, which completes immediately by calling the receiver’s set_stopped.
Returns a sender that reaches into a receiver’s environment and pulls out the current value associated with the customization point denoted by Tag. It then sends the value read back to the receiver through the value channel. For instance, get_scheduler() (with no arguments) is a sender that asks the receiver for the currently suggested scheduler and passes it to the receiver’s set_value completion-signal.
-
This can be useful when scheduling nested dependent work. The following sender pulls the current schduler into the value channel and then schedules more work onto it.
-
execution::senderautotask=
- execution::get_scheduler()
- |execution::let_value([](autosched){
- returnexecution::on(sched,somenestedworkhere);
- });
-
-this_thread::sync_wait(std::move(task));// wait for it to finish
-
-
This code uses the fact that sync_wait associates a scheduler with the receiver that it connects with task. get_scheduler() reads that scheduler out of the receiver, and passes it to let_value's receiver’s set_value function, which in turn passes it to the lambda. That lambda returns a new sender that uses the scheduler to schedule some nested work onto sync_wait's scheduler.
-
4.21. User-facing sender adaptors
-
A sender adaptor is an algorithm that takes one or more senders, which it may execution::connect, 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.22.1 execution::start_detached and § 4.22.2 this_thread::sync_wait start senders.
execution::schedulerautocpu_sched=get_system_thread_pool().scheduler();
-execution::schedulerautogpu_sched=cuda::scheduler();
-
-execution::senderautocpu_task=execution::schedule(cpu_sched);
-// cpu_task describes the creation of a new task on the system thread pool
-
-execution::senderautogpu_task=execution::transfer(cpu_task,gpu_sched);
-// gpu_task describes the transition of the task graph described by cpu_task to the gpu
-
then 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.
-
then is guaranteed to not begin executing function until the returned sender is started.
-
execution::senderautoinput=get_input();
-execution::senderautosnd=execution::then(input,[](auto...args){
- std::print(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.
upon_error and upon_stopped are similar to then, but where then works with values sent by the input sender, upon_error works with errors, and upon_stopped is invoked when the "stopped" signal is sent.
let_value is very similar to then: 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 then sends exactly what that function ends up returning - let_value requires that the function return a sender, and the sender returned by let_value 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.
-
let_value is guaranteed to not begin executing function until the returned sender is started.
-
let_error and let_stopped are similar to let_value, but where let_value works with values sent by the input sender, let_error works with errors, and let_stopped is invoked when the "stopped" signal is sent.
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.
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.
Returns a sender that maps the value channel from a T to an optional<decay_t<T>>, and maps the stopped channel to a value of an empty optional<decay_t<T>>.
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 function will begin executing until the returned sender is started. Each invocation of function 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 bulk execute with the same guarantee. The number of execution agents used by bulk is not specified. This allows a scheduler to execute some invocations of the function in parallel.
-
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.
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.
when_all 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 set_value 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 when_all. It completes inline on the execution context on which the last input sender completes, unless stop is requested before when_all is started, in which case it completes inline within the call to start.
-
when_all_with_variant does the same, but it adapts all the input senders using into_variant, and so it does not constrain the input arguments as when_all does.
execution::schedulerautosched=thread_pool.scheduler();
-
-execution::senderautosends_1=...;
-execution::senderautosends_abc=...;
-
-execution::senderautoboth=execution::when_all(sched,
- sends_1,
- sends_abc
-);
-
-execution::senderautofinal=execution::then(both,[](auto...args){
- std::cout<<std::format("the two args: {}, {}",args...);
-});
-// when final executes, it will print "the two args: 1, abc"
-
Once ensure_started returns, it is known that the provided sender has been connected and start 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 execution::connect() is called, or if execution::connect() is called but the
-returned operation-state is destroyed before execution::start() 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 std::shared_ptr 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 execution::connect, as parameters, and does not return a sender.
this_thread::sync_wait is a sender consumer that submits the work described by the provided sender for execution, similarly to ensure_started, except that it blocks the current std::thread or thread of main 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, sync_wait 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, sync_wait throws that error as an exception, or rethrows the original exception if the error is of type std::exception_ptr.
-
If the provided sender sends the "stopped" signal instead of values, sync_wait returns an empty optional.
-
For an explanation of the requires clause, see § 5.8 All senders are typed. That clause also explains another sender consumer, built on top of sync_wait: sync_wait_with_variant.
-
Note: This function is specified inside std::this_thread, and not inside execution. This is because sync_wait 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 std::this_thread, 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 std::this_fiber::sync_wait would be provided. We also expect that runtimes with execution agents that use different synchronization mechanisms than std::thread's will provide their own flavors of sync_wait 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.
A receiver is a callback that supports more than one channel. In fact, it supports three of them:
-
-
-
set_value, which is the moral equivalent of an operator() or a function call, which signals successful completion of the operation its execution depends on;
-
-
set_error, 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; and
-
-
set_stopped, which signals that the operation completed without succeeding (set_value) and without failing (set_error). 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.
-
-
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 set_value failed with an exception, either set_error or set_stopped 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 std::promise, which provides the first two signals as set_value and set_error, 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.
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: start, 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 execution::ensure_started and this_thread::sync_wait, 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.
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 connect will be fulfilled.
-
execution::senderautosnd=someinputsender;
-execution::receiverautorcv=somereceiver;
-execution::operation_stateautostate=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 then, 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 execution::then(sender,invocable) to be equivalent to:
-
-
-
sender.then(invocable), if that expression is well formed; otherwise
-
-
then(sender,invocable), performed in a context where this call always performs ADL, if that expression is well formed; otherwise
-
-
a default implementation of then, which returns a sender adaptor, and then define the exact semantics of said adaptor.
-
-
However, this definition is problematic. Imagine another sender adaptor, bulk, 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 bulk to have specialized behavior, which invokes a kernel of more than one thread (with its size defined by the call to bulk); therefore, we would like to customize bulk for CUDA senders to achieve this. However, there’s no reason for CUDA kernels to
-necessarily customize the then sender adaptor, as the generic implementation is perfectly sufficient. This creates a problem, though; consider the following snippet:
-
execution::schedulerautocuda_sch=cuda_scheduler{};
-
-execution::senderautoinitial=execution::schedule(cuda_sch);
-// the type of initial is a type defined by the cuda_scheduler
-// let’s call it cuda::schedule_sender<>
-
-execution::senderautonext=execution::then(cuda_sch,[]{return1;});
-// 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::senderautokernel_sender=execution::bulk(next,shape,[](inti){...});
-
-
How can we specialize the bulk sender adaptor for our wrapped schedule_sender? 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):
However, if the input sender is not just a then_sender_adaptor like in the example above, but another sender that overrides bulk 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 execution::<sender-algorithm>(sender,args...), for any given sender algorithm that accepts a sender as its first argument, should be
-equivalent to:
-
-
-
tag_invoke(<sender-algorithm>,get_completion_scheduler<Signal>(get_attrs(sender)),sender,args...), if that expression is well-formed; otherwise
-
-
tag_invoke(<sender-algorithm>,sender,args...), if that expression is well-formed; otherwise
-
-
a default implementation, if there exists a default implementation of the given sender algorithm.
-
-
where Signal is one of set_value, set_error, or set_stopped. For most sender algorithms, the completion scheduler for set_value would be used, but for some (like upon_error or let_stopped), one of the others would be used.
-
For sender algorithms which accept concepts other than sender as their first argument, we propose that the customization scheme remains as it has been in A Unified Executors Proposal for C++ so far, except it should also use tag_invoke.
-
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 execution::start 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 execution::transfer 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 transfer 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 transfer.
-
To allow for such customization from both ends, we propose the inclusion of a secondary transitioning sender adaptor, called schedule_from. This adaptor is a form of schedule, 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 transfer, to ensure that both schedulers have a say in how the transitions are made. Any scheduler that specializes transfer(snd,sch) shall ensure that the
-return value of their customization is equivalent to schedule_from(sch,snd2), where snd2 is a successor of snd that sends values equivalent to those sent by snd.
-
The default implementation of transfer(snd,sched) is schedule_from(sched,snd).
-
5.8. All senders are typed
-
All senders must 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 execution::then is hard.
-
The mechanism for this advertisement is similar to the one in A Unified Executors Proposal for C++; the
-way to query the types is through completion_signatures_of_t<S,[Env]>::value_types<tuple_like,variant_like>.
-
completion_signatures_of_t::value_types 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 when_all). 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 std::optional to handle the set_stopped signal. However, this assumes that those values can be represented as a tuple,
-like here:
-
execution::senderautosends_1=...;
-execution::senderautosends_2=...;
-execution::senderautosends_3=...;
-
-auto[a,b,c]=this_thread::sync_wait(
- execution::transfer_when_all(
- execution::get_completion_scheduler<execution::set_value_t>(get_attrs(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 value_types of a sender which sends Types... to be as follows:
If senders could only ever send one specific set of values, this would probably need to be the required form of value_types 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) set_value 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, A Unified Executors Proposal for C++ also includes a second template parameter to value_types, one that represents a variant-like type. If we permit such senders, we would almost certainly need to require that the canonical form of value_types 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 Types1..., Types2..., ..., TypesN... to be as follows:
This, however, introduces a couple of complications:
-
-
-
A just(1) sender would also need to follow this structure, so the correct type for storing the value sent by it would be std::variant<std::tuple<int>> 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 where value_types 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.
-
-
As a consequence of (1): because sync_wait needs to store the above type, it can no longer return just a std::tuple<int> for just(1); it has to return std::variant<std::tuple<int>>. 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 of sync_wait must be the same across all sender types.
-
-
One possible solution to (2) above is to place a requirement on sync_wait that it can only accept senders which send only a single set of values, therefore removing the need for std::variant to appear in its API; because of this, we propose to expose both sync_wait, which is a simple, user-friendly version of the sender consumer, but requires that value_types have only one possible variant, and sync_wait_with_variant, which accepts any sender, but returns an optional whose value type is the variant of all the
-possible tuples sent by the input sender:
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 (A Unified Executors Proposal for C++ and Towards C++23 executors: A proposal for an initial set of algorithms) 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: sync_wait. We imagine that if, in the future, C++ was to
-gain fibers support, we would want to also have std::this_fiber::sync_wait, in addition to std::this_thread::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.
In short, instead of using globally reserved names, tag_invoke uses the type of the customization point object itself as the mechanism to find customizations. It globally reserves only a single name - tag_invoke - which itself is used the same way that
-ranges-style customization points are used. All other customization points are defined in terms of tag_invoke. For example, the customization for std::this_thread::sync_wait(s) will call tag_invoke(std::this_thread::sync_wait,s), instead of attempting
-to invoke s.sync_wait(), and then sync_wait(s) if the member call is not valid.
-
Using tag_invoke 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<typenameTag,typename...Args>
-friendautotag_invoke(Tag&&tag,wrapper&self,Args&&...args){
- returnstd::forward<Tag>(tag)(self.subobject,std::forward<Args>(args)...);
-}
-
-// but override one of them with a specific value
-friendautotag_invoke(specific_customization_point_t,wrapper&self){
- returnself.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 A Unified Executors Proposal for C++ 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 tag_invoke, dispatching to manually constructed vtables containing pointers to specialized implementations for the wrapped objects. For an example of such a polymorphic wrapper, see unifex::any_unique (example).
7.1.1.1. The std::terminate function [except.terminate]
-
At the end of the bulleted list in the Note in paragraph 1, add a new bullet as follows:
-
-
-
-
-
when a callback invocation exits via an exception when requesting stop on a std::stop_source or a std::in_place_stop_source ([stopsource.mem], [stopsource.inplace.mem]), or in
-the constructor of std::stop_callback or std::in_place_stop_callback ([stopcallback.cons], [stopcallback.inplace.cons]) when a callback invocation exits
-via an exception.
-
-
-
-
8. Library introduction [library]
-
- Add the header <execution> to Table 23: C++ library headers [tab:headers.cpp]
-
In subclause [conforming], after [lib.types.movedfrom], add the following new subclause with suggested stable name [lib.tmpl-heads].
-
-
-
-
- 16.4.6.17 Class template-heads
-
-
-
If a class template’s template-head is marked with "arguments are not
-associated entities"", any template arguments do not contribute to the
-associated entities ([basic.lookup.argdep]) of a function call where a
-specialization of the class template is an associated entity. In such a case,
-the class template may be implemented as an alias template referring to a
-templated class, or as a class template where the template arguments
-themselves are templated classes.
-
-
[Example:
-
template<classT>// arguments are not associated entities
-structS{};
-
-namespaceN{
- intf(auto);
- structA{};
-}
-
-intx=f(S<N::A>{});// error: N::f not a candidate
-
-
The template S specified above may be implemented as
Insert this section as a new subclause, between Searchers [func.search] and Class template hash[unord.hash].
-
-
-
-
-
The name std::tag_invoke denotes a customization point object [customization.point.object]. Given subexpressions T and A..., the expression std::tag_invoke(T,A...) is expression-equivalent [defns.expression-equivalent] to tag_invoke(T,A...) if it is a well-formed expression with overload resolution performed in a context in which unqualified lookup for tag_invoke finds only the declaration
-
voidtag_invoke();
-
-
Otherwise, std::tag_invoke(T,A...) is ill-formed.
-
-
[Note: Diagnosable ill-formed cases above result in substitution failure when std::tag_invoke(T,A...) appears in the immediate context of a template instantiation. —end note]
Insert this section as a new subclause between Header <stop_token> synopsis [thread.stoptoken.syn] and Class stop_token[stoptoken].
-
-
-
-
-
The stoppable_token concept checks for the basic interface of a stop token
-that is copyable and allows polling to see if stop has been requested and
-also whether a stop request is possible. For a stop token type T and a type CB that is callable with no arguments, the type T::callback_type<CB> is
-valid and denotes 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. The stoppable_token_for concept checks for a stop token type compatible
-with a given callback type. The unstoppable_token concept checks for a stop
-token type that does not allow stopping.
- LWG directed me to replace T::stop_possible() with t.stop_possible() because
-of the recent constexpr changes in P2280R2. However, even with those changes, a nested
-requirement like requires(!t.stop_possible()), where t is an argument in the requirement-parameter-list, is ill-formed according to [expr.prim.req.nested/p2]:
-
-
A local parameter shall only appear as an unevaluated operand within the constraint-expression.
Let t and u be distinct, valid objects of type T. The type T models stoppable_token only if:
-
-
-
If t.stop_possible() evaluates to false then, if t and u reference the same logical shared stop state, u.stop_possible() shall also subsequently evaluate to false and u.stop_requested() shall also subsequently evaluate to false.
-
-
If t.stop_requested() evaluates to true then, if t and u reference the same logical shared stop state, u.stop_requested() shall also subsequently evaluate to true and u.stop_possible() shall also subsequently evaluate to true.
-
-
-
Let t and u be distinct, valid objects of type T and let init be an
-object of type Initializer. Then for some type CB, the type T models stoppable_token_for<CB,Initializer> only if:
Direct non-list initializing an object cb of type T::callback_type<CB> from t,init shall, if t.stop_possible() is true, construct an
-instance, callback, of type CB, direct-initialized with init,
-and register callback with t'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.
-
-
-
If t.stop_requested() evaluates to true at the time callback is
-registered then callback may be invoked on the thread executing cb's constructor.
-
-
If callback is invoked then, if t and u reference the same shared stop
-state, an evaluation of u.stop_requested() will be true if the beginning of the invocation of callback strongly-happens-before the evaluation of u.stop_requested().
-
-
[Note: If t.stop_possible() evaluates to false then the construction of cb is not required to construct and initialize callback. --end note]
-
-
-
Construction of a T::callback_type<CB> instance shall only throw exceptions thrown by the initialization of the CB instance from the value of type Initializer.
-
-
Destruction of the T::callback_type<CB> object, cb, removes callback from the shared stop state such that callback will not be invoked after the destructor returns.
-
-
-
If callback is currently being invoked on another thread then the destructor of cb will block until the invocation of callback returns such that the return from the invocation of callback strongly-happens-before the destruction of callback.
-
-
Destruction of a callback cb shall not block on the completion of the invocation of some other callback registered with the same shared stop state.
-
-
-
-
-
-
10.1.3. Class stop_token[stoptoken]
-
10.1.3.1. General [stoptoken.general]
-
Modify the synopsis of class stop_token in section General [stoptoken.general] as follows:
Insert a new subclause, Class never_stop_token[stoptoken.never], after section Class template stop_callback[stopcallback], as a new subclause of Stop tokens [thread.stoptoken].
-
10.1.4.1. General [stoptoken.never.general]
-
-
-
The class never_stop_token provides an implementation of the unstoppable_token concept. It provides a stop token interface, but also provides static information that a stop is never possible nor requested.
10.1.5. Class in_place_stop_token[stoptoken.inplace]
-
Insert a new subclause, Class in_place_stop_token[stoptoken.inplace], after the section added above, as a new subclause of Stop tokens [thread.stoptoken].
-
10.1.5.1. General [stoptoken.inplace.general]
-
-
-
The class in_place_stop_token provides an interface for querying whether a stop request has been made (stop_requested) or can ever be made (stop_possible) using an associated in_place_stop_source object ([stopsource.inplace]).
-An in_place_stop_token can also be passed to an in_place_stop_callback ([stopcallback.inplace]) constructor to register a callback to be called when a stop request has been made from an associated in_place_stop_source.
10.1.5.2. Constructors, copy, and assignment [stoptoken.inplace.cons]
-
in_place_stop_token()noexcept;
-
-
-
-
Effects: initializes source_ with nullptr.
-
-
voidswap(stop_token&rhs)noexcept;
-
-
-
-
Effects: Exchanges the values of source_ and rhs.source_.
-
-
10.1.5.3. Members [stoptoken.inplace.mem]
-
[[nodiscard]]boolstop_requested()constnoexcept;
-
-
-
-
Effects: Equivalent to: returnsource_!=nullptr&&source_->stop_requested();
-
-
[Note: The behavior of stop_requested() is undefined unless the call
-strongly happens before the start of the destructor of the associated in_place_stop_source, if any ([basic.life]). --end note]
-
-
[[nodiscard]]boolstop_possible()constnoexcept;
-
-
-
-
Effects: Equivalent to: returnsource_!=nullptr;
-
-
[Note: The behavior of stop_possible() is implementation-defined unless
-the call strongly happens before the end of the storage duration of the
-associated in_place_stop_source object, if any ([basic.stc.general]). --end note]
10.1.6. Class in_place_stop_source[stopsource.inplace]
-
Insert a new subclause, Class in_place_stop_source[stopsource.inplace], after the section added above, as a new subclause of Stop tokens [thread.stoptoken].
-
10.1.6.1. General [stopsource.inplace.general]
-
-
-
The class in_place_stop_source implements the semantics of making a stop request, without the need for a dynamic allocation of a shared state.
-A stop request made on a in_place_stop_source object is visible to all associated in_place_stop_token ([stoptoken.inplace]) objects.
-Once a stop request has been made it cannot be withdrawn (a subsequent stop request has no effect).
-All uses of in_place_stop_token objects associated with a given in_place_stop_source object must happen before the start of the destructor of that in_place_stop_source object.
An instance of in_place_stop_source maintains a list of registered callback invocations.
-The registration of a callback invocation either succeeds or fails. When an invocation
-of a callback is registered, the following happens atomically:
-
-
-
The stop state is checked. If stop has not been requested, the callback invocation is
-added to the list of registered callback invocations, and registration has succeeded.
-
-
Otherwise, registration has failed.
-
-
When an invocation of a callback is unregistered, the invocation is atomically removed
-from the list of registered callback invocations. The removal is not blocked by the concurrent
-execution of another callback invocation in the list. If the callback invocation
-being unregistered is currently executing, then:
-
-
-
If the execution of the callback invocation is happening concurrently on another thread,
-the completion of the execution strongly happens before ([intro.races]) the end of the
-callback’s lifetime.
-
-
Otherwise, the execution is happening on the current thread. Removal of the
-callback invocation does not block waiting for the execution to complete.
-
-
-
10.1.6.2. Constructors, copy, and assignment [stopsource.inplace.cons]
-
in_place_stop_source()noexcept;
-
-
-
-
Effects: Initializes a new stop state inside *this.
Returns: A new associated in_place_stop_token object.
-
-
[[nodiscard]]boolstop_requested()constnoexcept;
-
-
-
-
Returns: true if the stop state inside *this has received a stop request; otherwise, false.
-
-
boolrequest_stop()noexcept;
-
-
-
-
Effects: Atomically determines whether the stop state inside *this has received a stop request, and if not, makes a stop request.
-The determination and making of the stop request are an atomic read-modify-write operation ([intro.races]).
-If the request was made, the registered invocations are executed and the evaluations of the invocations are indeterminately sequenced.
-If an invocation of a callback exits via an exception then terminate is invoked ([except.terminate]).
-
-
Postconditions: stop_requested() is true.
-
-
Returns: true if this call made a stop request; otherwise false.
-
-
10.1.7. Class template in_place_stop_callback[stopcallback.inplace]
-
Insert a new subclause, Class template in_place_stop_callback[stopcallback.inplace], after the section added above, as a new subclause of Stop tokens [thread.stoptoken].
Mandates: in_place_stop_callback is instantiated with an argument for the template parameter Callback that satisfies both invocable and destructible.
-
-
Preconditions: in_place_stop_callback is instantiated with an argument for the template parameter Callback that models both invocable and destructible.
-
-
Recommended practice: Implementations should use the storage of the in_place_stop_callback objects to store the state necessary for their association with an in_place_stop_source object.
-
-
10.1.7.2. Constructors and destructor [stopcallback.inplace.cons]
Constraints: Callback and C satisfy constructible_from<Callback,C>.
-
-
Preconditions: Callback and C model constructible_from<Callback,C>.
-
-
Effects: Initializes callback_ with std::forward<C>(cb).
-Any in_place_stop_source associated with st becomes associated with *this.
-Registers ([stopsource.inplace.general]) the callback invocation std::forward<Callback>(callback_)() with the associated in_place_stop_source, if any. If the registration fails, evaluates
-the callback invocation.
-
-
Throws: Any exception thrown by the initialization of callback_.
-
-
Remarks: If evaluating std::forward<Callback>(callback_)() exits via an exception, then terminate is invoked ([except.terminate]).
-
-
~in_place_stop_callback();
-
-
-
-
Effects: Unregisters ([stopsource.inplace.general]) the callback invocation from
-the associated in_place_stop_source object, if any.
-
-
Remarks: A program has undefined behavior if the start of this destructor does
-not strongly happen before the start of the destructor of the associated in_place_stop_source object, if any.
-
-
11. Execution control library [exec]
-
-
-
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.
-
-
-
Table 1: Execution control library summary [tab:execution.summary]
[Note: A large number of execution control primitives are customization point objects. For an object one might define multiple types of customization point objects, for which different rules apply. Table 2 shows the types of customization point objects used in the execution control library:
-
-
-
Table 2: Types of customization point objects in the execution control library [tab:execution.cpos]
-
-
-
Customization point object type
-
Purpose
-
Examples
-
-
core
-
provide core execution functionality, and connection between core components
-
connect, start, execute
-
-
completion signals
-
called by senders to announce the completion of the work (success, error, or cancellation)
-
template<classT>
- conceptmovable-value=// exposition only
- move_constructible<decay_t<T>>&&
- constructible_from<decay_t<T>,T>;
-
-
11.3. Queries [exec.queries]
-
-
-
A query object is a customization point object
-([customization.point.object]) that accepts as its
-first argument a queryable object, and for each such invocation that is
-valid, produces a value of the corresponding property of the object.
-
-
Unless otherwise specified, given a queryable object e, a query object Q, and a pack of subexpressions args, the value
-returned by the expression Q(e,args...) is valid as
-long as e is valid.
Let e be an object of type E. The type E models queryable if for each callable
-object Q and a pack of subexpressions args, if requires{Q(e,args...)} is true then Q(e,args...) meets any semantic requirements imposed by Q.
-
-
11.3.2. std::forwarding_query[exec.fwd_env]
-
-
-
std::forwarding_query is used to ask a query object whether it should be forwarded through queryable adaptors.
-
-
The name std::forwarding_query denotes a query object. For some query object q of type Q, std::forwarding_query(q) is expression equivalent to:
-
-
-
tag_invoke(std::forwarding_query,q) if that expression is well-formed.
-
-
-
Mandates: The expression is a core constant expressions if q is a core constant expression, has type bool, and is not potentially-throwing.
-
-
-
Otherwise, true if derived_from<Q,std::forwarding_query_t> is true.
execution::get_delegatee_scheduler is used to ask an object for a scheduler that may be used to delegate work to for the purpose of forward progress delegation.
-
-
The name execution::get_delegatee_scheduler denotes a query object. For some subexpression r, execution::get_delegatee_scheduler(r) is expression equivalent to:
-
-
-
tag_invoke(execution::get_delegatee_scheduler,as_const(r)), if this expression is well formed.
-
-
-
Mandates: The tag_invoke expression above is not
-potentially-throwing and its type satisfies execution::scheduler.
-
-
-
Otherwise, execution::get_delegatee_scheduler(r) is ill-formed.
-
-
-
std::forwarding_query(std::get_delegatee_scheduler) is true.
-
-
execution::get_delegatee_scheduler() (with no arguments) is expression-equivalent to execution::read(execution::get_delegatee_scheduler) ([exec.read]).
execution::get_forward_progress_guarantee is used to ask a scheduler about the forward progress guarantees of execution agents created by that scheduler.
-
-
The name execution::get_forward_progress_guarantee denotes a query object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::scheduler, execution::get_forward_progress_guarantee is ill-formed.
-Otherwise, execution::get_forward_progress_guarantee(s) is expression equivalent to:
-
-
-
tag_invoke(execution::get_forward_progress_guarantee,as_const(s)), if this expression is well formed.
-
-
-
Mandates: The tag_invoke expression above is not potentially
-throwing and its type is execution::forward_progress_guarantee.
If execution::get_forward_progress_guarantee(s) for some scheduler s returns execution::forward_progress_guarantee::concurrent, all execution agents created by that scheduler shall provide the concurrent forward progress guarantee. If it returns execution::forward_progress_guarantee::parallel, all execution agents created by that scheduler shall provide at least the parallel forward progress guarantee.
this_thread::execute_may_block_caller is used to ask a scheduler s whether a call execution::execute(s,f) with any invocable f may block the thread where such a call occurs.
-
-
The name this_thread::execute_may_block_caller denotes a query object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::scheduler, this_thread::execute_may_block_caller is ill-formed. Otherwise, this_thread::execute_may_block_caller(s) is expression equivalent to:
-
-
-
tag_invoke(this_thread::execute_may_block_caller,as_const(s)), if this expression is well formed.
-
-
-
Mandates: The tag_invoke expression above is not potentially
-throwing and its type is bool.
-
-
-
Otherwise, true.
-
-
-
If this_thread::execute_may_block_caller(s) for some scheduler s returns false, no execution::execute(s,f) call with some invocable f shall block the calling thread.
execution::get_completion_scheduler is used to obtain the completion scheduler for one of its signals from the sender attributes.
-
-
The name execution::get_completion_scheduler denotes a query object template.
-For some subexpression q, let Q be decltype((q)). If the template
-argument CPO in execution::get_completion_scheduler<CPO>(q) is not one of execution::set_value_t, execution::set_error_t, or execution::set_stopped_t, execution::get_completion_scheduler<CPO>(q) is ill-formed. Otherwise, execution::get_completion_scheduler<CPO>(q) is expression-equivalent to:
-
-
-
tag_invoke(execution::get_completion_scheduler<CPO>,as_const(q)) if this expression is well formed.
-
-
-
Mandates: The tag_invoke expression above is not potentially throwing and its type satisfies execution::scheduler.
-
-
-
Otherwise, execution::get_completion_scheduler<CPO>(q) is ill-formed.
-
-
-
If, for some sender s and customization point object CPO, get_completion_scheduler<decltype(CPO)>(get_attrs(s)) is well-formed and results in a scheduler sch, and the sender s invokes CPO(r,args...), for some receiver r that has been connected to s, with additional arguments args..., on an execution agent that does not belong to the associated execution context of sch, the behavior is undefined.
-
-
The expression execution::forwarding_query(get_completion_scheduler<CPO>) has value true.
-
-
11.4. Execution environments [exec.env]
-
-
-
An execution environment is a queryable object ([exec.queryable]) that contains
-state associated with the completion of an asynchronous operation. Every receiver
-has an associated execution environment, accessible with the get_env customization
-point object ([exec.get_env]).
-
-
11.4.1. execution::get_env[exec.get_env]
-
-
-
get_env is a customization point object. For some subexpression r, get_env(r) is expression-equivalent to
-
-
-
tag_invoke(execution::get_env,r) if that expression is well-formed.
-
-
-
Mandates: The type of the expression satisfies the queryable concept ([exec.queryable]).
-
-
-
Otherwise, get_env(r) is ill-formed.
-
-
-
If get_env(r) is an lvalue, the object it refers to shall be valid while r is valid.
-
-
11.5. Schedulers [exec.sched]
-
-
-
The scheduler concept defines the requirements of a type that allows for scheduling of work on its associated execution context.
Let S be the type of a scheduler and let E be the type of an execution environment for which sender_in<schedule_result_t<S>,E> is true. Then sender_of<schedule_result_t<S>,set_value_t(),E> shall be true.
-
-
None of a scheduler’s copy constructor, destructor, equality comparison, or swap member functions shall exit via an exception.
-
-
None of these member functions, nor a scheduler type’s schedule function, shall introduce data races as a result of concurrent invocations of those functions from different threads.
-
-
For any two (possibly const) values s1 and s2 of some scheduler type S, s1==s2 shall return true only if both s1 and s2 are handles to the same associated execution context.
-
-
For a given scheduler expression s, the expression execution::get_completion_scheduler<set_value_t>(execution::get_attrs(execution::schedule(s))) shall compare equal to s.
-
-
A scheduler type’s destructor shall not block pending completion of any receivers connected to the sender objects returned from schedule. The ability to wait for completion of submitted function objects may be provided by the associated execution
-context of the scheduler.
-
-
11.6. Receivers [exec.recv]
-
-
-
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, set_error, and set_stopped. These are
-collectively known as a receiver’s completion-signal operations.
-
-
The receiver concept defines the requirements for a receiver type with an
-unknown set of completion signatures. The receiver_of concept defines the
-requirements for a receiver type with a known set of completion signatures.
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 execution::start has been called on the operation state object that was returned by execution::connect to connect that receiver to a sender.
-
-
Once execution::start has been called on the operation state object, exactly one of the receiver’s completion-signal operations shall complete before the receiver is destroyed.
-
-
-
Once one of a receiver’s completion-signal operations has completed, the receiver contract has been satisfied.
-
-
Receivers have an associated execution environment ([exec.env]) that is accessible
-by passing the receiver to execution::get_env. A sender algorithm can obtain
-information about the current execution environment by querying the environment of
-the receiver to which its sender is connected.
-
-
Given a non-const rvalue r of type R where R models receiver,
-then the expressions Q(get_env(r),args...) and Q(get_attrs(static_cast<constR&>(r)),args...) shall be equivalent for any query object Q and for
-any pack of subexpressions args.
-
-
Let r be a receiver, s be a sender, and op_state be an operation state
-resulting from an execution::connect(s,r) call. Let token be a stop
-token resulting from an execution::get_stop_token(execution::get_env(r)) call. token must remain valid at least until a call to a receiver
-completion-signal function of r returns successfully. [Note: this
-means that, unless it knows about further guarantees provided by the
-receiver r, the implementation of op_state should not use token after
-it makes a call to a receiver completion-signal function of r. This also
-implies that any stop callbacks registered on token by the implementation
-of op_state or s must be destroyed before such a call to a receiver
-completion-signal function of r. --end note]
-
-
11.6.1. execution::set_value[exec.set_value]
-
-
-
execution::set_value is used to send a value completion signal to a receiver.
-
-
The name execution::set_value denotes a customization point object. The
-expression execution::set_value(R,Vs...) for some subexpression R and
-pack of subexpressions Vs is expression-equivalent to:
-
-
-
tag_invoke(execution::set_value,R,Vs...), if that expression is
-valid. The function selected by tag_invoke shall send the
-value(s) Vs... to the receiver R’s value channel.
-
-
-
Mandates: The tag_invoke expression above is not potentially
-throwing.
-
-
-
Otherwise, execution::set_value(R,Vs...) is ill-formed.
-
-
-
11.6.2. execution::set_error[exec.set_error]
-
-
-
execution::set_error is used to send an error completion signal to a receiver.
-
-
The name execution::set_error denotes a customization point object. The expression execution::set_error(R,E) for some subexpressions R and E is expression-equivalent to:
-
-
-
tag_invoke(execution::set_error,R,E), if that expression is valid.
-The function selected by tag_invoke shall send the error E to the
-receiver R’s error channel.
-
-
-
Mandates: The tag_invoke expression above is not potentially
-throwing.
-
-
-
Otherwise, execution::set_error(R,E) is ill-formed.
-
-
-
11.6.3. execution::set_stopped[exec.set_stopped]
-
-
-
execution::set_stopped is used to send a stopped completion signal to a receiver.
-
-
The name execution::set_stopped denotes a customization point object. The expression execution::set_stopped(R) for some subexpression R is expression-equivalent to:
-
-
-
tag_invoke(execution::set_stopped,R), if that expression is valid. The function selected by tag_invoke shall signal the receiver R’s stopped channel.
-
-
-
Mandates: The tag_invoke expression above is not potentially
-throwing.
-
-
-
Otherwise, execution::set_stopped(R) is ill-formed.
-
-
-
11.7. Operation states [exec.op_state]
-
-
-
The operation_state concept defines the requirements for an operation state type, which allows for starting the execution of work.
Any operation state types defined by the implementation are non-movable types.
-
-
11.7.1. execution::start[exec.op_state.start]
-
-
-
execution::start is used to start work represented by an operation state object.
-
-
The name execution::start denotes a customization point object. The expression execution::start(O) for some lvalue subexpression O is expression-equivalent to:
-
-
-
tag_invoke(execution::start,O), if that expression is valid. If the function selected by tag_invoke does not start the work represented by the operation state O, the behavior of calling execution::start(O) is undefined.
-
-
-
Mandates: The tag_invoke expression above is not potentially
-throwing.
-
-
-
Otherwise, execution::start(O) is ill-formed.
-
-
-
The caller of execution::start(O) must guarantee that the lifetime of the operation state object O extends at least until one of the receiver completion-signal functions of a receiver R passed into the execution::connect call that produced O is ready
-to successfully return. 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.
-
-
11.8. Senders [exec.snd]
-
-
-
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.
-
-
11.8.1. execution::get_attrs[exec.get_attrs]
-
-
-
get_attrs is a customization point object. For some subexpression s, get_attrs(s) is expression-equivalent to
-
-
-
tag_invoke(execution::get_attrs,s) if that expression is well-formed.
-
-
-
Mandates: The type of the expression satisfies the queryable concept ([exec.queryable]).
-
-
-
Otherwise, empty-attrs{} if the type of s satisfies
-the is-awaitable<env-promise<empty-env>> concept ([exec.complsigs]).
-
-
Otherwise, get_attrs(s) is ill-formed.
-
-
-
If get_attrs(s) is an lvalue, the object it refers to shall be valid while s is valid.
-
-
11.8.2. Sender concepts [exec.snd_concepts]
-
-
-
The sender concept defines the requirements for a sender type.
-The sender_in concept defines the requirements for a sender within a
-particular execution environment.
-The sender_to concept defines the requirements for a sender type capable of
-being connected with a specific receiver type.
Given a non-const rvalue s of type S where S models sender,
-then the expressions Q(get_attrs(s),args...) and Q(get_attrs(static_cast<constS&>(s)),args...) shall be equivalent for any query object Q and for
-any pack of subexpressions args.
-
-
The sender_of concept defines the requirements for a sender type that
-completes with the completion signature specified for the given completion
-channel.
The alias template completion_signatures_of_t is used to obtain from a sender the list of completion signatures it may complete with.
-
-
completion_signatures_of_t also recognizes awaitables as senders. For this clause ([exec]):
-
-
-
An awaitable is an expression that would be well-formed as the operand of a co_await expression within a given context.
-
-
For any type T, is-awaitable<T> is 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 await_transform. For a coroutine promise type P, is-awaitable<T,P> is 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 a such that decltype((a)) is type A, await-result-type<A> is an alias for decltype(e), where e is a's await-resume expression ([expr.await]) within the context of a coroutine whose promise type does not define a member await_transform. For a coroutine promise type P, await-result-type<A,P> is an alias for decltype(e), where e is a's await-resume expression ([expr.await]) within the context of a coroutine whose promise type is P.
-
-
-
For types S and E, the type completion_signatures_of_t<S,E> is an
-alias for decltype(get_completion_signatures(declval<S>(),declval<E>())) if that expression is well-formed.
-
-
Given an execution environment type Env, let p be a non-const lvalue
-of type env-promise<Env>, where env-promise<Env> names a class type such that:
-
-
-
For some subexpression e, the expression p.await_transform(e) is
-expression-equivalent to tag_invoke(as_awaitable,e,p) if that
-expression is well-formed ([exec.as_awaitable]); otherwise, e.
-
-
decltype(get_env(as_const(p))) names the type constEnv&.
-
-
-
execution::get_completion_signatures is a customization point object. Let s be an expression such that decltype((s)) is S, and let e be an
-expression such that decltype((e)) is E. Then get_completion_signatures(s,e) is expression-equivalent to:
-
-
-
tag_invoke_result_t<get_completion_signatures_t,S,E>{} if that expression is well-formed,
-
-
-
Mandates:is-instance-of<Sigs,completion_signatures>, where Sigs names the type tag_invoke_result_t<get_completion_signatures_t,S,E>.
-
-
-
Otherwise, if remove_cvref_t<S>::completion_signatures is well-formed
-and names a type, then a value-initialized prvalue of type remove_cvref_t<S>::completion_signatures,
-
-
-
Mandates:is-instance-of<Sigs,completion_signatures>, where Sigs names the type remove_cvref_t<S>::completion_signatures.
-
-
-
Otherwise, if is-awaitable<S,env-promise<E>> is true, then
-
-
-
If await-result-type<S,env-promise<E>> is cvvoid then a prvalue of a type equivalent to:
Otherwise, get_completion_signatures(s,e) is ill-formed.
-
-
-
The exposition-only type variant-or-empty<Ts...> is
- defined as follows:
-
-
-
If sizeof...(Ts) is greater than zero, variant-or-empty<Ts...> names the type variant<Us...> where Us... is the pack decay_t<Ts>... with duplicate types removed.
-
-
Otherwise, variant-or-empty<Ts...> names an implementation defined class type equivalent to the following:
Let r be an rvalue receiver of type R, and let S be the type of a
-sender. If value_types_of_t<S,env_of_t<R>,Tuple,Variant> is well
-formed, it shall name the type Variant<Tuple<Args0...>,Tuple<Args1...>,...,Tuple<ArgsN...>>>, where the type packs Args0 through ArgsN are the packs of types the sender S passes as arguments to execution::set_value (besides the receiver object).
-Such a sender S shall not odr-use ([basic.def.odr]) execution::set_value(r,args...), where decltype(args)... is not one of the type packs Args0... through ArgsN... (ignoring differences in
-rvalue-reference qualification).
-
-
Let r be an rvalue receiver of type R, and let S be the type of a
-sender. If error_types_of_t<S,env_of_t<R>,Variant> is well formed, it
-shall name the type Variant<E0,E1,...,EN>, where the types E0 through EN are the types the sender S passes as arguments to execution::set_error (besides the receiver object). Such a sender S shall not odr-use execution::set_error(r,e), where decltype(e) is not one of the types E0 through EN (ignoring differences in rvalue-reference qualification).
-
-
Let r be an rvalue receiver of type R, and let S be the type of a
-sender. If sends_stopped<S,env_of_t<R>> is well formed and false, such a sender S shall not odr-use execution::set_stopped(r).
-
-
[Note: The types Argsi... and Ei... captured in value_types and error_types can appear in any order.
- For example, a sender that can yield, in case of an error, either exception_ptr or error_code can have error_types be either Variant<exception_ptr,error_code> or Variant<error_code,exception_ptr>. --end note]
-
-
11.8.4. execution::connect[exec.connect]
-
-
-
execution::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.
-
-
The name execution::connect denotes a customization point object. For some subexpressions s and r, let S be decltype((s)) and R be decltype((r)), and let S' and R' be the decayed types of S and R, respectively. If R does not satisfy execution::receiver, execution::connect(s,r) is ill-formed. Otherwise, the expression execution::connect(s,r) is expression-equivalent to:
-
-
-
tag_invoke(execution::connect,s,r), if the constraints below are satisfied. If the function selected by tag_invoke does not return an operation state for which execution::start starts work described by s, the behavior of calling execution::connect(s,r) is undefined.
Mandates: The type of the tag_invoke expression above satisfies operation_state.
-
-
-
Otherwise, connect-awaitable(s,r) if is-awaitable<S,connect-awaitable-promise> is true and that expression is valid, where connect-awaitable is a coroutine equivalent to the following:
where connect-awaitable-promise is the promise type of connect-awaitable, and where connect-awaitable suspends at the initial suspends point ([dcl.fct.def.coroutine]), and:
-
-
-
set-value-expr first evaluates co_awaitstd::move(s), then suspends the coroutine and evaluates execution::set_value(std::move(r)) if await-result-type<S,connect-awaitable-promise> is cvvoid; otherwise, it evaluates auto&&res=co_awaitstd::move(s), then suspends the coroutine and evaluates execution::set_value(std::move(r),std::forward<decltype(res)>(res)).
-
If the call to execution::set_value exits with an exception, the coroutine is resumed and the exception is immediately propagated in the context of the coroutine.
-
[Note: If the call to execution::set_value exits normally, then the connect-awaitable coroutine is never resumed. --end note]
-
-
set-error-expr first suspends the coroutine and then executes execution::set_error(std::move(r),std::move(ep)).
-
[Note: The connect-awaitable coroutine is never resumed after the call to execution::set_error. --end note]
-
-
operation-state-task is a type that models operation_state. Its execution::start resumes the connect-awaitable coroutine, advancing it past the initial suspend point.
-
-
Let p be an lvalue reference to the promise of the connect-awaitable coroutine, let b be a const lvalue reference to the receiver r. Then get_env(p) is expression-equivalent to get_env(b).
-
-
The expression p.unhandled_stopped() is expression-equivalent to (execution::set_stopped(std::move(r)),noop_coroutine()).
-
-
For some expression e, the expression p.await_transform(e) is expression-equivalent to tag_invoke(as_awaitable,e,p) if that expression is well-formed ([exec.as_awaitable]); otherwise, it is expression-equivalent to e.
-
-
Let Res be await-result-type<S,connect-awaitable-promise>, and let Vs... be an empty parameter pack if Res is cvvoid, or a pack containing the single type Res otherwise. The operand of the requires-clause of connect-awaitable is equivalent to receiver_of<R,Sigs> where Sigs names the type:
Standard sender types shall always expose an rvalue-qualified overload of a customization of execution::connect. Standard sender types shall only expose an lvalue-qualified overload of a customization of execution::connect if they are copyable.
-
-
11.8.5. Sender factories [exec.factories]
-
11.8.5.1. General [exec.factories.general]
-
-
-
Subclause [exec.factories] defines sender factories, which are utilities that return senders without accepting senders as arguments.
-
-
11.8.5.2. execution::schedule[exec.schedule]
-
-
-
execution::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.
-
-
The name execution::schedule denotes a customization point object. For some subexpression s, the expression execution::schedule(s) is expression-equivalent to:
-
-
-
tag_invoke(execution::schedule,s), if that expression is valid. If the function selected by tag_invoke does not return a sender whose set_value completion scheduler is equivalent to s, the behavior of calling execution::schedule(s) is undefined.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
execution::just is used to create a sender that propagates a set of values to a connected receiver. execution::just_error is used to create a sender that propagates an error to a connected receiver. execution::just_stopped is used to create a sender that propagates a stopped signal to a connected receiver.
The name execution::just denotes a customization point object. For some subexpressions vs..., let Vs... be decltype((vs)).... If any type V in Vs does not satisfy movable-value, execution::just(vs...) is ill-formed.
-Otherwise, execution::just(vs...) is expression-equivalent to just-sender<set_value_t,decay_t<Ts>...>(vs...).
-
-
The name execution::just_error denotes a customization point object. For some subexpression err, let Err be decltype((err)). If Err does not satisfy movable-value, execution::just_error(err) is ill-formed.
-Otherwise, execution::just_error(err) is expression-equivalent to just-sender<set_error_t,decay_t<Err>>(err).
-
-
Then name execution::just_stopped denotes a customization point object. execution::just_stopped is expression-equivalent to just-sender<set_stopped_t>().
execution::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.
-
-
The name execution::transfer_just denotes a customization point object. For some subexpressions s and vs..., let S be decltype((s)) and Vs... be decltype((vs)). If S does not satisfy execution::scheduler, or any type V in Vs does not
-satisfy movable-value, execution::transfer_just(s,vs...) is ill-formed. Otherwise, execution::transfer_just(s,vs...) is expression-equivalent to:
-
-
-
tag_invoke(execution::transfer_just,s,vs...), if that expression is
-valid. If the function selected by tag_invoke does not return a sender
-whose set_value completion scheduler is equivalent to s and sends
-values equivalent to auto(vs)... to a receiver connected to it, the
-behavior of calling execution::transfer_just(s,vs...) is undefined.
-
-
-
Mandates:execution::sender_of<R,execution::set_value_t(decltype(auto(vs))...)>, where R is the type of the tag_invoke expression above.
execution::read is used to create a sender that retrieves a value from the receiver’s associated environment and sends it back to the receiver through the value channel.
-
-
execution::read is a customization point object of an unspecified class type equivalent to:
-
template<classTag>
- structread-sender;// exposition only
-
-structread-t{// exposition only
- template<classTag>
- read-sender<Tag>operator()(Tag)constnoexcept{return{};}
-};
-
-
-
read-sender is an exposition only class template equivalent to:
-
template<classTag>
- structread-sender{// exposition only
- template<classR>
- structoperation-state{// exposition only
- Rr_;// exposition only
-
- friendvoidtag_invoke(start_t,operation-state&s)noexcept{
- TRY-SET-VALUE(std::move(s.r_),auto(Tag{}(get_env(s.r_))));
- }
- };
-
- template<receiverR>
- friendoperation-state<decay_t<R>>tag_invoke(connect_t,read-sender,R&&r){
- return{std::forward<R>(r)};
- }
-
- template<classEnv>
- requirescallable<Tag,Env>
- friendautotag_invoke(get_completion_signatures_t,read-sender,Env)
- ->completion_signatures<
- set_value_t(call-result-t<Tag,Env>),set_error_t(exception_ptr)>;// not defined
-
- template<classEnv>
- requiresnothrow-callable<Tag,Env>
- friendautotag_invoke(get_completion_signatures_t,read-sender,Env)
- ->completion_signatures<set_value_t(call-result-t<Tag,Env>)>;// not defined
-
- friendempty-attrstag_invoke(get_attrs_t,constread-sender&)noexcept{
- return{};
- }
- };
-
-
where TRY-SET-VALUE(r,e), for two subexpressions r and e,
-is equivalent to:
if e is potentially throwing; or execution::set_value(r,e) otherwise.
-
-
11.8.6. Sender adaptors [exec.adapt]
-
11.8.6.1. General [exec.adapt.general]
-
-
-
Subclause [exec.adapt] 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 execution::connect, and execution::start is called on the resulting operation state. This requirement applies to any function that is selected by the implementation of the sender adaptor.
-
-
Unless otherwise specified, all sender adaptors that accept a single sender argument return sender objects that delegate get_attrs to that single sender argument. Unless otherwise specified, all sender adaptors that accept more than one sender argument return sender objects that return empty-attrs from get_attrs. These requirements apply to any function that is selected by the implementation of the
-sender adaptor.
-
-
Unless otherwise specified, whenever a sender adaptor constructs a receiver that it passes to another sender’s connect, that receiver shall delegate get_env to a receiver accepted as an argument of execution::connect. This requirements
-applies to any sender returned from a function that is selected by the implementation of such sender adaptor.
-
-
For any sender type, receiver type, operation state type, execution environment type, or coroutine promise type that is part of the implementation of any sender adaptor in this subclause and that is a class template, the template arguments do not contribute to the associated entities ([basic.lookup.argdep]) of a function call where a specialization of the class template is an associated entity.
If the specification of a sender adaptor requires that the implementation of the get_completion_signatures customization point adds the signature set_error_t(exception_ptr) as an additional signature, but a customization of that sender adaptor never
-calls the exception_ptr overload of set_error, that customization is allowed to omit the set_error_t(exception_ptr) additional signature from its implementation of the get_completion_signatures sender query.
A pipeable sender adaptor closure object is a function object that accepts one or more sender arguments and returns a sender. For a sender adaptor closure object C and an expression S such that decltype((S)) models sender, the following
-expressions are equivalent and yield a sender:
-
C(S)
-S|C
-
-
Given an additional pipeable sender adaptor closure object D, the expression C|D produces another pipeable sender adaptor closure object E:
-
E is a perfect forwarding call wrapper ([func.require]) with the following properties:
-
-
-
Its target object is an object d of type decay_t<decltype((D))> direct-non-list-initialized with D.
-
-
It has one bound argument entity, an object c of type decay_t<decltype((C))> direct-non-list-initialized with C.
-
-
Its call pattern is d(c(arg)), where arg is the argument used in a function call expression of E.
-
-
The expression C|D is well-formed if and only if the initializations of the state entities of E are all well-formed.
-
-
An object t of type T is a pipeable sender adaptor closure object if T models derived_from<sender_adaptor_closure<T>>, T has no other base
-classes of type sender_adaptor_closure<U> for any other type U, and T does not model sender.
-
-
The template parameter D for sender_adaptor_closure may be an incomplete type. Before any expression of type cvD appears as
-an operand to the | operator, D shall be complete and model derived_from<sender_adaptor_closure<D>>. The behavior of an expression involving an
-object of type cvD as an operand to the | operator is undefined if overload resolution selects a program-defined operator| function.
-
-
A pipeable sender adaptor object is a customization point object that accepts a sender as its first argument and returns a 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 adaptor accepts more than one argument, then let s be an expression such that decltype((s)) models sender,
-let args... be arguments such that adaptor(s,args...) is a well-formed expression as specified in the rest of this subclause
-([exec.adapt.objects]), and let BoundArgs be a pack that denotes decay_t<decltype((args))>.... The expression adaptor(args...) produces a pipeable sender adaptor closure object f that is a perfect forwarding call wrapper with the following properties:
-
-
-
Its target object is a copy of adaptor.
-
-
Its bound argument entities bound_args consist of objects of types BoundArgs... direct-non-list-initialized with std::forward<decltype((args))>(args)..., respectively.
-
-
Its call pattern is adaptor(r,bound_args...), where r is the argument used in a function call expression of f.
-
-
The expression adaptor(args...) is well-formed if and only if the initializations of the bound argument entities of the result, as specified above,
- are all well-formed.
-
-
11.8.6.3. execution::on[exec.on]
-
-
-
execution::on is used to adapt a sender into a sender that will start the input sender on an execution agent belonging to a specific execution context.
-
-
Let replace-scheduler(e,sch) be an expression denoting an object e' such that execution::get_scheduler(e) returns a copy of sch, and tag_invoke(tag,e',args...) is expression-equivalent to tag(e,args...) for all arguments args... and for all tag whose type satisfies forwarding-query and is not execution::get_scheduler_t.
-
-
The name execution::on denotes a customization point object. For some subexpressions sch and s, let Sch be decltype((sch)) and S be decltype((s)). If Sch does not satisfy execution::scheduler, or S does not satisfy execution::sender, execution::on is ill-formed. Otherwise, the expression execution::on(sch,s) is expression-equivalent to:
-
-
-
tag_invoke(execution::on,sch,s), if that expression is valid. If the function selected above does not return a sender which starts s on an execution agent of the associated execution context of sch when
-started, the behavior of calling execution::on(sch,s) is undefined.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, constructs a sender s1. When s1 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r such that:
-
-
-
When execution::set_value(r) is called, it calls execution::connect(s,r2), where r2 is as specified below, which results in op_state3. It calls execution::start(op_state3). If any of these throws an exception, it calls execution::set_error on out_r, passing current_exception() as the second argument.
-
-
execution::set_error(r,e) is expression-equivalent to execution::set_error(out_r,e).
-
-
execution::set_stopped(r) is expression-equivalent to execution::set_stopped(out_r).
-
-
execution::get_env(r) is expression-equivalent to execution::get_env(out_r).
-
-
-
Calls execution::schedule(sch), which results in s2. It then calls execution::connect(s2,r), resulting in op_state2.
-
-
op_state2 is wrapped by a new operation state, op_state1, that is returned to the caller.
-
-
r2 is a receiver that wraps a reference to out_r and forwards all
-receiver completion-signals to it. In addition, execution::get_env(r2) returns replace-scheduler(e,sch).
-
-
When execution::start is called on op_state1, it calls execution::start on op_state2.
-
-
The lifetime of op_state2, once constructed, lasts until either op_state3 is constructed or op_state1 is destroyed, whichever comes first. The lifetime of op_state3, once constructed, lasts until op_state1 is destroyed.
-
-
-
Given subexpressions s1 and e, where s1 is a sender returned from on or a copy of such, let S1 be decltype((s1)).
-Let E' be decltype((replace-scheduler(e,sch))).
-Then the type of tag_invoke(get_completion_signatures,s1,e) shall be:
where no-value-completions<As...> names the type completion_signatures<> for any set of types As....
-
-
-
11.8.6.4. execution::transfer[exec.transfer]
-
-
-
execution::transfer is used to adapt a sender into a sender with a different associated set_value completion scheduler. [Note: it results in a transition between different execution contexts when executed. --end note]
-
-
The name execution::transfer denotes a customization point object. For some subexpressions sch and s, let Sch be decltype((sch)) and S be decltype((s)). If Sch does not satisfy execution::scheduler, or S does not satisfy execution::sender, execution::transfer is ill-formed. Otherwise, the expression execution::transfer(s,sch) is expression-equivalent to:
-
-
-
tag_invoke(execution::transfer,get_completion_scheduler<set_value_t>(get_attrs(s)),s,sch), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, tag_invoke(execution::transfer,s,sch), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies 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 execution::schedule_from(sch,s2), where s2 is a sender which
-sends values equivalent to those sent by s, the behavior of calling execution::transfer(s,sch) is undefined.
-
-
For a sender t returned from execution::transfer(s,sch), get_attrs(t) shall
-return a queryable object q such that get_completion_scheduler<CPO>(q) returns
-a copy of sch, where CPO is either set_value_t or set_stopped_t. The get_completion_scheduler<set_error_t> query is not implemented, as the scheduler
-cannot be guaranteed in case an error is thrown while trying to schedule work on
-the given scheduler object. For all other query objects Q whose type satisfies forwarding_query, the expression Q(q,args...) shall be equivalent to Q(get_attrs(s),args...).
execution::schedule_from is used to schedule work dependent on the completion of a sender onto a scheduler’s associated execution context. [Note: schedule_from is not meant to be used in user code; it is used in the implementation of transfer. -end note]
-
-
The name execution::schedule_from denotes a customization point object. For some subexpressions sch and s, let Sch be decltype((sch)) and S be decltype((s)). If Sch does not satisfy execution::scheduler, or S does not satisfy execution::sender, execution::schedule_from is ill-formed. Otherwise, the expression execution::schedule_from(sch,s) is expression-equivalent to:
-
-
-
tag_invoke(execution::schedule_from,sch,s), if that expression is valid. If the function selected by tag_invoke does not return a sender which completes on an execution agent belonging to the associated
-execution context of sch and sends signals equivalent to those sent by s, the behavior of calling execution::schedule_from(sch,s) is undefined.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r such that when a receiver completion-signal Signal(r,args...) is called, it decay-copies args... into op_state (see below) as args'... and constructs a receiver r2 such that:
-
-
-
When execution::set_value(r2) is called, it calls Signal(out_r,std::move(args')...).
-
-
execution::set_error(r2,e) is expression-equivalent to execution::set_error(out_r,e).
-
-
execution::set_stopped(r2) is expression-equivalent to execution::set_stopped(out_r).
-
-
It then calls execution::schedule(sch), resulting in a sender s3. It then calls execution::connect(s3,r2), resulting in an operation state op_state3. It then calls execution::start(op_state3). If any of these throws an exception,
-it catches it and calls execution::set_error(out_r,current_exception()). If any of these expressions would be ill-formed, then Signal(r,args...) is ill-formed.
-
-
Calls execution::connect(s,r) resulting in an operation state op_state2. If this expression would be ill-formed, execution::connect(s2,out_r) is ill-formed.
-
-
Returns an operation state op_state that contains op_state2. When execution::start(op_state) is called, calls execution::start(op_state2). The lifetime of op_state3 ends when op_state is destroyed.
-
-
-
Given subexpressions s2 and e, where s2 is a sender returned from schedule_from or a copy of such, let S2 be decltype((s2)) and let E be decltype((e)). Then the type of tag_invoke(get_completion_signatures,s2,e) shall be:
-
make_completion_signatures<
- copy_cvref_t<S2,S>,
- E,
- make_completion_signatures<
- schedule_result_t<Sch>,
- E,
- completion_signatures<set_error_t(exception_ptr)>,
- no-value-completions>>;
-
-
where no-value-completions<As...> names the type completion_signatures<> for any set of types As....
-
-
-
For a sender t returned from execution::schedule_from(sch,s), get_attrs(t) shall
-return a queryable object q such that get_completion_scheduler<CPO>(q) returns
-a copy of sch, where CPO is either set_value_t or set_stopped_t. The get_completion_scheduler<set_error_t> query is not implemented, as the scheduler
-cannot be guaranteed in case an error is thrown while trying to schedule work on
-the given scheduler object. For all other query objects Q whose type satisfies forwarding_query, the expression Q(q,args...) shall be equivalent to Q(get_attrs(s),args...).
-
-
11.8.6.6. execution::then[exec.then]
-
-
-
execution::then is used to attach an invocable as a continuation for the successful completion of the input sender.
-
-
The name execution::then denotes a customization point object. For some
-subexpressions s and f, let S be decltype((s)), let F be the
-decayed type of f, and let f' be an xvalue refering to an object
-decay-copied from f. If S does not satisfy execution::sender, or F does not model movable-value, execution::then is
-ill-formed. Otherwise, the expression execution::then(s,f) is
-expression-equivalent to:
-
-
-
tag_invoke(execution::then,get_completion_scheduler<set_value_t>(get_attrs(s)),s,f), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, tag_invoke(execution::then,s,f), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r such that:
-
-
-
When execution::set_value(r,args...) is called, let v be the
-expression invoke(f',args...). If decltype(v) is void,
-calls execution::set_value(out_r); otherwise, it calls execution::set_value(out_r,v). If any of these throw an
-exception, it catches it and calls execution::set_error(out_r,current_exception()). If any of these expressions would be
-ill-formed, the expression execution::set_value(r,args...) is
-ill-formed.
-
-
execution::set_error(r,e) is expression-equivalent to execution::set_error(out_r,e).
-
-
execution::set_stopped(r) is expression-equivalent to execution::set_stopped(out_r).
-
-
-
Returns an expression equivalent to execution::connect(s,r).
-
-
Let compl-sig-t<Tag,Args...> name the type Tag() if Args... is a template paramter pack containing the
-single type void; otherwise, Tag(Args...). Given
-subexpressions s2 and e where s2 is a sender returned from then or a copy of such, let S2 be decltype((s2)) and let E be decltype((e)). The type of tag_invoke(get_completion_signatures,s2,e) shall be equivalent
-to:
and set-error-signature is an alias for completion_signatures<set_error_t(exception_ptr)> if any of the types
-in the type-list named by value_types_of_t<copy_cvref_t<S2,S>,E,potentially-throwing,type-list> are true_type; otherwise, completion_signatures<>, where potentially-throwing is the template alias:
If the function selected above does not return a sender that invokes f with the result of the set_value signal of s, passing the return value as the value to any connected receivers, and propagates the other completion-signals sent by s, the behavior of calling execution::then(s,f) is undefined.
-
-
11.8.6.7. execution::upon_error[exec.upon_error]
-
-
-
execution::upon_error is used to attach an invocable as a continuation for the unsuccessful completion of the input sender.
-
-
The name execution::upon_error denotes a customization point object. For
-some subexpressions s and f, let S be decltype((s)), let F be the
-decayed type of f, and let f' be an xvalue refering to an object
-decay-copied from f. If S does not satisfy execution::sender, or F does not model movable-value, execution::upon_error is
-ill-formed. Otherwise, the expression execution::upon_error(s,f) is
-expression-equivalent to:
-
-
-
tag_invoke(execution::upon_error,get_completion_scheduler<set_error_t>(get_attrs(s)),s,f), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, tag_invoke(execution::upon_error,s,f), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r such that:
-
-
-
execution::set_value(r,args...) is expression-equivalent to execution::set_value(out_r,args...).
-
-
When execution::set_error(r,e) is called, let v be the
-expression invoke(f',e). If decltype(v) is void, calls execution::set_value(out_r); otherwise, it calls execution::set_value(out_r,v). If any of these throw an
-exception, it catches it and calls execution::set_error(out_r,current_exception()). If any of these expressions would be
-ill-formed, the expression execution::set_error(r,e) is
-ill-formed.
-
-
execution::set_stopped(r) is expression-equivalent to execution::set_stopped(out_r).
-
-
-
Returns an expression equivalent to execution::connect(s,r).
-
-
Let compl-sig-t<Tag,Args...> name the type Tag() if Args... is a template paramter pack containing the
-single type void; otherwise, Tag(Args...). Given
-subexpressions s2 and e where s2 is a sender returned from upon_error or a copy of such, let S2 be decltype((s2)) and let E be decltype((e)). The type of tag_invoke(get_completion_signatures,s2,e) shall be equivalent
-to:
and set-error-signature is an alias for completion_signatures<set_error_t(exception_ptr)> if any of the types
-in the type-list named by error_types_of_t<copy_cvref_t<S2,S>,E,potentially-throwing> are true_type; otherwise, completion_signatures<>, where potentially-throwing is the template alias:
If the function selected above does not return a sender which invokes f with the result of the set_error signal of s, passing the return value as the value to any connected receivers, and propagates the other completion-signals sent by s, the behavior of calling execution::upon_error(s,f) is undefined.
execution::upon_stopped is used to attach an invocable as a continuation for the completion of the input sender using the "stopped" channel.
-
-
The name execution::upon_stopped denotes a customization point object. For
-some subexpressions s and f, let S be decltype((s)), let F be the
-decayed type of f, and let f' be an xvalue refering to an object
-decay-copied from f. If S does not satisfy execution::sender, or F does not model both movable-value and invocable, execution::upon_stopped is ill-formed. Otherwise, the expression execution::upon_stopped(s,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::upon_stopped,get_completion_scheduler<set_stopped_t>(get_attrs(s)),s,f), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, tag_invoke(execution::upon_stopped,s,f), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r such that:
-
-
-
execution::set_value(r,args...) is expression-equivalent to execution::set_value(out_r,args...).
-
-
execution::set_error(r,e) is expression-equivalent to execution::set_error(out_r,e).
-
-
When execution::set_stopped(r) is called, let v be the
-expression invoke(f'). If v has type void, calls execution::set_value(out_r); otherwise, calls execution::set_value(out_r,v). If any of these throw an
-exception, it catches it and calls execution::set_error(out_r,current_exception()). If any of these expressions would be
-ill-formed, the expression execution::set_stopped(r) is
-ill-formed.
-
-
-
Returns an expression equivalent to execution::connect(s,r).
-
-
Let compl-sig-t<Tag,Args...> name the type Tag() if Args... is a template paramter pack containing the
-single type void; otherwise, Tag(Args...). Given
-subexpressions s2 and e where s2 is a sender returned from upon_stopped or a copy of such, let S2 be decltype((s2)) and let E be decltype((e)). The type of tag_invoke(get_completion_signatures,s2,e) shall be equivalent
-to:
where set-stopped-completions names the type completion_signatures<compl-sig-t<set_value_t,invoke_result_t<F>>, and set-error-signature names the type completion_signatures<set_error_t(exception_ptr)> if is_nothrow_invocable_v<F> is true, or completion_signatures<> otherwise.
-
-
-
If the function selected above does not return a sender which invokes f when s completes by calling set_stopped, passing the return value as the value to any connected receivers, and propagates the other completion-signals sent by s, the behavior of calling execution::upon_stopped(s,f) is undefined.
execution::let_value is used to insert continuations creating more work dependent on the results of their input senders into a sender chain. execution::let_error is used to insert continuations creating more work dependent on the error of its input senders into a sender chain. execution::let_stopped is used to insert continuations creating more work dependent on the stopped signal of its input senders into a sender chain.
-
-
The names execution::let_value, execution::let_error, and execution::let_stopped denote a customization point object.
-Let the expression let-cpo be one of execution::let_value, execution::let_error, or execution::let_stopped.
-For some subexpressions s and f, let S be decltype((s)), let F be the decayed type of f, and let f' be an xvalue that refers to an object decay-copied from f.
-If S does not satisfy execution::sender, the expression let-cpo(s,f) is ill-formed.
-If F does not satisfy invocable, the expression execution::let_stopped(s,f) is ill-formed.
-Otherwise, the expression let-cpo(s,f) is expression-equivalent to:
-
-
-
tag_invoke(let-cpo,get_completion_scheduler<set_value_t>(get_attrs(s)),s,f), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, tag_invoke(let-cpo,s,f), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, given a receiver out_r and an lvalue out_r' refering to an object decay-copied from out_r.
-
-
-
For execution::let_value, let set-cpo be execution::set_value.
-For execution::let_error, let set-cpo be execution::set_error.
-For execution::let_stopped, let set-cpo be execution::set_stopped.
-Let signal be one of execution::set_value, execution::set_error, or execution::set_stopped.
-
-
Let r be an rvalue of a receiver type R such that:
-
-
-
When set-cpo(r,args...) is called, the receiver r decay-copies args... into op_state2 as args'..., then calls invoke(f',args'...), resulting in a sender s3.
-It then calls execution::connect(s3,std::move(out_r')), resulting in an operation state op_state3. op_state3 is saved as a part of op_state2.
-It then calls execution::start(op_state3).
-If any of these throws an exception, it catches it and calls execution::set_error(std::move(out_r'),current_exception()).
-If any of these expressions would be ill-formed, set-cpo(r,args...) is ill-formed.
-
-
signal(r,args...) is expression-equivalent to signal(std::move(out_r'),args...), when signal is different from set-cpo.
-
-
-
let-cpo(s,f) returns a sender s2 such that:
-
-
-
If the expression execution::connect(s,r) is ill-formed, execution::connect(s2,out_r) is ill-formed.
-
-
Otherwise, let op_state2 be the result of execution::connect(s,r). execution::connect(s2,out_r) returns an operation state op_state that stores op_state2. execution::start(op_state) is expression-equivalent to execution::start(op_state2).
-
-
-
Given subexpressions s2 and e, where s2 is a sender returned
-from let-cpo(s,f) or a copy of such, let S2 be decltype((s2)), let E be decltype((e)), and let S' be copy_cvref_t<S2,S>. Then the type of tag_invoke(get_completion_signatures,s2,e) is specified as
-follows:
-
-
-
If sender_in<S',E> is false, the expression tag_invoke(get_completion_signatures,s2,e) is ill-formed.
-
-
Otherwise, let Sigs... be the set of template arguments of the completion_signatures specialization named by completion_signatures_of_t<S',E>,
-let Sigs2... be the set of function types in Sigs... whose return type
-is set-cpo, and let Rest... be the set of function types
-in Sigs... but not Sigs2....
-
-
For each Sig2i in Sigs2..., let Vsi... be the set of function
-arguments in Sig2i and let S3i be invoke_result_t<F,decay_t<Vsi>&...>. If S3i is ill-formed, or if sender_in<S3i,E> is not satisfied,
-then the expression tag_invoke(get_completion_signatures,s2,e) is ill-formed.
-
-
Otherwise, let Sigs3i... be the
-set of template arguments of the completion_signatures specialization named by completion_signatures_of_t<S3i,E>. Then the type of tag_invoke(get_completion_signatures,s2,e) shall be equivalent to completion_signatures<Sigs30...,Sigs31...,...Sigs3n-1...,Rest...,set_error_t(exception_ptr)>, where n is sizeof...(Sigs2).
-
-
-
-
If let-cpo(s,f) does not return a sender that invokes f when set-cpo is called, and makes its completion dependent on the completion of a sender returned by f, and propagates the other completion-signals sent by s, the behavior of calling let-cpo(s,f) is undefined.
-
-
11.8.6.10. execution::bulk[exec.bulk]
-
-
-
execution::bulk is used to run a task repeatedly for every index in an index space.
-
-
The name execution::bulk denotes a customization point object. For some
-subexpressions s, shape, and f, let S be decltype((s)), Shape be decltype((shape)), and F be decltype((f)). If S does not satisfy execution::sender or Shape does not satisfy integral, execution::bulk is ill-formed. Otherwise, the expression execution::bulk(s,shape,f) is expression-equivalent to:
-
-
-
tag_invoke(execution::bulk,get_completion_scheduler<set_value_t>(get_attrs(s)),s,shape,f), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, tag_invoke(execution::bulk,s,shape,f), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r:
-
-
-
When execution::set_value(r,args...) is called, calls f(i,args...) for each i of type Shape from 0 to shape, then calls execution::set_value(out_r,args...). If any of these throws an exception, it catches it and calls execution::set_error(out_r,current_exception()).
-
-
When execution::set_error(r,e) is called, calls execution::set_error(out_r,e).
-
-
When execution::set_stopped(r) is called, calls execution::set_stopped(out_r,e).
-
-
-
Calls execution::connect(s,r), which results in an operation state op_state2.
-
-
Returns an operation state op_state that contains op_state2. When execution::start(op_state) is called, calls execution::start(op_state2).
-
-
Given subexpressions s2 and e where s2 is a sender returned
-from bulk or a copy of such, let S2 be decltype((s2)), let E be decltype((e)), let S' be copy_cvref_t<S2,S>, let Shape be decltype((shape)) and let nothrow-callable be the alias template:
If any of the types in the type-list named by value_types_of_t<S',E,nothrow-callable,type-list> are false_type, then the type of tag_invoke(get_completion_signatures,s2,e) shall be
-equivalent to:
Otherwise, the type of tag_invoke(get_completion_signatures,s2,e) shall be equivalent to completion_signatures_of_t<S',E>.
-
-
-
-
If the function selected above does not return a sender which invokes f(i,args...) for each i of type Shape from 0 to shape when
-the input sender sends values args..., or does not propagate the
-values of the signals sent by the input sender to a connected receiver,
-the behavior of calling execution::bulk(s,shape,f) is undefined.
-
-
-
11.8.6.11. execution::split[exec.split]
-
-
-
execution::split is used to adapt an arbitrary sender into a sender that can be connected multiple times.
-
-
Let split-env be the type of an execution environment such that, given an instance e, the expression get_stop_token(e) is well formed and has type stop_token.
-
-
The name execution::split denotes a customization point object. For some
-subexpression s, let S be decltype((s)). If execution::sender_in<S,split-env> or constructible_from<decay_t<attrs_of_t<S>>,attrs_of_t<S>> is false, execution::split is ill-formed. Otherwise, the expression execution::split(s) is expression-equivalent to:
-
-
-
tag_invoke(execution::split,get_completion_scheduler<set_value_t>(get_attrs(s)),s),
-if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, tag_invoke(execution::split,s), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, constructs a sender s2, which:
-
-
-
Creates an object sh_state that contains a stop_source, a list of
-pointers to operation states awaiting the completion of s, and that
-also reserves space for storing:
-
-
-
the operation state that results from connecting s with r described below, and
-
-
the sets of values and errors with which s may complete, with
-the addition of exception_ptr.
-
-
the result of decay-copying get_attrs(s).
-
-
-
Constructs a receiver r such that:
-
-
-
When execution::set_value(r,args...) is called, decay-copies
-the expressions args... into sh_state. It then notifies all
-the operation states in sh_state's list of operation states
-that the results are ready. If any exceptions are thrown, the
-exception is caught and execution::set_error(r,current_exception()) is called instead.
-
-
When execution::set_error(r,e) is called, decay-copies e into sh_state. It then notifies the operation states in sh_state's list of operation states that the results are ready.
-
-
When execution::set_stopped(r) is called, it then notifies the
-operation states in sh_state's list of operation states that
-the results are ready.
-
-
get_env(r) is an expression e of type split-env such that execution::get_stop_token(e) is well-formed
-and returns the results of calling get_token() on sh_state's
-stop source.
-
-
-
Calls execution::get_attrs(s) and decay-copies the result into sh_state.
-
-
Calls execution::connect(s,r), resulting in an operation state op_state2. op_state2 is saved in sh_state.
-
-
When s2 is connected with a receiver out_r of type OutR, it
-returns an operation state object op_state that contains:
-
-
-
An object out_r' of type OutR decay-copied from out_r,
-
-
A reference to sh_state,
-
-
A stop callback of type optional<stop_token_of_t<env_of_t<OutR>>::callback_type<stop-callback-fn>>,
-where stop-callback-fn is an implementation
-defined class type equivalent to the following:
If r's receiver contract has already been satisfied, then let Signal be whichever receiver completion-signal
-was used to complete r’s receiver contract ([exec.recv]). Calls Signal(out_r',args2...), where args2... is a
-pack of const lvalues referencing the subobjects of sh_state that
-have been saved by the original call to Signal(r,args...) and returns.
-
-
Otherwise, it emplace constructs the stop callback optional with
-the arguments execution::get_stop_token(get_env(out_r')) and stop-callback-fn{stop-src}, where stop-src refers to the stop source of sh_state.
-
-
Otherwise, it adds a pointer to op_state to the list of
-operation states in sh_state. If op_state is the first such
-state added to the list:
-
-
-
If stop-src.stop_requested() is true,
- all of the operation states in sh_state's list of operation
- states are notified as if execution::set_stopped(r) had
- been called.
-
-
Otherwise, execution::start(op_state2) is called.
-
-
-
-
When r completes it will notify op_state that the result are
-ready. Let Signal be whichever receiver
-completion-signal was used to complete r's receiver contract
-([exec.recv]). op_state's stop callback optional is reset. Then Signal(std::move(out_r'),args2...) is called,
-where args2... is a pack of const lvalues referencing the subobjects of sh_state that have been saved by the original call to Signal(r,args...).
-
-
Ownership of sh_state is shared by s2 and by every op_state that results from connecting s2 to a receiver.
-
-
-
Given subexpressions s2 where s2 is a sender returned from split or a copy of such, get_attrs(s2) shall return an lvalue reference to the
-object in sh_state that was initialized with the result of get_attrs(s).
-
-
Given subexpressions s2 and e where s2 is a sender returned
-from split or a copy of such, let S2 be decltype((s2)) and let E be decltype((e)). The type of tag_invoke(get_completion_signatures,s2,e) shall be equivalent
-to:
Let s be a sender expression, r be an instance of the receiver type
-described above, s2 be a sender returned
-from split(s) or a copy of such, r2 is the receiver
-to which s2 is connected, and args is the pack of subexpressions
-passed to r's completion-signal operation CSO when s completes. s2 shall satisfy r2's receiver contract
-([exec.recv]) by invoking CSO(r2,args2...) where args2 is a pack of const lvalue references to objects decay-copied from args, or by calling set_error(r2,e2) for some subexpression e2. The objects passed to r2's completion-signal operation shall
-be valid until after the completion of the invocation of r2's completion-
-signal operation.
-
-
-
11.8.6.12. execution::when_all[exec.when_all]
-
-
-
execution::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_with_variant 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.
-
-
The name execution::when_all denotes a customization point object. For some subexpressions si..., let Si... be decltype((si)).... The expression execution::when_all(si...) is ill-formed if any of the following is true:
-
-
-
If the number of subexpressions si... is 0, or
-
-
If any type Si does not satisfy execution::sender.
-
-
Otherwise, the expression execution::when_all(si...) is expression-equivalent to:
-
-
-
tag_invoke(execution::when_all,si...), if
-that expression is valid. If the function selected by tag_invoke does
-not return a sender that sends a concatenation of values sent by si... when they all complete with set_value, the behavior of calling execution::when_all(si...) is undefined.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, constructs a sender w of type W. When w is connected
-with some receiver out_r of type OutR, it returns an operation state op_state specified as below:
-
-
-
For each sender si, constructs a receiver ri such that:
-
-
-
If execution::set_value(ri,ti...) is called for every ri, op_state's associated stop callback optional is reset and execution::set_value(out_r,t0...,t1...,...,tn-1...) is called, where n the number of subexpressions in si....
-
-
Otherwise, execution::set_error or execution::set_stopped was called for at least one receiver ri. If the first such to complete did so with the call execution::set_error(ri,e), request_stop is called on op_state's associated stop source. When all child operations have completed, op_state's associated stop callback optional is reset and execution::set_error(out_r,e) is called.
-
-
Otherwise, request_stop is called on op_state's associated stop source. When all child operations have completed, op_state's associated stop callback optional is reset and execution::set_stopped(out_r) is called.
-
-
For each receiver ri, get_env(ri) is an expression e such that execution::get_stop_token(e) is well-formed and returns the results of calling get_token() on op_state's associated stop source, and for which tag_invoke(tag,e,args...) is expression-equivalent to tag(get_env(out_r),args...) for all arguments args... and all tag whose type satisfies forwarding-query and is not get_stop_token_t.
-
-
-
For each sender si, calls execution::connect(si,ri), resulting in operation states child_opi.
-
-
Returns an operation state op_state that contains:
-
-
-
Each operation state child_opi,
-
-
A stop source of type in_place_stop_source,
-
-
A stop callback of type optional<stop_token_of_t<env_of_t<OutR>>::callback_type<stop-callback-fn>>, where stop-callback-fn is an implementation defined class type equivalent to the following:
Emplace constructs the stop callback optional with the arguments execution::get_stop_token(get_env(out_r)) and stop-callback-fn{stop-src}, where stop-src refers to the stop source of op_state.
-
-
Then, it checks to see if stop-src.stop_requested() is true. If so, it calls execution::set_stopped(out_r).
-
-
Otherwise, calls execution::start(child_opi) for each child_opi.
-
-
-
Given subexpressions s2 and e where s2 is a sender returned
-from when_all or a copy of such, let S2 be decltype((s2)), let E be decltype((e)), and let Ss... be the decayed types of the
-arguments to the when_all expression that created s2. Let WE be a type such that stop_token_of_t<WE> is in_place_stop_token and tag_invoke_result_t<Tag,WE,As...> names the type, if any, of call-result-t<Tag,E,As...> for all types As... and all types Tag besides get_stop_token_t. The type of tag_invoke(get_completion_signatures,s2,e) shall be as follows:
-
-
-
For each type Si in Ss..., let S'i name the type copy_cvref_t<S2,Si>. If for
-any type S'i, the type completion_signatures_of_t<S'i,WE> is ill-formed, the expression of tag_invoke(get_completion_signatures,s2,e) is
-ill-formed.
-
-
Otherwise, for each type S'i, let Sigsi... be the set of template
-arguments in the instantiation of completion_signatures named
-by completion_signatures_of_t<S'i,WE>, and let Ci be the
-count of function types in Sigsi... for which the return
-type is set_value_t. If any Ci is two or greater, then the
-expression tag_invoke(get_completion_signatures,s2,e) is
-ill-formed.
-
-
Otherwise, let Sigs2i... be the set of
-function types in Sigsi... whose
-return types are notset_value_t, and let Ws... be
-the unique set of types in [Sigs20...,Sigs21...,...Sigs2n-1...,set_stopped_t()], where n is sizeof...(Ss). If any Ci is 0, then the type of tag_invoke(get_completion_signatures,s2,e) shall be completion_signatures<Ws...>.
-
-
Otherwise, let Vi... be the
-function argument types of the single type in Sigsi... for which the return
-type is set_value_t. Then the type of tag_invoke(get_completion_signatures,s2,e) shall be completion_signatures<Ws...,set_value_t(decay_t<V0>&&...,decay_t<V1>&&...,...decay_t<Vn-1>&&...)>.
-
-
-
-
-
The name execution::when_all_with_variant denotes a customization point object. For some subexpressions s..., let S be decltype((s)). If any type Si in S... does not satisfy execution::sender, execution::when_all_with_variant is ill-formed. Otherwise, the expression execution::when_all_with_variant(s...) is expression-equivalent to:
-
-
-
tag_invoke(execution::when_all_with_variant,s...), if that expression
-is valid. If the function selected by tag_invoke does not return a
-sender that, when connected with a receiver of type R, sends the types into-variant-type<S,env_of_t<R>>... when they
-all complete with set_value, the behavior of calling execution::when_all(si...) is undefined.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
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
-that only send a single set of values each, while also making sure that they
-complete on the specified scheduler. execution::transfer_when_all_with_variant 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]
-
-
The name execution::transfer_when_all denotes a customization point object. For some subexpressions sch and s..., let Sch be decltype(sch) and S be decltype((s)). If Sch does not satisfy scheduler, or any type Si in S... does not satisfy execution::sender, execution::transfer_when_all is ill-formed. Otherwise, the expression execution::transfer_when_all(sch,s...) is expression-equivalent to:
-
-
-
tag_invoke(execution::transfer_when_all,sch,s...), if that expression
-is valid. If the function selected by tag_invoke does not return a
-sender that sends a concatenation of values sent by s... when they all
-complete with set_value, 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 of sch, the behavior of
-calling execution::transfer_when_all(sch,s...) is undefined.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
The name execution::transfer_when_all_with_variant denotes a customization
-point object. For some subexpressions sch and s..., let Sch be decltype((sch)) and let S be decltype((s)). If any type Si in S... does not satisfy execution::sender, execution::transfer_when_all_with_variant is
-ill-formed. Otherwise, the expression execution::transfer_when_all_with_variant(sch,s...) is expression-equivalent
-to:
-
-
-
tag_invoke(execution::transfer_when_all_with_variant,s...), if that
-expression is valid. If the function selected by tag_invoke does not
-return a sender that, when connected with a receiver of type R, sends
-the types into-variant-type<S,env_of_t<R>>... when they all complete with set_value, the behavior
-of calling execution::transfer_when_all_with_variant(sch,s...) is undefined.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
For a sender t returned from execution::transfer_when_all(sch,s...), get_attrs(t) shall
-return a queryable object q such that get_completion_scheduler<CPO>(q) returns
-a copy of sch, where CPO is either set_value_t or set_stopped_t. The get_completion_scheduler<set_error_t> query is not implemented, as the scheduler
-cannot be guaranteed in case an error is thrown while trying to schedule work on
-the given scheduler object.
execution::into_variant can be used to turn a sender which sends multiple sets of values into a sender which sends a variant of all of those sets of values.
-
-
The template into-variant-type is used to compute the type sent by a sender returned from execution::into_variant.
execution::into_variant is a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::sender, execution::into_variant(s) is ill-formed. Otherwise, execution::into_variant(s) returns
-a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r:
-
-
-
If execution::set_value(r,ts...) is called, calls execution::set_value(out_r,into-variant-type<S,env_of_t<decltype((r))>>(decayed-tuple<decltype(ts)...>(ts...))). If this expression throws an exception, calls execution::set_error(out_r,current_exception()).
-
-
execution::set_error(r,e) is expression-equivalent to execution::set_error(out_r,e).
-
-
execution::set_stopped(r) is expression-equivalent to execution::set_stopped(out_r).
-
-
-
Calls execution::connect(s,r), resulting in an operation state op_state2.
-
-
Returns an operation state op_state that contains op_state2. When execution::start(op_state) is called, calls execution::start(op_state2).
-
-
Given subexpressions s2 and e, where s2 is a sender returned from into_variant or a copy of such, let S2 be decltype((s2)) and E be decltype((e)).
-Let into-variant-set-value be the class template:
Let INTO-VARIANT-ERROR-SIGNATURES(S,E) be completion_signatures<set_error_t(exception_ptr)> if any of the types in the type-list named by value_types_of_t<S,E,into-variant-is-nothrow<S,E>::templateapply,type-list> are false_type; otherwise, completion_signatures<>.
-
The type of tag_invoke(get_completion_signatures_t{},s2,e) shall be equivalent to:
execution::stopped_as_optional is used to handle a stopped 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 stopped, reporting cancellation by completing with an empty optional.
-
-
The name execution::stopped_as_optional denotes a customization point object. For some subexpression s, let S be decltype((s)). Let get-env-sender be an expression such that, when it is connected with a receiver r, start on the resulting operation state completes immediately by calling execution::set_value(r,get_env(r)). The expression execution::stopped_as_optional(s) is expression-equivalent to:
execution::stopped_as_error is used to handle a stopped signal by mapping it into the error channel as a custom exception type. The result is a sender that never completes with stopped, reporting cancellation by completing with an error.
-
-
The name execution::stopped_as_error denotes a customization point object. For some subexpressions s and e, let S be decltype((s)) and let E be decltype((e)). If the type S does not satisfy sender or if the type E doesn’t satisfy movable-value, execution::stopped_as_error(s,e) is ill-formed. Otherwise, the expression execution::stopped_as_error(s,e) is expression-equivalent to:
execution::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.
-
-
Let ensure-started-env be the type of an execution
-environment such that, given an instance e, the expression get_stop_token(e) is well formed and has type stop_token.
-
-
The name execution::ensure_started denotes a customization point object.
-For some subexpression s, let S be decltype((s)). If execution::sender_in<S,ensure-started-env> or constructible_from<decay_t<attrs_of_t<S>>,attrs_of_t<S>> is false, execution::ensure_started(s) is ill-formed. Otherwise, the
-expression execution::ensure_started(s) is expression-equivalent to:
-
-
-
tag_invoke(execution::ensure_started,get_completion_scheduler<set_value_t>(get_attrs(s)),s), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, tag_invoke(execution::ensure_started,s), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies execution::sender.
-
-
-
Otherwise, constructs a sender s2, which:
-
-
-
Creates an object sh_state that contains a stop_source, an
-initially null pointer to an operation state awaitaing completion,
-and that also reserves space for storing:
-
-
-
the operation state that results from connecting s with r described below, and
-
-
the sets of values and errors with which s may complete, with
-the addition of exception_ptr.
-
-
the result of decay-copying get_attrs(s).
-
-
s2 shares ownership of sh_state with r described below.
-
-
Constructs a receiver r such that:
-
-
-
When execution::set_value(r,args...) is called, decay-copies
-the expressions args... into sh_state. It then checks sh_state to see if there is an operation state awaiting
-completion; if so, it notifies the operation state that the
-results are ready. If any exceptions are thrown, the exception
-is caught and execution::set_error(r,current_exception()) is
-called instead.
-
-
When execution::set_error(r,e) is called, decay-copies e into sh_state. If there is an operation state awaiting completion,
-it then notifies the operation state that the results are ready.
-
-
When execution::set_stopped(r) is called, it then notifies any
-awaiting operation state that the results are ready.
-
-
get_env(r) is an expression e of type ensure-started-env such that execution::get_stop_token(e) is well-formed
-and returns the results of calling get_token() on sh_state's
-stop source.
-
-
r shares ownership of sh_state with s2. After r's
-receiver contract has been completed, it releases its ownership
-of sh_state.
-
-
-
Calls execution::get_attrs(s) and decay-copies the result into sh_state.
-
-
Calls execution::connect(s,r), resulting in an operation state op_state2. op_state2 is saved in sh_state. It then calls execution::start(op_state2).
-
-
When s2 is connected with a receiver out_r of type OutR, it
-returns an operation state object op_state that contains:
-
-
-
An object out_r' of type OutR decay-copied from out_r,
-
-
A reference to sh_state,
-
-
A stop callback of type optional<stop_token_of_t<env_of_t<OutR>>::callback_type<stop-callback-fn>>,
-where stop-callback-fn is an implementation
-defined class type equivalent to the following:
s2 transfers its ownership of sh_state to op_state.
-
-
When execution::start(op_state) is called:
-
-
-
If r's receiver contract has already been satisfied, then let Signal be whichever receiver completion-signal
-was used to complete r's receiver contract ([exec.recv]). Calls Signal(out_r',args2...), where args2... is a
-pack of xvalues referencing the subobjects of sh_state that have
-been saved by the original call to Signal(r,args...) and returns.
-
-
Otherwise, it emplace constructs the stop callback optional with
-the arguments execution::get_stop_token(get_env(out_r')) and stop-callback-fn{stop-src}, where stop-src refers to the stop source of sh_state.
-
-
Then, it checks to see if stop-src.stop_requested() is true. If so, it
-calls execution::set_stopped(out_r').
-
-
Otherwise, it sets sh_state operation state pointer to the
-address of op_state, registering itself as awaiting the result
-of the completion of r.
-
-
-
When r completes it will notify op_state that the result are
-ready. Let Signal be whichever receiver
-completion-signal was used to complete r's receiver contract
-([exec.recv]). op_state's stop callback optional is reset. Then Signal(std::move(out_r'),args2...) is called,
-where args2... is a pack of xvalues referencing the subobjects of sh_state that have been saved by the original call to Signal(r,args...).
-
-
[Note: If sender s2 is destroyed without being connected to a
-receiver, or if it is connected but the operation state is destroyed
-without having been started, then when r's receiver contract
-completes and it releases its shared ownership of sh_state, sh_state will be destroyed and the results of the operation are
-discarded. -- end note]
-
-
-
Given a subexpression s, let s2 be the result of ensure_started(s).
-The result of get_attrs(s2) shall return an lvalue reference to the
-object in sh_state that was initialized with the result of get_attrs(s).
-
-
Given subexpressions s2 and e where s2 is a sender returned
-from ensure_started or a copy of such, let S2 be decltype((s2)) and let E be decltype((e)). The type of tag_invoke(get_completion_signatures,s2,e) shall be equivalent
-to:
Let s be a sender expression, r be an instance of the receiver type
- described above, s2 be a sender returned
- from ensure_started(s) or a copy of such, r2 is the receiver
- to which s2 is connected, and args is the pack of subexpressions
- passed to r's completion-signal operation CSO when s completes. s2 shall satisfy r2's receiver contract
- ([exec.recv]) by invoking CSO(r2,args2...) where args2 is a pack of xvalue references to objects decay-copied from args, or by calling set_error(r2,e2) for some subexpression e2. The objects passed to r2's completion-signal operation shall
- be valid until after the completion of the invocation of r2's completion-
- signal operation.
execution::start_detached is used to eagerly start a sender without the caller needing to manage the lifetimes of any objects.
-
-
The name execution::start_detached denotes a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::sender, execution::start_detached is ill-formed. Otherwise, the expression execution::start_detached(s) is expression-equivalent to:
-
-
-
tag_invoke(execution::start_detached,execution::get_completion_scheduler<execution::set_value_t>(get_attrs(s)),s), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above is void.
-
-
-
Otherwise, tag_invoke(execution::start_detached,s), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above is void.
-
-
-
Otherwise:
-
-
-
Let R be the type of a receiver, let r be an rvalue of type R, and let cr be a
-lvalue reference to constR such that:
-
-
-
The expression set_value(r) is not potentially throwing and has no effect,
-
-
For any subexpression e, the expression set_error(r,e) is expression-equivalent
-to terminate(),
-
-
The expression set_stopped(r) is not potentially throwing and has no effect, and
-
-
The expression get_env(cr) is expression-equivalent to empty-env{}.
-
-
-
Calls execution::connect(s,r), resulting in an operation state op_state, then calls execution::start(op_state). The lifetime of op_state lasts until one of the receiver completion-signals of r is called.
-
-
-
If the function selected above does not eagerly start the sender s after
-connecting it with a receiver which ignores the set_value and set_stopped signals and calls terminate() on the set_error signal,
-the behavior of calling execution::start_detached(s) is undefined.
-
-
11.8.7.2. this_thread::sync_wait[exec.sync_wait]
-
-
-
this_thread::sync_wait and this_thread::sync_wait_with_variant 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.
-
-
For any receiver r created by an implementation of sync_wait and sync_wait_with_variant, the expressions get_scheduler(get_env(r)) and get_delegatee_scheduler(get_env(r)) shall be well-formed. For a receiver
-created by the default implementation of this_thread::sync_wait, these
-expressions shall return a scheduler to the same thread-safe,
-first-in-first-out queue of work such that tasks scheduled to the queue
-execute on the thread of the caller of sync_wait. [Note: The
-scheduler for an instance of execution::run_loop that is a local variable
-within sync_wait is one valid implementation. -- end note]
-
-
The templates sync-wait-type and sync-wait-with-variant-type are used to determine the
-return types of this_thread::sync_wait and this_thread::sync_wait_with_variant. Let sync-wait-env be the type of the expression get_env(r) where r is an instance of the
-receiver created by the default implementation of sync_wait.
The name this_thread::sync_wait denotes a customization point object. For
-some subexpression s, let S be decltype((s)). If execution::sender_in<S,sync-wait-env> is false,
-or the number of the arguments completion_signatures_of_t<S,sync-wait-env>::value_types passed into the Variant template
-parameter is not 1, this_thread::sync_wait(s) is ill-formed. Otherwise, this_thread::sync_wait(s) is expression-equivalent to:
-
-
-
tag_invoke(this_thread::sync_wait,execution::get_completion_scheduler<execution::set_value_t>(get_attrs(s)),s), if this expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above is sync-wait-type<S,sync-wait-env>.
-
-
-
Otherwise, tag_invoke(this_thread::sync_wait,s), if this expression is valid and its type is.
-
-
-
Mandates: The type of the tag_invoke expression above is sync-wait-type<S,sync-wait-env>.
-
-
-
Otherwise:
-
-
-
Constructs a receiver r.
-
-
Calls execution::connect(s,r), resulting in an operation state op_state, then calls execution::start(op_state).
-
-
Blocks the current thread until a receiver completion-signal of r is called. When it is:
-
-
-
If execution::set_value(r,ts...) has been called, returns sync-wait-type<S,sync-wait-env>{decayed-tuple<decltype(ts)...>{ts...}}. If that expression exits exceptionally, the exception is propagated to the caller of sync_wait.
-
-
If execution::set_error(r,e) has been called, let E be the decayed type of e. If E is exception_ptr, calls std::rethrow_exception(e). Otherwise, if the E is error_code, throws system_error(e). Otherwise, throws e.
-
-
If execution::set_stopped(r) has been called, returns sync-wait-type<S,sync-wait-env>{}.
-
-
-
-
-
The name this_thread::sync_wait_with_variant denotes a customization point
-object. For some subexpression s, let S be the type of execution::into_variant(s). If execution::sender_in<S,sync-wait-env> is false, this_thread::sync_wait_with_variant(s) is ill-formed. Otherwise, this_thread::sync_wait_with_variant(s) is expression-equivalent to:
-
-
-
tag_invoke(this_thread::sync_wait_with_variant,execution::get_completion_scheduler<execution::set_value_t>(get_attrs(s)),s), if this expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above is sync-wait-with-variant-type<S,sync-wait-env>.
-
-
-
Otherwise, tag_invoke(this_thread::sync_wait_with_variant,s), if this expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above is sync-wait-with-variant-type<S,sync-wait-env>.
execution::execute is used to create fire-and-forget tasks on a specified scheduler.
-
-
The name execution::execute denotes a customization point object. For some subexpressions sch and f, let Sch be decltype((sch)) and F be decltype((f)). If Sch does not satisfy execution::scheduler or F does not satisfy invocable, execution::execute is ill-formed. Otherwise, execution::execute is expression-equivalent to:
-
-
-
tag_invoke(execution::execute,sch,f), if that expression is valid. If
-the function selected by tag_invoke does not invoke the function f (or an object decay-copied from f) on an execution agent belonging to
-the associated execution context of sch, or if it does not call std::terminate if an error occurs after control is returned to the
-caller, the behavior of calling execution::execute is undefined.
-
-
-
Mandates: The type of the tag_invoke expression above is void.
template<
- class-typeDerived,
- receiverBase=unspecified>// arguments are not associated entities ([lib.tmpl-heads])
- classreceiver_adaptor;
-
-
-
-
receiver_adaptor is used to simplify the implementation of one receiver type in terms of another. It defines tag_invoke overloads that forward to named members if they exist, and to the adapted receiver otherwise.
-
-
If Base is an alias for the unspecified default template argument, then:
-
-
-
Let HAS-BASE be false, and
-
-
Let GET-BASE(d) be d.base().
-
-
otherwise, let:
-
-
-
Let HAS-BASE be true, and
-
-
Let GET-BASE(d) be c-style-cast<receiver_adaptor<Derived,Base>>(d).base().
-
-
Let BASE-TYPE(D) be the type of GET-BASE(declval<D>()).
-
-
receiver_adaptor<Derived,Base> is equivalent to the following:
[Note:receiver_adaptor provides tag_invoke overloads on behalf of
-the derived class Derived, which is incomplete when receiver_adaptor is
-instantiated.]
Let SET-VALUE be the expression std::move(self).set_value(std::forward<As>(as)...).
-
-
Constraints: Either SET-VALUE is a valid expression or typenameDerived::set_value denotes a type and callable<set_value_t,BASE-TYPE(Derived),As...> is true.
-
-
Mandates:SET-VALUE, if that expression is valid, is not potentially throwing.
Let SET-ERROR be the expression std::move(self).set_error(std::forward<E>(e)).
-
-
Constraints: Either SET-ERROR is a valid expression or typenameDerived::set_error denotes a type and callable<set_error_t,BASE-TYPE(Derived),E> is true.
-
-
Mandates:SET-ERROR, if that expression is valid, is not potentially throwing.
Let SET-STOPPED be the expression std::move(self).set_stopped().
-
-
Constraints: Either SET-STOPPED is a valid expression or typenameDerived::set_stopped denotes a type and callable<set_stopped_t,BASE-TYPE(Derived)> is true.
-
-
Mandates:SET-STOPPED, if that expression is valid, is not potentially throwing.
-
-
Effects: Equivalent to:
-
-
-
If SET-STOPPED is a valid expression, SET-STOPPED;
Constraints: Either self.get_env() is a valid expression or typenameDerived::get_env denotes a type and callable<get_env_t,BASE-TYPE(constDerived&)> is true.
-
-
Effects: Equivalent to:
-
-
-
If self.get_env() is a valid expression, self.get_env();
-
-
Otherwise, execution::get_env(GET-BASE(self)).
-
-
-
Remarks: The expression in the noexcept clause is:
-
-
-
If self.get_env() is a valid expression, noexcept(self.get_env());
completion_signatures is used to describe the completion signals of a receiver that
-a sender may invoke. Its template argument list is a list of function types corresponding
-to the signatures of the receiver’s completion signals.
-
-
[Example:
-
classmy_sender{
- usingcompletion_signatures=
- execution::completion_signatures<
- execution::set_value_t(),
- execution::set_value_t(int,float),
- execution::set_error_t(exception_ptr),
- execution::set_error_t(error_code),
- execution::set_stopped_t()>;
-};
-
-// Declares my_sender to be a sender that can complete by calling
-// one of the following for a receiver expression R:
-// execution::set_value(R)
-// execution::set_value(R, int{...}, float{...})
-// execution::set_error(R, exception_ptr{...})
-// execution::set_error(R, error_code{...})
-// execution::set_stopped(R)
-
-
-- end example]
-
-
This section makes use of the following exposition-only entities:
Let Fns... be a template parameter pack of the arguments of the completion_signatures instantiation named by completion_signatures_of_t<S,E>, let TagFns be a
-template parameter pack of the function types in Fns whose return types
-are Tag, and let Tsn be a template parameter
-pack of the function argument types in the n-th type
-in TagFns. Then, given two variadic templates Tuple and Variant, the type gather-signatures<Tag,S,E,Tuple,Variant> names the type Variant<Tuple<Ts0...>,Tuple<Ts1...>,...Tuple<Tsm-1...>>, where m is the size of the parameter pack TagFns.
make_completion_signatures is an alias template used to adapt the
-completion signatures of a sender. It takes a sender, and environment, and
-several other template arguments that apply modifications to the sender’s
-completion signatures to generate a new instantiation of execution::completion_signatures.
-
-
[Example:
-
// Given a sender S and an environment Env, adapt a S’s completion
-// signatures by lvalue-ref qualifying the values, adding an additional
-// exception_ptr error completion if its not already there, and leaving the
-// other signals alone.
-template<class...Args>
- usingmy_set_value_t=
- execution::completion_signatures<
- execution::set_value_t(add_lvalue_reference_t<Args>...)>;
-
-usingmy_completion_signals=
- execution::make_completion_signatures<
- S,Env,
- execution::completion_signatures<execution::set_error_t(exception_ptr)>,
- my_set_value_t>;
-
-
-- end example]
-
-
This section makes use of the following exposition-only entities:
SetValue shall name an alias template such that for any template
-parameter pack As..., the type SetValue<As...> is either ill-formed
-or else valid-completion-signatures<SetValue<As...>,E> is satisfied.
-
-
SetError shall name an alias template such that for any type Err, SetError<Err> is either ill-formed or else valid-completion-signatures<SetError<Err>,E> is satisfied.
-
-
Then:
-
-
-
Let Vs... be a pack of the types in the type-list named
-by value_types_of_t<Sndr,Env,SetValue,type-list>.
-
-
Let Es... be a pack of the types in the type-list named by error_types_of_t<Sndr,Env,error-list>, where error-list is an
-alias template such that error-list<Ts...> names type-list<SetError<Ts>...>.
-
-
Let Ss name the type completion_signatures<> if sends_stopped<Sndr,Env> is false; otherwise, SetStopped.
-
-
Then:
-
-
-
If any of the above types are ill-formed, then make_completion_signatures<Sndr,Env,AddlSigs,SetValue,SetError,SetStopped> is ill-formed,
-
-
Otherwise, make_completion_signatures<Sndr,Env,AddlSigs,SetValue,SetError,SetStopped> names the type completion_signatures<Sigs...> where Sigs... is the unique set of types in all the template arguments
-of all the completion_signatures instantiations in [AddlSigs,Vs...,Es...,Ss].
-
-
-
11.11. Execution contexts [exec.ctx]
-
-
-
This section specifies some execution contexts on which work can be scheduled.
-
-
11.11.1. run_loop[exec.run_loop]
-
-
-
A run_loop is an execution context on which work can be scheduled. It maintains a simple, thread-safe first-in-first-out queue of work. Its run() member function removes elements from the queue and executes them in a loop on whatever thread of execution calls run().
-
-
A run_loop instance has an associated count that corresponds to the number of work items that are in its queue. Additionally, a run_loop has an associated state that can be one of starting, running, or finishing.
-
-
Concurrent invocations of the member functions of run_loop, other than run and its destructor, do not introduce data races. The member functions pop_front, push_back, and finish execute atomically.
-
-
[Note: Implementations are encouraged to use an intrusive queue of operation states to hold the work units to make scheduling allocation-free. — end note]
-
classrun_loop{
- // [exec.run_loop.types] Associated types
- classrun-loop-scheduler;// exposition only
- classrun-loop-sender;// exposition only
- structrun-loop-opstate-base{// exposition only
- virtualvoidexecute()=0;
- run_loop*loop_;
- run-loop-opstate-base*next_;
- };
- template<receiver_ofR>
- usingrun-loop-opstate=unspecified;// exposition only
-
- // [exec.run_loop.members] Member functions:
- run-loop-opstate-base*pop_front();// exposition only
- voidpush_back(run-loop-opstate-base*);// exposition only
-
- public:
- // [exec.run_loop.ctor] construct/copy/destroy
- run_loop()noexcept;
- run_loop(run_loop&&)=delete;
- ~run_loop();
-
- // [exec.run_loop.members] Member functions:
- run-loop-schedulerget_scheduler();
- voidrun();
- voidfinish();
-};
-
-
-
11.11.1.1. Associated types [exec.run_loop.types]
-
classrun-loop-scheduler;
-
-
-
-
run-loop-scheduler is an implementation defined type that models the scheduler concept.
-
-
Instances of run-loop-scheduler remain valid until the end of the lifetime of the run_loop instance from which they were obtained.
-
-
Two instances of run-loop-scheduler compare equal if and only if they were obtained from the same run_loop instance.
-
-
Let sch be an expression of type run-loop-scheduler. The expression execution::schedule(sch) is not potentially throwing and has type run-loop-sender.
-
-
classrun-loop-sender;
-
-
-
-
run-loop-sender is an implementation-defined type such that sender_of<run-loop-sender,set_value_t()> is true. Additionally, the types reported by its error_types associated type is exception_ptr, and the value of its sends_stopped trait is true.
-
-
An instance of run-loop-sender remains valid until the end of the lifetime of its associated execution::run_loop instance.
-
-
Let s be an expression of type run-loop-sender, let r be an expression such that decltype(r) models the receiver_of concept, and let C be either set_value_t or set_stopped_t. Then:
-
-
-
The expression execution::connect(s,r) has type run-loop-opstate<decay_t<decltype(r)>> and is potentially throwing if and only if the initialiation of decay_t<decltype(r)> from r is potentially throwing.
-
-
The expression get_completion_scheduler<C>(get_attrs(s)) is not potentially throwing, has type run-loop-scheduler, and compares equal to the run-loop-scheduler instance from which s was obtained.
-
-
-
template<receiver_ofR>// arguments are not associated entities ([lib.tmpl-heads])
- structrun-loop-opstate;
-
-
-
-
run-loop-opstate<R> inherits unambiguously from run-loop-opstate-base.
-
-
Let o be a non-const lvalue of type run-loop-opstate<R>, and let REC(o) be a non-const lvalue reference to an instance of type R that was initialized with the expression r passed to the invocation of execution::connect that returned o. Then:
-
-
-
The object to which REC(o) refers remains valid for the lifetime of the object to which o refers.
-
-
The type run-loop-opstate<R> overrides run-loop-opstate-base::execute() such that o.execute() is equivalent to the following:
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:
where ENV_OF(P) names the type env_of_t<P> if that type
-is well-formed, or empty-env otherwise.
-
-
-
Alias template single-sender-value-type is defined as follows:
-
-
-
If value_types_of_t<S,E,Tuple,Variant> would have the form Variant<Tuple<T>>, then single-sender-value-type<S,E> is an alias for type T.
-
-
Otherwise, if value_types_of_t<S,E,Tuple,Variant> would have the form Variant<Tuple<>> or Variant<>, then single-sender-value-type<S,E> is an alias for type void.
-
-
Otherwise, single-sender-value-type<S,E> is ill-formed.
-
-
-
The type sender-awaitable<S,P> is equivalent to the following:
Let r be an rvalue expression of type awaitable-receiver, let cr be a const lvalue that refers to r, let vs... be an arbitrary function parameter pack of types Vs..., and let err be an arbitrary expression of type Err. Then:
-
-
-
If constructible_from<result_t,Vs...> is satisfied, the expression execution::set_value(r,vs...) is not potentially throwing and is equivalent to:
err if decay_t<Err> names the same type as exception_ptr,
-
-
Otherwise, make_exception_ptr(system_error(err)) if decay_t<Err> names the same type as error_code,
-
-
Otherwise, make_exception_ptr(err).
-
-
-
The expression execution::set_stopped(r) is not potentially throwing and is equivalent to static_cast<coroutine_handle<>>(r.continuation_.promise().unhandled_stopped()).resume().
-
-
For any expression tag whose type satisfies forwarding-query and for any pack of subexpressions as, tag_invoke(tag,get_env(cr),as...) is expression-equivalent to tag(get_env(as_const(cr.continuation_.promise())),as...) when that expression is well-formed.
-
-
-
sender-awaitable::sender-awaitable(S&&s,P&p)
-
-
-
Effects: initializes state_ with connect(std::forward<S>(s),awaitable-receiver{&result_,coroutine_handle<P>::from_promise(p)}).
as_awaitable is a customization point object. For some subexpressions e and p where p is an lvalue, E names the type decltype((e)) and P names the type decltype((p)), as_awaitable(e,p) is expression-equivalent to the following:
-
-
-
tag_invoke(as_awaitable,e,p) if that expression is well-formed.
-
-
-
Mandates:is-awaitable<A,P> is true, where A is the type of the tag_invoke expression above.
-
-
-
Otherwise, e if is-awaitable<E> is true. The condition is not is-awaitable<E,P> as that creates the potential for constraint recursion.
-
-
Otherwise, sender-awaitable{e,p} if awaitable-sender<E,P> is true.
with_awaitable_senders, when used as the base class of a coroutine promise type, makes senders awaitable in that coroutine type.
-
In addition, it provides a default implementation of unhandled_stopped() such that if a sender completes by calling execution::set_stopped, it is treated as if an uncatchable "stopped" exception were thrown from the await-expression. In practice, the coroutine is never resumed, and the unhandled_stopped of the coroutine caller’s promise type is called.
ISO/IEC JTC1/SC22/WG21 14882: Programming Language — C++
-
Audience:
-
SG1, LEWG
-
-
-
-
-
-
-
-
1. Introduction
-
This paper proposes a self-contained design for a Standard C++ framework for managing asynchronous execution on generic execution resources. It is based on the ideas in A Unified Executors Proposal for C++ 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 needs 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 of concurrency primitives (std::atomic, std::mutex, std::counting_semaphore, etc) and lower level building blocks (std::thread, etc), we lack a Standard vocabulary and framework for asynchrony and parallelism that C++ programmers desperately need. std::async/std::future/std::promise, 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 resources.
-
-
Encapsulate common asynchronous patterns in customizable and reusable algorithms, so users don’t have to invent things themselves.
-
-
Make it easy to be correct by construction.
-
-
Support the diversity of execution resources and execution agents, because not all execution agents are created equal; some are less capable than others, but not less important.
-
-
Allow everything to be customized by an execution resource, including transfer to other execution resources, but don’t require that execution resources customize everything.
-
-
Care about all reasonable use cases, domains and platforms.
-
-
Errors must be propagated, but error handling must not present a burden.
-
-
Support cancellation, which is not an error.
-
-
Have clear and concise answers for where things execute.
-
-
Be able to manage and terminate the lifetimes of objects asynchronously.
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. A sender describes asynchronous work and sends a signal (value, error, or stopped) 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 std::invocable, and calls the std::invocable on the signal sent by the input sender. The sender returned by then sends the result of that invocation. In this case, the input sender came from schedule, so its void, meaning it won’t send us a value, so our std::invocable takes no parameters. But we return an int, which will be sent to the next recipient.
-
-
Now, we add another operation to the chain, again using § 4.21.2 execution::then. This time, we get sent a value - the int from the previous step. We add 42 to it, and then return the result.
-
-
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 std::optional<std::tuple<...>> with the value sent by the last sender, or an empty std::optional if the last sender sent a stopped signal, or it throws an exception if the last sender sent an error.
This example builds an asynchronous computation of an inclusive scan:
-
-
-
It scans a sequence of doubles (represented as the std::span<constdouble>input) and stores the result in another sequence of doubles (represented as std::span<double>output).
-
-
It takes a scheduler, which specifies what execution resource the scan should be launched on.
-
-
It also takes a tile_count parameter that controls the number of execution agents that will be spawned.
-
-
First we need to allocate temporary storage needed for the algorithm, which we’ll do with a std::vector, partials. We need one double of temporary storage for each execution agent we create.
-
-
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 sch, which means the next item in the chain will use sch.
-
-
Senders and sender adaptors support composition via operator|, similar to C++ ranges. We’ll use operator| to attach the next piece of work, which will spawn tile_count execution agents using § 4.21.9 execution::bulk (see § 4.13 Most sender adaptors are pipeable for details).
-
-
Each agent will call a std::invocable, passing it two arguments. The first is the agent’s index (i) in the § 4.21.9 execution::bulk operation, in this case a unique integer in [0,tile_count). The second argument is what the input sender sent - the temporary storage.
-
-
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 std::inclusive_scan over our elements. We store the scan result for our last element, which is the sum of all of our elements, in our temporary storage 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 partials. 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.
-
-
§ 4.21.2 execution::then takes an input sender and an std::invocable and calls the std::invocable with the value sent by the input sender. Inside our std::invocable, we call std::inclusive_scan on partials, which the input senders will send to us.
-
-
Then we return partials, which the next phase will need.
-
-
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 partials to integrate the sums from other tiles into our elements, completing the inclusive scan.
-
-
async_inclusive_scan returns a sender that sends the output std::span<double>. A consumer of the algorithm can chain additional work that uses the scan result. At the point at which async_inclusive_scan returns, the computation may not have completed. In fact, it may not have even started.
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:
-
-
-
async_read 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 a std::span<std::byte>, and a handle to an I/O context. It will asynchronously read into the input buffer, up to the size of the std::span. It returns a sender which will send the number of bytes read once the read completes.
-
-
async_read_array 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 a dynamic_buffer object that owns the data that was sent.
-
-
dynamic_buffer is an aggregate struct that contains a std::unique_ptr<std::byte[]> and a size.
-
-
The first thing we do inside of async_read_array is create a sender that will send a new, empty dynamic_array object using § 4.20.2 execution::just. We can attach more work to the pipeline using operator| composition (see § 4.13 Most sender adaptors are pipeable for details).
-
-
We need the lifetime of this dynamic_array object to last for the entire pipeline. So, we use let_value, which takes an input sender and a std::invocable that must return a sender itself (see § 4.21.4 execution::let_* for details). let_value sends the value from the input sender to the std::invocable. Critically, the lifetime of the sent object will last until the sender returned by the std::invocable completes.
-
-
Inside of the let_valuestd::invocable, we have the rest of our logic. First, we want to initiate an async_read of the buffer size. To do that, we need to send a std::span pointing to buf.size. We can do that with § 4.20.2 execution::just.
Next, we pipe a std::invocable that will be invoked after the async_read completes using § 4.21.2 execution::then.
-
-
That std::invocable gets sent the number of bytes read.
-
-
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 std::span<std::byte> to the storage for the data from the std::invocable. This will be sent to the next recipient in the pipeline.
-
-
And that recipient will be another async_read, which will read the data.
-
-
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 dynamic_buffer object. It will get sent by the sender returned by async_read_array. We can attach more things to that sender to use the data in the buffer.
-
-
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 async_recv() operation for a Windows Socket.
-
structoperation_base:WSAOVERALAPPED{
- usingcompletion_fn=void(operation_base*op,DWORDbytesTransferred,interrorCode)noexcept;
-
- // Assume IOCP event loop will call this when this OVERLAPPED structure is dequeued.
- completion_fn*completed;
-};
-
-template<typenameReceiver>
-structrecv_op:operation_base{
- recv_op(SOCKETs,void*data,size_tlen,Receiverr)
- :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);
- }
-
- friendvoidtag_invoke(std::execution::start_t,recv_op&self)noexcept{
- // Avoid even calling WSARecv() if operation already cancelled
- autost=std::execution::get_stop_token(
- std::get_env(self.receiver));
- if(st.stop_requested()){
- std::execution::set_stopped(std::move(self.receiver));
- return;
- }
-
- // Store and cache result here in case it changes during execution
- constboolstopPossible=st.stop_possible();
- if(!stopPossible){
- self.ready.store(true,std::memory_order_relaxed);
- }
-
- // Launch the operation
- DWORDbytesTransferred=0;
- DWORDflags=0;
- intresult=WSARecv(self.sock,&self.buffer,1,&bytesTransferred,&flags,
- static_cast<WSAOVERLAPPED*>(&self), NULL);
- if(result==SOCKET_ERROR){
- interrorCode=WSAGetLastError();
- if(errorCode!=WSA_IO_PENDING)){
- if(errorCode==WSA_OPERATION_ABORTED){
- std::execution::set_stopped(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();
-
- BOOLok=WSAGetOverlappedResult(self.sock,(WSAOVERLAPPED*)&self,&bytesTransferred,FALSE,&flags);
- if(ok){
- std::execution::set_value(std::move(self.receiver),bytesTransferred);
- }else{
- interrorCode=WSAGetLastError();
- std::execution::set_error(std::move(self.receiver),
- std::error_code(errorCode,std::system_category()));
- }
- }
- }
- }
-
- structcancel_cb{
- recv_op&op;
-
- voidoperator()()noexcept{
- CancelIoEx((HANDLE)op.sock,(OVERLAPPED*)(WSAOVERLAPPED*)&op);
- }
- };
-
- staticvoidon_complete(operation_base*op,DWORDbytesTransferred,interrorCode)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()));
- }
- }
- }
-
- Receiverreceiver;
- SOCKETsock;
- WSABUFbuffer;
- std::optional<typenamestop_callback_type_t<Receiver>
- ::templatecallback_type<cancel_cb>>stopCallback;
- std::atomic<bool>ready{false};
-};
-
-structrecv_sender{
- usingis_sender=void;
- SOCKETsock;
- void*data;
- size_tlen;
-
- template<typenameReceiver>
- friendrecv_op<Receiver>tag_invoke(std::execution::connect_t,
- constrecv_sender&s,
- Receiverr){
- returnrecv_op<Receiver>{s.sock,s.data,s.len,std::move(r)};
- }
-};
-
-recv_senderasync_recv(SOCKETs,void*data,size_tlen){
- returnrecv_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.
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.
In this code, NN::async_read_some and NN::async_write_some are asynchronous socket-based networking APIs that return senders. EX::repeat_effect_until, EX::let_value, and EX::then 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 repeat_effect_until 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.
This code builds a then 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 (set_error and set_stopped), as well as all receiver queries,
-are passed through unchanged.
-
In detail, it does the following:
-
-
-
Defines a receiver in terms of execution::receiver_adaptor that aggregates
-another receiver and an invocable that:
-
-
-
Defines a constrained tag_invoke overload for transforming the value
-channel.
-
-
Defines another constrained overload of tag_invoke that passes all other
-customizations through unchanged.
-
-
The tag_invoke overloads are actually implemented by execution::receiver_adaptor; they dispatch either to named members, as
-shown above with _then_receiver::set_value, or to the adapted receiver.
-
-
Defines a sender that aggregates another sender and the invocable, which defines a tag_invoke customization for std::execution::connect that wraps the incoming receiver in the receiver from (1) and passes it and the incoming sender to std::execution::connect, returning the result. It also defines a tag_invoke customization of get_completion_signatures that declares the sender’s completion signatures when executed within a particular environment.
-
-
1.5.2. retry
-
usingnamespacestd;
-namespaceexec=execution;
-
-template<classFrom,classTo>
-concept_decays_to=same_as<decay_t<From>,To>;
-
-// _conv needed so we can emplace construct non-movable types into
-// a std::optional.
-template<invocableF>
- requiresis_nothrow_move_constructible_v<F>
-struct_conv{
- Ff_;
- explicit_conv(Ff)noexcept:f_((F&&)f){}
- operatorinvoke_result_t<F>()&&{
- return((F&&)f_)();
- }
-};
-
-template<classS,classR>
-struct_op;
-
-// pass through all customizations except set_error, which retries the operation.
-template<classS,classR>
-struct_retry_receiver
- :exec::receiver_adaptor<_retry_receiver<S,R>>{
- _op<S,R>*o_;
-
- R&&base()&&noexcept{return(R&&)o_->r_;}
- constR&base()const&noexcept{returno_->r_;}
-
- explicit_retry_receiver(_op<S,R>*o):o_(o){}
-
- voidset_error(auto&&)&&noexcept{
- o_->_retry();// This causes the op to be retried
- }
-};
-
-// Hold the nested operation state in an optional so we can
-// re-construct and re-start it if the operation fails.
-template<classS,classR>
-struct_op{
- Ss_;
- Rr_;
- optional<
- exec::connect_result_t<S&,_retry_receiver<S,R>>>o_;
-
- _op(Ss,Rr):s_((S&&)s),r_((R&&)r),o_{_connect()}{}
- _op(_op&&)=delete;
-
- auto_connect()noexcept{
- return_conv{[this]{
- returnexec::connect(s_,_retry_receiver<S,R>{this});
- }};
- }
- void_retry()noexcepttry{
- o_.emplace(_connect());// potentially-throwing
- exec::start(*o_);
- }catch(...){
- exec::set_error((R&&)r_,std::current_exception());
- }
- friendvoidtag_invoke(exec::start_t,_op&o)noexcept{
- exec::start(*o.o_);
- }
-};
-
-template<classS>
-struct_retry_sender{
- usingis_sender=void;
- Ss_;
- explicit_retry_sender(Ss):s_((S&&)s){}
-
- template<class...Ts>
- using_value_t=
- exec::completion_signatures<exec::set_value_t(Ts...)>;
- template<class>
- using_error_t=exec::completion_signatures<>;
-
- // Declare the signatures with which this sender can complete
- template<classEnv>
- friendautotag_invoke(exec::get_completion_signatures_t,const_retry_sender&,Env)
- ->exec::make_completion_signatures<S&,Env,
- exec::completion_signatures<exec::set_error_t(std::exception_ptr)>,
- _value_t,_error_t>;
-
- template<exec::receiverR>
- friend_op<S,R>tag_invoke(exec::connect_t,_retry_sender&&self,Rr){
- return{(S&&)self.s_,(R&&)r};
- }
-
- frienddecltype(auto)tag_invoke(exec::get_env_t,const_retry_sender&self)noexcept{
- returnget_env(self.s_);
- }
-};
-
-template<exec::senderS>
-exec::senderautoretry(Ss){
- return_retry_sender{(S&&)s};
-}
-
-
The retry algorithm takes a multi-shot sender and causes it to repeat on error, passing
-through values and stopped signals. Each time the input sender is restarted, a new receiver
-is connected and the resulting operation state is stored in an optional, which allows us
-to reinitialize it multiple times.
-
This example does the following:
-
-
-
Defines a _conv utility that takes advantage of C++17’s guaranteed copy elision to
-emplace a non-movable type in a std::optional.
-
-
Defines a _retry_receiver 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 set_error, which causes a _retry() function to be called instead.
-
-
Defines an operation state that aggregates the input sender and receiver, and declares
-storage for the nested operation state in an optional. Constructing the operation
-state constructs a _retry_receiver with a pointer to the (under construction) operation
-state and uses it to connect to the aggregated sender.
-
-
Starting the operation state dispatches to start on the inner operation state.
-
-
The _retry() 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.
-
-
After reinitializing the inner operation state, _retry() calls start on it, causing
-the failed operation to be rescheduled.
-
-
Defines a _retry_sender that implements the connect customization point to return
-an operation state constructed from the passed-in sender and receiver.
-
-
_retry_sender also implements the get_completion_signatures customization point to describe the ways this sender may complete when executed in a particular execution resource.
-
-
1.6. Examples: Schedulers
-
In this section we look at some schedulers of varying complexity.
The inline scheduler is a trivial scheduler that completes immediately and synchronously on
-the thread that calls std::execution::start on the operation state produced by its sender.
-In other words, start(connect(schedule(inline-scheduler),receiver)) is
-just a fancy way of saying set_value(receiver), with the exception of the fact that start wants to be passed an lvalue.
-
Although not a particularly useful scheduler, it serves to illustrate the basics of
-implementing one. The inline_scheduler:
-
-
-
Customizes execution::schedule to return an instance of the sender type _sender.
-
-
The _sender type models the sender concept and provides the metadata
-needed to describe it as a sender of no values
-and that never calls set_error or set_stopped. This
-metadata is provided with the help of the execution::completion_signatures utility.
-
-
The _sender type customizes execution::connect to accept a receiver of no
-values. It returns an instance of type _op that holds the receiver by
-value.
-
-
The operation state customizes std::execution::start to call std::execution::set_value on the receiver.
-
-
1.6.2. Single thread scheduler
-
This example shows how to create a scheduler for an execution resource that consists of a single
-thread. It is implemented in terms of a lower-level execution resource called std::execution::run_loop.
The single_thread_context 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 execution::run_loop 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 this_thread::sync_wait, 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. Examples: Server theme
-
In this section we look at some examples of how one would use senders to implement an HTTP server. The examples ignore the low-level details of the HTTP server and looks at how senders can be combined to achieve the goals of the project.
-
General application context:
-
-
-
server application that processes images
-
-
execution resources:
-
-
-
1 dedicated thread for network I/O
-
-
N worker threads used for CPU-intensive work
-
-
M threads for auxiliary I/O
-
-
optional GPU context that may be used on some types of servers
-
-
-
all parts of the applications can be asynchronous
-
-
no locks shall be used in user code
-
-
1.7.1. Composability with execution::let_*
-
Example context:
-
-
-
we are looking at the flow of processing an HTTP request and sending back the response
-
-
show how one can break the (slightly complex) flow into steps with execution::let_* functions
-
-
different phases of processing HTTP requests are broken down into separate concerns
-
-
each part of the processing might use different execution resources (details not shown in this example)
-
-
error handling is generic, regardless which component fails; we always send the right response to the clients
-
-
Goals:
-
-
-
show how one can break more complex flows into steps with let_* functions
-
-
exemplify the use of let_value, let_error, let_stopped, and just algorithms
-
-
namespaceex=std::execution;
-
-// Returns a sender that yields an http_request object for an incoming request
-ex::senderautoschedule_request_start(read_requests_ctxctx){...}
-// Sends a response back to the client; yields a void signal on success
-ex::senderautosend_response(consthttp_response&resp){...}
-// Validate that the HTTP request is well-formed; forwards the request on success
-ex::senderautovalidate_request(consthttp_request&req){...}
-
-// Handle the request; main application logic
-ex::senderautohandle_request(consthttp_request&req){
- //...
- returnex::just(http_response{200,result_body});
-}
-
-// Transforms server errors into responses to be sent to the client
-ex::senderautoerror_to_response(std::exception_ptrerr){
- try{
- std::rethrow_exception(err);
- }catch(conststd::invalid_argument&e){
- returnex::just(http_response{404,e.what()});
- }catch(conststd::exception&e){
- returnex::just(http_response{500,e.what()});
- }catch(...){
- returnex::just(http_response{500,"Unknown server error"});
- }
-}
-// Transforms cancellation of the server into responses to be sent to the client
-ex::senderautostopped_to_response(){
- returnex::just(http_response{503,"Service temporarily unavailable"});
-}
-//...
-// The whole flow for transforming incoming requests into responses
-ex::senderautosnd=
- // get a sender when a new request comes
- schedule_request_start(the_read_requests_ctx)
- // make sure the request is valid; throw if not
- |ex::let_value(validate_request)
- // process the request in a function that may be using a different execution resource
- |ex::let_value(handle_request)
- // If there are errors transform them into proper responses
- |ex::let_error(error_to_response)
- // If the flow is cancelled, send back a proper response
- |ex::let_stopped(stopped_to_response)
- // write the result back to the client
- |ex::let_value(send_response)
- // done
- ;
-// execute the whole flow asynchronously
-ex::start_detached(std::move(snd));
-
-
The example shows how one can separate out the concerns for interpreting requests, validating requests, running the main logic for handling the request, generating error responses, handling cancellation and sending the response back to the client.
-They are all different phases in the application, and can be joined together with the let_* functions.
-
All our functions return execution::sender objects, so that they can all generate success, failure and cancellation paths.
-For example, regardless where an error is generated (reading request, validating request or handling the response), we would have one common block to handle the error, and following error flows is easy.
-
Also, because of using execution::sender objects at any step, we might expect any of these steps to be completely asynchronous; the overall flow doesn’t care.
-Regardless of the execution resource in which the steps, or part of the steps are executed in, the flow is still the same.
-
1.7.2. Moving between execution resources with execution::on and execution::transfer
-
Example context:
-
-
-
reading data from the socket before processing the request
-
-
reading of the data is done on the I/O context
-
-
no processing of the data needs to be done on the I/O context
-
-
Goals:
-
-
-
show how one can change the execution resource
-
-
exemplify the use of on and transfer algorithms
-
-
namespaceex=std::execution;
-
-size_tlegacy_read_from_socket(intsock,char*buffer,size_tbuffer_len){}
-voidprocess_read_data(constchar*read_data,size_tread_len){}
-//...
-
-// A sender that just calls the legacy read function
-autosnd_read=ex::just(sock,buf,buf_len)|ex::then(legacy_read_from_socket);
-// The entire flow
-autosnd=
- // start by reading data on the I/O thread
- ex::on(io_sched,std::move(snd_read))
- // do the processing on the worker threads pool
- |ex::transfer(work_sched)
- // process the incoming data (on worker threads)
- |ex::then([buf](intread_len){process_read_data(buf,read_len);})
- // done
- ;
-// execute the whole flow asynchronously
-ex::start_detached(std::move(snd));
-
-
The example assume that we need to wrap some legacy code of reading sockets, and handle execution resource switching.
-(This style of reading from socket may not be the most efficient one, but it’s working for our purposes.)
-For performance reasons, the reading from the socket needs to be done on the I/O thread, and all the processing needs to happen on a work-specific execution resource (i.e., thread pool).
-
Calling execution::on will ensure that the given sender will be started on the given scheduler.
-In our example, snd_read is going to be started on the I/O scheduler.
-This sender will just call the legacy code.
-
The completion-signal will be issued in the I/O execution resource, so we have to move it to the work thread pool.
-This is achieved with the help of the execution::transfer algorithm.
-The rest of the processing (in our case, the last call to then) will happen in the work thread pool.
-
The reader should notice the difference between execution::on and execution::transfer.
-The execution::on algorithm will ensure that the given sender will start in the specified context, and doesn’t care where the completion-signal for that sender is sent.
-The execution::transfer algorithm will not care where the given sender is going to be started, but will ensure that the completion-signal of will be transferred to the given context.
-
1.8. What this proposal is not
-
This paper is not a patch on top of A Unified Executors Proposal for C++; 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 A Unified Executors Proposal for C++; 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.9. Design changes from P0443
-
-
-
The executor concept has been removed and all of its proposed functionality
-is now based on schedulers and senders, as per SG1 direction.
-
-
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.
This paper extends the sender traits/typed sender design to support typed
-senders whose value/error types depend on type information provided late via
-the receiver.
-
-
Support for untyped senders is dropped; the typed_sender concept is renamed sender; sender_traits is replaced with completion_signatures_of_t.
-
-
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.
-
-
Some additional utilities are added:
-
-
-
run_loop: An execution resource that provides a multi-producer,
-single-consumer, first-in-first-out work queue.
-
-
receiver_adaptor: A utility for algorithm authors for defining one
-receiver type in terms of another.
-
-
completion_signatures and make_completion_signatures:
-Utilities for describing the ways in which a sender can complete in a
-declarative syntax.
-
-
-
1.10. 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.10.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.10.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 Halo: coroutine Heap Allocation eLision Optimization: the joint response, 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.10.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.11. Field experience
-
1.11.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 (with slight variations -- the design of P2300 has evolved due to LEWG feedback), many of this paper’s algorithms (some under different names), and much more besides.
-
Libunifex has several concrete schedulers in addition to the run_loop suggested here (where it is called manual_event_loop). 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 resource 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.11.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 resource. In HPX, algorithms can migrate execution between execution resources, 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 Simberg 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 resources.
-
-
The adaptors. Things like let_value are really nice additions.
-
-
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 f2 in the continuation will always be ready here f1.then([](future<T>f2){...}) is enough of a reason to separate the channels. All the other obvious reasons apply as well of course.
-
-
For futures we have a thing called hpx::dataflow which is an optimized version of when_all(...).then(...) which avoids intermediate allocations. With the sender/receiver when_all(...)|then(...) we get that "for free".
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 complete 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. It is
-fit for broad use and for contribution to libc++.
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.11.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:
Make it valid to pass non-variadic templates to the exposition-only alias
-template gather-signatures, fixing the definitions of value_types_of_t, error_types_of_t, and the exposition-only alias
-template sync-wait-type.
-
-
Removed the query forwarding from receiver_adaptor that was
-inadvertantly left over from a previous edit.
-
-
When adapting a sender to an awaitable with as_awaitable, the sender’s
-value result datum is decayed before being stored in the exposition-only variant.
-
-
Correctly specify the completion signatures of the schedule_from algorithm.
-
-
The sender_of concept no longer distinguishes between a sender of a
-type T and a sender of a type T&&.
-
-
The just and just_error sender factories now reject C-style arrays
-instead of silently decaying them to pointers.
-
-
Enhancements:
-
-
-
The sender and receiver concepts get explicit opt-in traits called enable_sender and enable_receiver, respectively. The traits have
-default implementations that look for nested is_sender and is_receiver types, respectively.
-
-
get_attrs is removed and get_env is used in its place.
-
-
The exposition-only type empty-env is made normative
-and is renamed empty_env.
-
-
get_env gets a fall-back implementation that simply returns empty_env{} if a tag_invoke overload is not found.
-
-
get_env is required to be insensitive to the cvref-qualification of its
-argument.
-
-
get_env, empty_env, and env_of_t are moved into the std:: namespace.
Fix typo in the specification of in_place_stop_source about the relative
-lifetimes of the tokens and the source that produced them.
-
-
get_completion_signatures tests for awaitability with a promise type
-similar to the one used by connect for the sake of consistency.
-
-
A coroutine promise type is an environment provider (that is, it implements get_env()) rather than being directly queryable. The previous draft was
-inconsistent about that.
-
-
Enhancements:
-
-
-
Sender queries are moved into a separate queryable "attributes" object
-that is accessed by passing the sender to get_attrs() (see below). The sender concept is reexpressed to require get_attrs() and separated
-from a new sender_in<Snd,Env> concept for checking whether a type is
-a sender within a particular execution environment.
-
-
The placeholder types no_env and dependent_completion_signatures<> are no longer needed and are dropped.
-
-
ensure_started and split are changed to persist the result of
-calling get_attrs() on the input sender.
-
-
Reorder constraints of the scheduler and receiver concepts to avoid constraint recursion
-when used in tandem with poorly-constrained, implicitly convertible types.
-
-
Re-express the sender_of concept to be more ergonomic and general.
-
-
Make the specification of the alias templates value_types_of_t and error_types_of_t, and the variable template sends_done more concise by
-expressing them in terms of a new exposition-only alias template gather-signatures.
-
-
2.2.1. Environments and attributes
-
In earlier revisions, receivers, senders, and schedulers all were directly
-queryable. In R4, receiver queries were moved into a separate "environment"
-object, obtainable from a receiver with a get_env accessor. In R6, the
-sender queries are given similar treatment, relocating to a "attributes"
-object obtainable from a sender with a get_attrs accessor. This was done
-to solve a number of design problems with the split and ensure_started algorithms; _e.g._, see NVIDIA/stdexec#466.
-
Schedulers, however, remain directly queryable. As lightweight handles
-that are required to be movable and copyable, there is little reason to
-want to dispose of a scheduler and yet persist the scheduler’s queries.
-
This revision also makes operation states directly queryable, even though
-there isn’t yet a use for such. Some early prototypes of cooperative bulk
-parallel sender algorithms done at NVIDIA suggest the utility of
-forwardable operation state queries. The authors chose to make opstates
-directly queryable since the opstate object is itself required to be kept
-alive for the duration of asynchronous operation.
-
2.3. R5
-
The changes since R4 are as follows:
-
Fixes:
-
-
-
start_detached requires its argument to be a void sender (sends no values
-to set_value).
-
-
Enhancements:
-
-
-
Receiver concepts refactored to no longer require an error channel for exception_ptr or a stopped channel.
-
-
sender_of concept and connect customization point additionally require
-that the receiver is capable of receiving all of the sender’s possible
-completions.
-
-
get_completion_signatures is now required to return an instance of either completion_signatures or dependent_completion_signatures.
-
-
make_completion_signatures made more general.
-
-
receiver_adaptor handles get_env as it does the set_* members; that is, receiver_adaptor will look for a member named get_env() in the derived
-class, and if found dispatch the get_env_t tag invoke customization to it.
-
-
just, just_error, just_stopped, and into_variant have been respecified
-as customization point objects instead of functions, following LEWG guidance.
-
-
2.4. R4
-
The changes since R3 are as follows:
-
Fixes:
-
-
-
Fix specification of get_completion_scheduler on the transfer, schedule_from and transfer_when_all algorithms; the completion scheduler cannot be guaranteed
-for set_error.
-
-
The value of sends_stopped for the default sender traits of types that are
-generally awaitable was changed from false to true to acknowledge the
-fact that some coroutine types are generally awaitable and may implement the unhandled_stopped() protocol in their promise types.
-
-
Fix the incorrect use of inline namespaces in the <execution> header.
-
-
Shorten the stable names for the sections.
-
-
sync_wait now handles std::error_code specially by throwing a std::system_error on failure.
-
-
Fix how ADL isolation from class template arguments is specified so it
-doesn’t constrain implmentations.
-
-
Properly expose the tag types in the header <execution> synopsis.
-
-
Enhancements:
-
-
-
Support for "dependently-typed" senders, where the completion signatures -- and
-thus the sender metadata -- depend on the type of the receiver connected
-to it. See the section dependently-typed
-senders below for more information.
-
-
Add a read(query) sender factory for issuing a query
-against a receiver and sending the result through the value channel. (This is
-a useful instance of a dependently-typed sender.)
-
-
Add completion_signatures utility for declaratively defining a typed
-sender’s metadata and a make_completion_signatures utility for adapting
-another sender’s completions in helpful ways.
-
-
Add make_completion_signatures utility for specifying a sender’s completion
-signatures by adapting those of another sender.
-
-
Drop support for untyped senders and rename typed_sender to sender.
-
-
set_done is renamed to set_stopped. All occurances of "done" in
-indentifiers replaced with "stopped"
-
-
Add customization points for controlling the forwarding of scheduler,
-sender, receiver, and environment queries through layers of adaptors;
-specify the behavior of the standard adaptors in terms of the new
-customization points.
-
-
Add get_delegatee_scheduler query to forward a scheduler that can be used
-by algorithms or by the scheduler to delegate work and forward progress.
-
-
Add schedule_result_t alias template.
-
-
More precisely specify the sender algorithms, including precisely what their
-completion signatures are.
-
-
stopped_as_error respecified as a customization point object.
-
-
tag_invoke respecified to improve diagnostics.
-
-
2.4.1. Dependently-typed senders
-
Background:
-
In the sender/receiver model, as with coroutines, contextual information about
-the current execution is most naturally propagated from the consumer to the
-producer. In coroutines, that means information like stop tokens, allocators and
-schedulers are propagated from the calling coroutine to the callee. In
-sender/receiver, that means that that contextual information is associated with
-the receiver and is queried by the sender and/or operation state after the
-sender and the receiver are connect-ed.
-
Problem:
-
The implication of the above is that the sender alone does not have all the
-information about the async computation it will ultimately initiate; some of
-that information is provided late via the receiver. However, the sender_traits mechanism, by which an algorithm can introspect the value and error types the
-sender will propagate, only accepts a sender parameter. It does not take into
-consideration the type information that will come in late via the receiver. The
-effect of this is that some senders cannot be typed senders when they
-otherwise could be.
-
Example:
-
To get concrete, consider the case of the "get_scheduler()" sender: when connect-ed and start-ed, it queries the receiver for its associated
-scheduler and passes it back to the receiver through the value channel. That
-sender’s "value type" is the type of the receiver’s scheduler. What then
-should sender_traits<get_scheduler_sender>::value_types report for the get_scheduler()'s value type? It can’t answer because it doesn’t know.
-
This causes knock-on problems since some important algorithms require a typed
-sender, such as sync_wait. To illustrate the problem, consider the following
-code:
-
namespaceex=std::execution;
-
-ex::senderautotask=
- ex::let_value(
- ex::get_scheduler(),// Fetches scheduler from receiver.
- [](autocurrent_sched){
- // Lauch some nested work on the current scheduler:
- returnex::on(current_sched,nestedwork...);
- });
-
-std::this_thread::sync_wait(std::move(task));
-
-
The code above is attempting to schedule some work onto the sync_wait's run_loop execution resource. But let_value only returns a typed sender when
-the input sender is typed. As we explained above, get_scheduler() is not
-typed, so task is likewise not typed. Since task isn’t typed, it cannot be
-passed to sync_wait which is expecting a typed sender. The above code would
-fail to compile.
-
Solution:
-
The solution is conceptually quite simple: extend the sender_traits mechanism
-to optionally accept a receiver in addition to the sender. The algorithms can
-use sender_traits<Sender,Receiver> to inspect the
-async operation’s completion-signals. The typed_sender concept would also need
-to take an optional receiver parameter. This is the simplest change, and it
-would solve the immediate problem.
-
Design:
-
Using the receiver type to compute the sender traits turns out to have pitfalls
-in practice. Many receivers make use of that type information in their
-implementation. It is very easy to create cycles in the type system, leading to
-inscrutible errors. The design pursued in R4 is to give receivers an associated environment object -- a bag of key/value pairs -- and to move the contextual
-information (schedulers, etc) out of the receiver and into the environment. The sender_traits template and the typed_sender concept, rather than taking a
-receiver, take an environment. This is a much more robust design.
-
A further refinement of this design would be to separate the receiver and the
-environment entirely, passing then as separate arguments along with the sender to connect. This paper does not propose that change.
-
Impact:
-
This change, apart from increasing the expressive power of the sender/receiver abstraction, has the following impact:
-
-
-
Typed senders become moderately more challenging to write. (The new completion_signatures and make_completion_signatures utilities are added
-to ease this extra burden.)
-
-
Sender adaptor algorithms that previously constrained their sender arguments
-to satisfy the typed_sender concept can no longer do so as the receiver is
-not available yet. This can result in type-checking that is done later, when connect is ultimately called on the resulting sender adaptor.
-
-
Operation states that own receivers that add to or change the environment
-are typically larger by one pointer. It comes with the benefit of far fewer
-indirections to evaluate queries.
-
-
"Has it been implemented?"
-
Yes, the reference implementation, which can be found at
-https://github.com/NVIDIA/stdexec, has implemented this
-design as well as some dependently-typed senders to confirm that it works.
-
Implementation experience
-
Although this change has not yet been made in libunifex, the most widely adopted sender/receiver implementation, a similar design can be found in Folly’s coroutine support library. In Folly.Coro, it is possible to await a special awaitable to obtain the current coroutine’s associated scheduler (called an executor in Folly).
-
For instance, the following Folly code grabs the current executor, schedules a task for execution on that executor, and starts the resulting (scheduled) task by enqueueing it for execution.
-
// From Facebook’s Folly open source library:
-template<classT>
-folly::coro::Task<void>CancellableAsyncScope::co_schedule(folly::coro::Task<T>&&task){
- this->add(std::move(task).scheduleOn(co_awaitco_current_executor));
- co_return;
-}
-
-
Facebook relies heavily on this pattern in its coroutine code. But as described
-above, this pattern doesn’t work with R3 of std::execution because of the lack
-of dependently-typed schedulers. The change to sender_traits in R4 rectifies that.
-
Why now?
-
The authors are loathe to make any changes to the design, however small, at this
-stage of the C++23 release cycle. But we feel that, for a relatively minor
-design change -- adding an extra template parameter to sender_traits and typed_sender -- the returns are large enough to justify the change. And there
-is no better time to make this change than as early as possible.
-
One might wonder why this missing feature not been added to sender/receiver
-before now. The designers of sender/receiver have long been aware of the need.
-What was missing was a clean, robust, and simple design for the change, which we
-now have.
-
Drive-by:
-
We took the opportunity to make an additional drive-by change: Rather than
-providing the sender traits via a class template for users to specialize, we
-changed it into a sender query: get_completion_signatures(sender,env). That function’s return type is used as the sender’s traits.
-The authors feel this leads to a more uniform design and gives sender authors a
-straightforward way to make the value/error types dependent on the cv- and
-ref-qualification of the sender if need be.
-
Details:
-
Below are the salient parts of the new support for dependently-typed senders in
-R4:
-
-
-
Receiver queries have been moved from the receiver into a separate environment
-object.
-
-
Receivers have an associated environment. The new get_env CPO retrieves a
-receiver’s environment. If a receiver doesn’t implement get_env, it returns
-an unspecified "empty" environment -- an empty struct.
-
-
sender_traits now takes an optional Env parameter that is used to
-determine the error/value types.
-
-
The primary sender_traits template is replaced with a completion_signatures_of_t alias implemented in terms of a new get_completion_signatures CPO that dispatches
-with tag_invoke. get_completion_signatures takes a sender and an optional
-environment. A sender can customize this to specify its value/error types.
-
-
Support for untyped senders is dropped. The typed_sender concept has been
-renamed to sender and now takes an optional environment.
-
-
The environment argument to the sender concept and the get_completion_signatures CPO defaults to no_env. All environment queries fail (are ill-formed) when
-passed an instance of no_env.
-
-
A type S is required to satisfy sender<S> to be
-considered a sender. If it doesn’t know what types it will complete with
-independent of an environment, it returns an instance of the placeholder
-traits dependent_completion_signatures.
-
-
If a sender satisfies both sender<S> and sender<S,Env>, then the completion signatures
-for the two cannot be different in any way. It is possible for an
-implementation to enforce this statically, but not required.
-
-
All of the algorithms and examples have been updated to work with
-dependently-typed senders.
-
-
2.5. R3
-
The changes since R2 are as follows:
-
Fixes:
-
-
-
Fix specification of the on algorithm to clarify lifetimes of
-intermediate operation states and properly scope the get_scheduler query.
-
-
Fix a memory safety bug in the implementation of connect-awaitable.
-
-
Fix recursive definition of the scheduler concept.
-
-
Enhancements:
-
-
-
Add run_loop execution resource.
-
-
Add receiver_adaptor utility to simplify writing receivers.
-
-
Require a scheduler’s sender to model sender_of and provide a completion scheduler.
-
-
Specify the cancellation scope of the when_all algorithm.
-
-
Make as_awaitable a customization point.
-
-
Change connect's handling of awaitables to consider those types that are awaitable owing to customization of as_awaitable.
-
-
Add value_types_of_t and error_types_of_t alias templates; rename stop_token_type_t to stop_token_of_t.
-
-
Add a design rationale for the removal of the possibly eager algorithms.
-
-
Expand the section on field experience.
-
-
2.6. R2
-
The changes since R1 are as follows:
-
-
-
Remove the eagerly executing sender algorithms.
-
-
Extend the execution::connect customization point and the sender_traits<> template to recognize awaitables as typed_senders.
-
-
Add utilities as_awaitable() and with_awaitable_senders<> so a coroutine type can trivially make senders awaitable with a coroutine.
-
-
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 bulk algorithm.
-
-
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 execution::just_error and execution::just_stopped.
-
-
Add sender adaptors execution::stopped_as_optional and execution::stopped_as_error.
-
-
Document more production uses of sender/receiver at scale.
-
-
Various fixes of typos and bugs.
-
-
2.7. 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 unschedule adaptor.
-
-
Various fixes of typos and bugs.
-
-
2.8. R0
-
Initial revision.
-
3. Design - introduction
-
The following three 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 A Unified Executors Proposal for C++: std::execution; however, for brevity, the std:: part of this name is
- omitted. When you see execution::foo, treat that as std::execution::foo.
-
-
Universal references and explicit calls to std::move/std::forward are
- omitted in code samples and signatures for simplicity; assume universal
- references and perfect forwarding unless stated otherwise.
-
-
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 callable 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 callable 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 resources describe the place of execution
-
An execution resource 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 resources
-
A scheduler is a lightweight handle that represents a strategy for
-scheduling work onto an execution resource. Since execution resources 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, schedule, which returns a sender that
-will complete on an execution resource determined by the scheduler. Logic that
-you want to run on that context can be placed in the receiver’s
-completion-signalling method.
-
execution::schedulerautosch=thread_pool.scheduler();
-execution::senderautosnd=execution::schedule(sch);
-// snd is a sender (see below) describing the creation of a new execution resource
-// on the execution resource associated with sch
-
-
Note that a particular scheduler type may provide other kinds of scheduling operations
-which are supported by its associated execution resource. It is not limited to scheduling
-purely using the execution::schedule API.
-
Future papers will propose additional scheduler concepts that extend scheduler to add other capabilities. For example:
-
-
-
A time_scheduler concept that extends scheduler to support time-based
-scheduling. Such a concept might provide access to schedule_after(sched,duration), schedule_at(sched,time_point) and now(sched) APIs.
-
-
Concepts that extend scheduler to support opening, reading and writing files
-asynchronously.
-
-
Concepts that extend scheduler to support connecting, sending data and
-receiving data over the network asynchronously.
-
-
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.
execution::schedulerautosch=thread_pool.scheduler();
-execution::senderautosnd=execution::schedule(sch);
-execution::senderautocont=execution::then(snd,[]{
- std::fstreamfile{"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 execution::connect) senders and return a sender.
-
-
sender consumers, algorithms that take (and potentially execution::connect) senders and do not return a sender.
-
-
4.5. Senders can propagate completion schedulers
-
One of the goals of executors is to support a diverse set of execution resources, 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 resource 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.
-
A Unified Executors Proposal for C++ 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 (Towards C++23 executors: A proposal for an initial set of algorithms) 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 resource (such as a CPU thread pool) from another execution resource (such as a GPU or a task framework), which assumes that all execution agents are as capable as a std::thread (which they aren’t).
-
-
forcibly interleaving two adjacent execution graph nodes that are both executing on one execution resource (such as a GPU) with glue code that runs on another execution resource (such as a CPU), which is prohibitively expensive for some execution resources (such as CUDA or SYCL).
-
-
having to customise most or all sender algorithms to support an execution resource, 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 on sender algorithm from Towards C++23 executors: A proposal for an initial set of algorithms, we are proposing a way for senders to advertise what scheduler (and by extension what execution resource) they will complete on.
-Any given sender may have completion schedulers for some or all of the signals (value, error, or stopped) 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
-
get_completion_scheduler is a query that retrieves the completion scheduler for a specific completion-signal from a sender’s environment.
-For a sender that lacks a completion scheduler query for a given signal, calling get_completion_scheduler 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 resource represented by a scheduler returned from this function.
-See § 4.5 Senders can propagate completion schedulers for more details.
-
execution::schedulerautocpu_sched=new_thread_scheduler{};
-execution::schedulerautogpu_sched=cuda::scheduler();
-
-execution::senderautosnd0=execution::schedule(cpu_sched);
-execution::schedulerautocompletion_sch0=
- execution::get_completion_scheduler<execution::set_value_t>(get_env(snd0));
-// completion_sch0 is equivalent to cpu_sched
-
-execution::senderautosnd1=execution::then(snd0,[]{
- std::cout<<"I am running on cpu_sched!\n";
-});
-execution::schedulerautocompletion_sch1=
- execution::get_completion_scheduler<execution::set_value_t>(get_env(snd1));
-// completion_sch1 is equivalent to cpu_sched
-
-execution::senderautosnd2=execution::transfer(snd1,gpu_sched);
-execution::senderautosnd3=execution::then(snd2,[]{
- std::cout<<"I am running on gpu_sched!\n";
-});
-execution::schedulerautocompletion_sch3=
- execution::get_completion_scheduler<execution::set_value_t>(get_env(snd3));
-// completion_sch3 is equivalent to gpu_sched
-
-
4.6. Execution resource transitions are explicit
-
A Unified Executors Proposal for C++ does not contain any mechanisms for performing an execution resource transition. The only sender algorithm that can create a sender that will move execution to a specific execution resource is execution::schedule, which does not take an input sender.
-That means that there’s no way to construct sender chains that traverse different execution resources. 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 resource transitions must be explicit; running user code anywhere but where they defined it to run must be considered a bug.
-
The execution::transfer sender adaptor performs a transition from one execution resource to another:
-
execution::schedulerautosch1=...;
-execution::schedulerautosch2=...;
-
-execution::senderautosnd1=execution::schedule(sch1);
-execution::senderautothen1=execution::then(snd1,[]{
- std::cout<<"I am running on sch1!\n";
-});
-
-execution::senderautosnd2=execution::transfer(then1,sch2);
-execution::senderautothen2=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 std::unique_ptr that it will be transferring ownership of to the
-operation-state returned by a call to execution::connect so that the operation has access to
-this resource. In such a sender, calling execution::connect 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 execution::connect only has overloads for
-an rvalue-qualified sender. Callers must pass the sender as an rvalue to the
-call to execution::connect, 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 execution::connect to accept an lvalue reference to the sender. Callers can
-indicate that they want the sender to remain valid after the call to execution::connect by passing an lvalue reference to the sender to call these
-overloads. Multi-shot senders should also define overloads of execution::connect 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 execution::connect.
-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 execution::connect. 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 execution::connect on the input sender, such as in this_thread::sync_wait or execution::start_detached.
-
Some multi-use sender algorithms may require that an input sender be copy-constructible but will only call execution::connect 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 execution::connect 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 split sender adaptor facilitates connecting to a sender multiple times, regardless of whether it is single-shot or multi-shot:
-
autosome_algorithm(execution::senderauto&&input){
- execution::senderautomulti_shot=split(input);
- // "multi_shot" is guaranteed to be multi-shot,
- // regardless of whether "input" was multi-shot or not
-
- returnwhen_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.
-
when_all 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. when_all returns a sender that also does not have an associated scheduler.
-
transfer_when_all 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 when_all. You can think of it as a composition of transfer(when_all(inputs...),scheduler), 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 execution::connect() before calling execution::start(), 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 when_all(ops...) algorithm should cancel other operations as soon as one operation fails
-
-
a first_successful(ops...) algorithm should cancel the other operations as soon as one operation completes successfuly
-
-
a generic timeout(src,duration) algorithm needs to be able to cancel the src operation after the timeout duration has elapsed.
-
-
a stop_when(src,trigger) algorithm should cancel src if trigger completes first and cancel trigger if src completes first
-
-
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:
In this example, if we take the operation returned by query_server_b(query), this operation will
-receive a stop-request when any of the following happens:
-
-
-
first_successful algorithm will send a stop-request if query_server_a(query) completes successfully
-
-
when_all algorithm will send a stop-request if the load_file("some_file.jpg") operation completes with an error or stopped result.
-
-
timeout algorithm will send a stop-request if the operation does not complete within 5 seconds.
-
-
stop_when algorithm will send a stop-request if the user clicks on the "Cancel" button in the user-interface.
-
-
The parent operation consuming the composed_cancellation_example() sends a stop-request
-
-
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.
At a high-level, the facilities proposed by this paper for supporting cancellation include:
-
-
-
Add std::stoppable_token and std::stoppable_token_for concepts that generalise the interface of std::stop_token type to allow other types with different implementation strategies.
-
-
Add std::unstoppable_token concept for detecting whether a stoppable_token can never receive a stop-request.
-
-
Add std::in_place_stop_token, std::in_place_stop_source and std::in_place_stop_callback<CB> types that provide a more efficient implementation of a stop-token for use in structured concurrency situations.
-
-
Add std::never_stop_token for use in places where you never want to issue a stop-request
-
-
Add std::execution::get_stop_token() CPO for querying the stop-token to use for an operation from its receiver’s execution environment.
-
-
Add std::execution::stop_token_of_t<T> for querying the type of a stop-token returned from 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 execution::get_stop_token() CPO.
-This CPO takes a single parameter, which is the execution environment of the receiver passed to execution::connect, and returns a std::stoppable_token that the operation can use to check for stop-requests for that operation.
-
As the caller of execution::connect typically has control over the receiver
-type it passes, it is able to customise the std::get_env() CPO for that
-receiver to return an execution environment that hooks the execution::get_stop_token() CPO to return a stop-token that the receiver 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’s execution environment does not customise the execution::get_stop_token() CPO then invoking the CPO on that receiver’s
-environment will invoke the default implementation which returns std::never_stop_token. This is a special stoppable_token type that is
-statically known to always return false from the stop_possible() 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 execution::get_stop_token(), 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 execution::start() 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 execution::start() 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 execution::start() implementation.
-
An implementation of execution::start() 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 execution::start 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 execution::start() call (which is still executing).
This paper currently includes the design for cancellation as proposed in Composable cancellation for sender-based async operations - "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 ensure_started 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 execution::connect?" or similarly, "What happens if a call
-to execution::connect is made, but the returned operation state is destroyed
-before execution::start 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 when_all(eager_op_1(),eager_op_2()). Imagine eager_op_1() starts an
-asynchronous operation successfully, but then eager_op_2() throws. For
-lazy senders, that failure happens in the context of the when_all 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 before when_all has been called.
-
It then becomes the responsibility, not of the algorithm, but of the end
-user to handle the exception and ensure that eager_op_1() is joined before
-allowing the exception to propagate. If they fail to do that, they incur
-undefined behavior.
-
-
Detach from the computation - let the operation continue in the background -
-like an implicit call to std::thread::detach(). 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::shared_ptr, to ensure resources are kept alive until the operations
-complete, and can make clean shutdown nigh impossible.
-
-
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 threads 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 std::terminate if an eager sender is destructed
-without joining it. This is the approach that std::thread destructor
-takes.
-
-
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 std::jthread uses for its
-destructor. It reduces the risk of deadlock but does not eliminate it.
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 execution::start 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 sequence that puts async operations in sequence by starting an operation only
-after the preceding one has completed. In an expression like sequence(a(),then(src,[]{b();}),c()), one my reasonably assume that a(), b() and c() 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.
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 execution::get_stop_token is an std::unstoppable_token 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 resource 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 resource) 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, get_forward_progress_guarantee, which returns one of the enumerators of a new enum type, forward_progress_guarantee. Each enumerator of forward_progress_guarantee 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 operator|.
-This mechanism is similar to the operator| 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.
-
a|b will pass the sender a as the first argument to the pipeable sender adaptor b. Pipeable sender adaptors support partial application of the parameters after the first. For example, all of the following are equivalent:
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:
Certain sender adaptors are not 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.
-
-
-
execution::when_all and execution::when_all_with_variant: 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::on: 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 to 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 R that is a range of senders, consider the following in a coroutine that returns an async generator type:
This transforms each element of the asynchronous sequence R with the function fn on demand, as the data arrives. The result is a new asynchronous sequence of the transformed values.
-
Now imagine that R is the simple expression views::iota(0)|views::transform(execution::just). 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 [fn(0), fn(1), fn(2), ...].
-
Far more interesting would be if R 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:
- returnread_socket_async(socket,span{buff})
- // For success (partial and full), specify the next action:
- |execution::let_value([](error_codeerr,size_tbytes_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(autosnd:read_socket_async(socket,span{buff})){
- try{
- if(optional<size_t>bytes_read=
- co_awaitexecution::stopped_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 sender 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 await_transform trickery from a promise type to make them awaitable.)
-
For an example, imagine a coroutine type called task<T> that knows nothing
-about senders. It doesn’t implement any of the sender customization points.
-Despite that fact, and despite the fact that the this_thread::sync_wait algorithm is constrained with the sender concept, the following would compile
-and do what the user wants:
-
task<int>doSomeAsyncWork();
-
-intmain(){
- // OK, awaitable types satisfy the requirements for senders:
- autoo=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 execution::with_awaitable_senders 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 retry algorithm:
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 with_awaitable_senders 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 into_variant sender algorithm -- before co_await-ing that sender.
-
4.18. Cancellation of a sender can unwind a stack of coroutines
-
When looking at the sender-based retry 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 execution::set_stopped?
-
When your task type’s promise inherits from with_awaitable_senders, what happens is this: the coroutine behaves as if an uncatchable exception had been thrown from the co_await 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 with_awaitable_senders, or more generally implement a member function called unhandled_stopped, the exception unwinds the chain of coroutines as if an exception were thrown except that it bypasses catch(...) clauses.
-
In order to "catch" this uncatchable stopped exception, one of the calling coroutines in the stack would have to await a sender that maps the stopped channel into either a value or an error. That is achievable with the execution::let_stopped, execution::upon_stopped, execution::stopped_as_optional, or execution::stopped_as_error sender adaptors. For instance, we can use execution::stopped_as_optional to "catch" the stopped signal and map it into an empty optional as shown below:
-
if(autoopt=co_awaitexecution::stopped_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 connect-ing an awaitable and a receiver, the adaptation layer awaits the awaitable within a coroutine that implements unhandled_stopped in its promise type. The effect of this is that an "uncatchable" stopped exception propagates seamlessly out of awaitables, causing execution::set_stopped to be called on the receiver.
-
Obviously, unhandled_stopped is a library extension of the coroutine promise interface. Many promise types will not implement unhandled_stopped. When an uncatchable stopped exception tries to propagate through such a coroutine, it is treated as an unhandled exception and terminate is called. The solution, as described above, is to use a sender adaptor to handle the stopped exception before awaiting it. It goes without saying that any future Standard Library coroutine types ought to implement unhandled_stopped. The author of Add lazy coroutine (coroutine task) type, 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 execution::par, 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.
This function would return an object of an unspecified 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 policy while using scheduler to create any work to be run. The expression may be ill-formed if scheduler 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 executing_on 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 void and (2) where a customization point pairing a sender with an execution policy would similarly be used to
-obtain an object of unspecified 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.
execution::schedulerautosch1=get_system_thread_pool().scheduler();
-
-execution::senderautosnd1=execution::schedule(sch1);
-// snd1 describes the creation of a new task on the system thread pool
-
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 set_value.
Returns a sender whose value completion scheduler is the provided scheduler, which sends the provided values in the same manner as just.
-
execution::senderautovals=execution::transfer_just(
- get_system_thread_pool().scheduler(),
- 1,2,3
-);
-execution::senderautosnd=execution::then(vals,[](auto...args){
- std::print(args...);
-});
-// when snd is executed, it will print "123"
-
-
This adaptor is included as it greatly simplifies lifting values into senders.
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 set_error. 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 set_error.
-
4.20.5. execution::just_stopped
-
execution::senderautojust_stopped();
-
-
Returns a sender with no completion schedulers, which completes immediately by calling the receiver’s set_stopped.
Returns a sender that reaches into a receiver’s environment and pulls out the current value associated with the customization point denoted by Tag. It then sends the value read back to the receiver through the value channel. For instance, get_scheduler() (with no arguments) is a sender that asks the receiver for the currently suggested scheduler and passes it to the receiver’s set_value completion-signal.
-
This can be useful when scheduling nested dependent work. The following sender pulls the current schduler into the value channel and then schedules more work onto it.
-
execution::senderautotask=
- execution::get_scheduler()
- |execution::let_value([](autosched){
- returnexecution::on(sched,somenestedworkhere);
- });
-
-this_thread::sync_wait(std::move(task));// wait for it to finish
-
-
This code uses the fact that sync_wait associates a scheduler with the receiver that it connects with task. get_scheduler() reads that scheduler out of the receiver, and passes it to let_value's receiver’s set_value function, which in turn passes it to the lambda. That lambda returns a new sender that uses the scheduler to schedule some nested work onto sync_wait's scheduler.
-
4.21. User-facing sender adaptors
-
A sender adaptor is an algorithm that takes one or more senders, which it may execution::connect, 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.22.1 execution::start_detached and § 4.22.2 this_thread::sync_wait start senders.
execution::schedulerautocpu_sched=get_system_thread_pool().scheduler();
-execution::schedulerautogpu_sched=cuda::scheduler();
-
-execution::senderautocpu_task=execution::schedule(cpu_sched);
-// cpu_task describes the creation of a new task on the system thread pool
-
-execution::senderautogpu_task=execution::transfer(cpu_task,gpu_sched);
-// gpu_task describes the transition of the task graph described by cpu_task to the gpu
-
then 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.
-
then is guaranteed to not begin executing function until the returned sender is started.
-
execution::senderautoinput=get_input();
-execution::senderautosnd=execution::then(input,[](auto...args){
- std::print(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.
upon_error and upon_stopped are similar to then, but where then works with values sent by the input sender, upon_error works with errors, and upon_stopped is invoked when the "stopped" signal is sent.
let_value is very similar to then: 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 then sends exactly what that function ends up returning - let_value requires that the function return a sender, and the sender returned by let_value 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.
-
let_value is guaranteed to not begin executing function until the returned sender is started.
-
let_error and let_stopped are similar to let_value, but where let_value works with values sent by the input sender, let_error works with errors, and let_stopped is invoked when the "stopped" signal is sent.
Returns a sender which, when started, will start the provided sender on an execution agent belonging to the execution resource associated with the provided scheduler. This returned sender has no completion schedulers.
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.
Returns a sender that maps the value channel from a T to an optional<decay_t<T>>, and maps the stopped channel to a value of an empty optional<decay_t<T>>.
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 function will begin executing until the returned sender is started. Each invocation of function 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 bulk execute with the same guarantee. The number of execution agents used by bulk is not specified. This allows a scheduler to execute some invocations of the function in parallel.
-
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.
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.
when_all 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 set_value 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 when_all. It completes inline on the execution resource on which the last input sender completes, unless stop is requested before when_all is started, in which case it completes inline within the call to start.
-
when_all_with_variant does the same, but it adapts all the input senders using into_variant, and so it does not constrain the input arguments as when_all does.
execution::schedulerautosched=thread_pool.scheduler();
-
-execution::senderautosends_1=...;
-execution::senderautosends_abc=...;
-
-execution::senderautoboth=execution::when_all(sched,
- sends_1,
- sends_abc
-);
-
-execution::senderautofinal=execution::then(both,[](auto...args){
- std::cout<<std::format("the two args: {}, {}",args...);
-});
-// when final executes, it will print "the two args: 1, abc"
-
Once ensure_started returns, it is known that the provided sender has been connected and start 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 resources. 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 execution::connect() is called, or if execution::connect() is called but the
-returned operation-state is destroyed before execution::start() 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 std::shared_ptr 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 execution::connect, as parameters, and does not return a sender.
this_thread::sync_wait is a sender consumer that submits the work described by the provided sender for execution, similarly to ensure_started, except that it blocks the current std::thread or thread of main 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, sync_wait 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, sync_wait throws that error as an exception, or rethrows the original exception if the error is of type std::exception_ptr.
-
If the provided sender sends the "stopped" signal instead of values, sync_wait returns an empty optional.
-
For an explanation of the requires clause, see § 5.8 All senders are typed. That clause also explains another sender consumer, built on top of sync_wait: sync_wait_with_variant.
-
Note: This function is specified inside std::this_thread, and not inside execution. This is because sync_wait 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 std::this_thread, 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 std::this_fiber::sync_wait would be provided. We also expect that runtimes with execution agents that use different synchronization mechanisms than std::thread's will provide their own flavors of sync_wait 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.
A receiver is a callback that supports more than one channel. In fact, it supports three of them:
-
-
-
set_value, which is the moral equivalent of an operator() or a function
-call, which signals successful completion of the operation its execution
-depends on;
-
-
set_error, 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; and
-
-
set_stopped, which signals that the operation completed without succeeding
-(set_value) and without failing (set_error). 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.
-
-
Once an async operation has been started exactly one of these functions must be invoked
-on a receiver before it is destroyed.
-
While the receiver interface may look novel, it is in fact very similar to the
-interface of std::promise, which provides the first two signals as set_value and set_exception, 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.
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: start, 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 execution::ensure_started and this_thread::sync_wait, 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.
execution::connect is a customization point which connects senders with receivers, resulting in an operation state that will ensure that if start is called that one of the completion operations will be called on the receiver passed to connect.
-
execution::senderautosnd=someinputsender;
-execution::receiverautorcv=somereceiver;
-execution::operation_stateautostate=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 resource, and that execution resource will eventually call one of the
-// completion operations on 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 then, 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 execution::then(sender,invocable) to be equivalent to:
-
-
-
sender.then(invocable), if that expression is well-formed; otherwise
-
-
then(sender,invocable), performed in a context where this call always performs ADL, if that expression is well-formed; otherwise
-
-
a default implementation of then, which returns a sender adaptor, and then define the exact semantics of said adaptor.
-
-
However, this definition is problematic. Imagine another sender adaptor, bulk, 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 bulk to have specialized behavior, which invokes a kernel of more than one thread (with its size defined by the call to bulk); therefore, we would like to customize bulk for CUDA senders to achieve this. However, there’s no reason for CUDA kernels to
-necessarily customize the then sender adaptor, as the generic implementation is perfectly sufficient. This creates a problem, though; consider the following snippet:
-
execution::schedulerautocuda_sch=cuda_scheduler{};
-
-execution::senderautoinitial=execution::schedule(cuda_sch);
-// the type of initial is a type defined by the cuda_scheduler
-// let’s call it cuda::schedule_sender<>
-
-execution::senderautonext=execution::then(cuda_sch,[]{return1;});
-// the type of next is a standard-library unspecified sender adaptor
-// that wraps the cuda sender
-// let’s call it execution::then_sender_adaptor<cuda::schedule_sender<>>
-
-execution::senderautokernel_sender=execution::bulk(next,shape,[](inti){...});
-
-
How can we specialize the bulk sender adaptor for our wrapped schedule_sender? 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):
However, if the input sender is not just a then_sender_adaptor like in the example above, but another sender that overrides bulk 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 execution::<sender-algorithm>(sender,args...), for any given sender algorithm that accepts a sender as its first argument, should be
-equivalent to:
-
-
-
tag_invoke(<sender-algorithm>,get_completion_scheduler<Tag>(get_env(sender)),sender,args...), if that expression is well-formed; otherwise
-
-
tag_invoke(<sender-algorithm>,sender,args...), if that expression is well-formed; otherwise
-
-
a default implementation, if there exists a default implementation of the given sender algorithm.
-
-
where Tag is one of set_value, set_error, or set_stopped. For most sender algorithms, the completion scheduler for set_value would be used, but for some (like upon_error or let_stopped), one of the others would be used.
-
For sender algorithms which accept concepts other than sender as their first argument, we propose that the customization scheme remains as it has been in A Unified Executors Proposal for C++ so far, except it should also use tag_invoke.
-
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 resource before a receiver is connected to a sender, and execution::start 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 resource, 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 resource. 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 resources 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 resource transitions are two-step
-
Because execution::transfer 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 resource, 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 transfer must be controllable by the target scheduler. Besides, the target
-scheduler may itself represent a specialized execution resource, 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 resources
-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 transfer.
-
To allow for such customization from both ends, we propose the inclusion of a secondary transitioning sender adaptor, called schedule_from. This adaptor is a form of schedule, 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 transfer, to ensure that both schedulers have a say in how the transitions are made. Any scheduler that specializes transfer(snd,sch) shall ensure that the
-return value of their customization is equivalent to schedule_from(sch,snd2), where snd2 is a successor of snd that sends values equivalent to those sent by snd.
-
The default implementation of transfer(snd,sched) is schedule_from(sched,snd).
-
5.8. All senders are typed
-
All senders must 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 execution::then is hard.
-
The mechanism for this advertisement is similar to the one in A Unified Executors Proposal for C++; the
-way to query the types is through completion_signatures_of_t<S,[Env]>::value_types<tuple_like,variant_like>.
-
completion_signatures_of_t::value_types 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 when_all). 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 std::optional to handle the set_stopped signal. However, this assumes that those values can be represented as a tuple,
-like here:
-
execution::senderautosends_1=...;
-execution::senderautosends_2=...;
-execution::senderautosends_3=...;
-
-auto[a,b,c]=this_thread::sync_wait(
- execution::transfer_when_all(
- execution::get_completion_scheduler<execution::set_value_t>(get_env(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 value_types of a sender which sends Types... to be as follows:
If senders could only ever send one specific set of values, this would probably need to be the required form of value_types 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) set_value 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, A Unified Executors Proposal for C++ also includes a second template parameter to value_types, one that represents a variant-like type. If we permit such senders, we would almost certainly need to require that the canonical form of value_types 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 Types1..., Types2..., ..., TypesN... to be as follows:
This, however, introduces a couple of complications:
-
-
-
A just(1) sender would also need to follow this structure, so the correct type for storing the value sent by it would be std::variant<std::tuple<int>> 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 where value_types 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.
-
-
As a consequence of (1): because sync_wait needs to store the above type, it can no longer return just a std::tuple<int> for just(1); it has to return std::variant<std::tuple<int>>. 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 of sync_wait must be the same across all sender types.
-
-
One possible solution to (2) above is to place a requirement on sync_wait that it can only accept senders which send only a single set of values, therefore removing the need for std::variant to appear in its API; because of this, we propose to expose both sync_wait, which is a simple, user-friendly version of the sender consumer, but requires that value_types have only one possible variant, and sync_wait_with_variant, which accepts any sender, but returns an optional whose value type is the variant of all the
-possible tuples sent by the input sender:
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 (A Unified Executors Proposal for C++ and Towards C++23 executors: A proposal for an initial set of algorithms) 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: sync_wait. We imagine that if, in the future, C++ was to
-gain fibers support, we would want to also have std::this_fiber::sync_wait, in addition to std::this_thread::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.
In short, instead of using globally reserved names, tag_invoke uses the type of the customization point object itself as the mechanism to find customizations. It globally reserves only a single name - tag_invoke - which itself is used the same way that
-ranges-style customization points are used. All other customization points are defined in terms of tag_invoke. For example, the customization for std::this_thread::sync_wait(s) will call tag_invoke(std::this_thread::sync_wait,s), instead of attempting
-to invoke s.sync_wait(), and then sync_wait(s) if the member call is not valid.
-
Using tag_invoke 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<typenameTag,typename...Args>
-friendautotag_invoke(Tag&&tag,wrapper&self,Args&&...args){
- returnstd::forward<Tag>(tag)(self.subobject,std::forward<Args>(args)...);
-}
-
-// but override one of them with a specific value
-friendautotag_invoke(specific_customization_point_t,wrapper&self){
- returnself.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 A Unified Executors Proposal for C++ 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 tag_invoke, dispatching to manually constructed vtables containing pointers to specialized implementations for the wrapped objects. For an example of such a polymorphic wrapper, see unifex::any_unique (example).
7.1.1.1. The std::terminate function [except.terminate]
-
At the end of the bulleted list in the Note in paragraph 1, add a new bullet as follows:
-
-
-
-
-
when a callback invocation exits via an exception when requesting stop on a std::stop_source or a std::in_place_stop_source ([stopsource.mem], [stopsource.inplace.mem]), or in
-the constructor of std::stop_callback or std::in_place_stop_callback ([stopcallback.cons], [stopcallback.inplace.cons]) when a callback invocation exits
-via an exception.
-
-
-
-
8. Library introduction [library]
-
- Add the header <execution> to Table 23: C++ library headers [tab:headers.cpp]
-
In subclause [conforming], after [lib.types.movedfrom], add the following new subclause with suggested stable name [lib.tmpl-heads].
-
-
-
-
- 16.4.6.17 Class template-heads
-
-
-
If a class template’s template-head is marked with "arguments are not
-associated entities"", any template arguments do not contribute to the
-associated entities ([basic.lookup.argdep]) of a function call where a
-specialization of the class template is an associated entity. In such a case,
-the class template can be implemented as an alias template referring to a
-templated class, or as a class template where the template arguments
-themselves are templated classes.
-
-
[Example:
-
template<classT>// arguments are not associated entities
-structS{};
-
-namespaceN{
- intf(auto);
- structA{};
-}
-
-intx=f(S<N::A>{});// error: N::f not a candidate
-
-
The template S specified above can be implemented as
Insert this section as a new subclause, between Searchers [func.search] and Class template hash[unord.hash].
-
-
-
-
-
Given a subexpression E, let REIFY(E) be expression-equivalent to
-a glvalue with the same type and value as E as if by identity()(E).
-
-
The name std::tag_invoke denotes a customization point object [customization.point.object].
-Given subexpressions T and A..., the expression std::tag_invoke(T,A...) is
-expression-equivalent [defns.expression-equivalent] to tag_invoke(REIFY(T),REIFY(A)...) with overload resolution performed in a context in which unqualified lookup for tag_invoke finds only the declaration
-
voidtag_invoke();
-
-
-
[Note: Diagnosable ill-formed cases above result in substitution failure when std::tag_invoke(T,A...) appears in the immediate context of a template instantiation. —end note]
Insert this section as a new subclause between Header <stop_token> synopsis [thread.stoptoken.syn] and Class stop_token[stoptoken].
-
-
-
-
-
The stoppable_token concept checks for the basic interface of a stop token
-that is copyable and allows polling to see if stop has been requested and
-also whether a stop request is possible. For a stop token type T and a type CB that is callable with no arguments, the type T::callback_type<CB> is
-valid and denotes 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. The stoppable_token_for concept checks for a stop token type compatible
-with a given callback type. The unstoppable_token concept checks for a stop
-token type that does not allow stopping.
- LWG directed me to replace T::stop_possible() with t.stop_possible() because
-of the recent constexpr changes in P2280R2. However, even with those changes, a nested
-requirement like requires(!t.stop_possible()), where t is an argument in the requirement-parameter-list, is ill-formed according to [expr.prim.req.nested/p2]:
-
-
A local parameter shall only appear as an unevaluated operand within the constraint-expression.
Let t and u be distinct, valid objects of type T. The type T models stoppable_token only if:
-
-
-
If t.stop_possible() evaluates to false then, if t and u reference the same logical shared stop state, u.stop_possible() shall also subsequently evaluate to false and u.stop_requested() shall also subsequently evaluate to false.
-
-
If t.stop_requested() evaluates to true then, if t and u reference the same logical shared stop state, u.stop_requested() shall also subsequently evaluate to true and u.stop_possible() shall also subsequently evaluate to true.
-
-
-
Let t and u be distinct, valid objects of type T and let init be an
-object of type Initializer. Then for some type CB, the type T models stoppable_token_for<CB,Initializer> only if:
Direct non-list initializing an object cb of type T::callback_type<CB> from t,init shall, if t.stop_possible() is true, construct an
-instance, callback, of type CB, direct-initialized with init,
-and register callback with t'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.
-
-
-
If t.stop_requested() evaluates to true at the time callback is
-registered then callback can be invoked on the thread executing cb's constructor.
-
-
If callback is invoked then, if t and u reference the same shared stop
-state, an evaluation of u.stop_requested() will be true if the beginning of the invocation of callback strongly-happens-before the evaluation of u.stop_requested().
-
-
[Note: If t.stop_possible() evaluates to false then the construction of cb is not required to construct and initialize callback. --end note]
-
-
-
Construction of a T::callback_type<CB> instance shall only throw exceptions thrown by the initialization of the CB instance from the value of type Initializer.
-
-
Destruction of the T::callback_type<CB> object, cb, removes callback from the shared stop state such that callback will not be invoked after the destructor returns.
-
-
-
If callback is currently being invoked on another thread then the destructor of cb will block until the invocation of callback returns such that the return from the invocation of callback strongly-happens-before the destruction of callback.
-
-
Destruction of a callback cb shall not block on the completion of the invocation of some other callback registered with the same shared stop state.
-
-
-
-
-
-
10.1.3. Class stop_token[stoptoken]
-
10.1.3.1. General [stoptoken.general]
-
Modify the synopsis of class stop_token in section General [stoptoken.general] as follows:
Insert a new subclause, Class never_stop_token[stoptoken.never], after section Class template stop_callback[stopcallback], as a new subclause of Stop tokens [thread.stoptoken].
-
10.1.4.1. General [stoptoken.never.general]
-
-
-
The class never_stop_token provides an implementation of the unstoppable_token concept. It provides a stop token interface, but also provides static information that a stop is never possible nor requested.
10.1.5. Class in_place_stop_token[stoptoken.inplace]
-
Insert a new subclause, Class in_place_stop_token[stoptoken.inplace], after the section added above, as a new subclause of Stop tokens [thread.stoptoken].
-
10.1.5.1. General [stoptoken.inplace.general]
-
-
-
The class in_place_stop_token provides an interface for querying whether a stop request has been made (stop_requested) or can ever be made (stop_possible) using an associated in_place_stop_source object ([stopsource.inplace]).
-An in_place_stop_token can also be passed to an in_place_stop_callback ([stopcallback.inplace]) constructor to register a callback to be called when a stop request has been made from an associated in_place_stop_source.
10.1.5.2. Constructors, copy, and assignment [stoptoken.inplace.cons]
-
in_place_stop_token()noexcept;
-
-
-
-
Effects: initializes source_ with nullptr.
-
-
voidswap(stop_token&rhs)noexcept;
-
-
-
-
Effects: Exchanges the values of source_ and rhs.source_.
-
-
10.1.5.3. Members [stoptoken.inplace.mem]
-
[[nodiscard]]boolstop_requested()constnoexcept;
-
-
-
-
Effects: Equivalent to: returnsource_!=nullptr&&source_->stop_requested();
-
-
[Note: The behavior of stop_requested() is undefined unless the call
-strongly happens before the start of the destructor of the associated in_place_stop_source, if any ([basic.life]). --end note]
-
-
[[nodiscard]]boolstop_possible()constnoexcept;
-
-
-
-
Effects: Equivalent to: returnsource_!=nullptr;
-
-
[Note: The behavior of stop_possible() is implementation-defined unless
-the call strongly happens before the end of the storage duration of the
-associated in_place_stop_source object, if any ([basic.stc.general]). --end note]
10.1.6. Class in_place_stop_source[stopsource.inplace]
-
Insert a new subclause, Class in_place_stop_source[stopsource.inplace], after the section added above, as a new subclause of Stop tokens [thread.stoptoken].
-
10.1.6.1. General [stopsource.inplace.general]
-
-
-
The class in_place_stop_source implements the semantics of making a stop request, without the need for a dynamic allocation of a shared state.
-A stop request made on a in_place_stop_source object is visible to all associated in_place_stop_token ([stoptoken.inplace]) objects.
-Once a stop request has been made it cannot be withdrawn (a subsequent stop request has no effect).
-All uses of in_place_stop_token objects associated with a given in_place_stop_source object must happen before the start of the destructor of that in_place_stop_source object.
An instance of in_place_stop_source maintains a list of registered callback invocations.
-The registration of a callback invocation either succeeds or fails. When an invocation
-of a callback is registered, the following happens atomically:
-
-
-
The stop state is checked. If stop has not been requested, the callback invocation is
-added to the list of registered callback invocations, and registration has succeeded.
-
-
Otherwise, registration has failed.
-
-
When an invocation of a callback is unregistered, the invocation is atomically removed
-from the list of registered callback invocations. The removal is not blocked by the concurrent
-execution of another callback invocation in the list. If the callback invocation
-being unregistered is currently executing, then:
-
-
-
If the execution of the callback invocation is happening concurrently on another thread,
-the completion of the execution strongly happens before ([intro.races]) the end of the
-callback’s lifetime.
-
-
Otherwise, the execution is happening on the current thread. Removal of the
-callback invocation does not block waiting for the execution to complete.
-
-
-
10.1.6.2. Constructors, copy, and assignment [stopsource.inplace.cons]
-
in_place_stop_source()noexcept;
-
-
-
-
Effects: Initializes a new stop state inside *this.
Returns: A new associated in_place_stop_token object.
-
-
[[nodiscard]]boolstop_requested()constnoexcept;
-
-
-
-
Returns: true if the stop state inside *this has received a stop request; otherwise, false.
-
-
boolrequest_stop()noexcept;
-
-
-
-
Effects: Atomically determines whether the stop state inside *this has received a stop request, and if not, makes a stop request.
-The determination and making of the stop request are an atomic read-modify-write operation ([intro.races]).
-If the request was made, the registered invocations are executed and the evaluations of the invocations are indeterminately sequenced.
-If an invocation of a callback exits via an exception then terminate is invoked ([except.terminate]).
-
-
Postconditions: stop_requested() is true.
-
-
Returns: true if this call made a stop request; otherwise false.
-
-
10.1.7. Class template in_place_stop_callback[stopcallback.inplace]
-
Insert a new subclause, Class template in_place_stop_callback[stopcallback.inplace], after the section added above, as a new subclause of Stop tokens [thread.stoptoken].
Mandates: in_place_stop_callback is instantiated with an argument for the template parameter Callback that satisfies both invocable and destructible.
-
-
Preconditions: in_place_stop_callback is instantiated with an argument for the template parameter Callback that models both invocable and destructible.
-
-
Recommended practice: Implementations should use the storage of the in_place_stop_callback objects to store the state necessary for their association with an in_place_stop_source object.
-
-
10.1.7.2. Constructors and destructor [stopcallback.inplace.cons]
Constraints: Callback and C satisfy constructible_from<Callback,C>.
-
-
Preconditions: Callback and C model constructible_from<Callback,C>.
-
-
Effects: Initializes callback_ with std::forward<C>(cb).
-Any in_place_stop_source associated with st becomes associated with *this.
-Registers ([stopsource.inplace.general]) the callback invocation std::forward<Callback>(callback_)() with the associated in_place_stop_source, if any. If the registration fails, evaluates
-the callback invocation.
-
-
Throws: Any exception thrown by the initialization of callback_.
-
-
Remarks: If evaluating std::forward<Callback>(callback_)() exits via an exception, then terminate is invoked ([except.terminate]).
-
-
~in_place_stop_callback();
-
-
-
-
Effects: Unregisters ([stopsource.inplace.general]) the callback invocation from
-the associated in_place_stop_source object, if any.
-
-
Remarks: A program has undefined behavior if the start of this destructor does
-not strongly happen before the start of the destructor of the associated in_place_stop_source object, if any.
-
-
11. Execution control library [exec]
-
11.1. General [exec.general]
-
-
-
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.
-
-
-
Table N: Execution control library summary [tab:execution.summary]
[Note: A large number of execution control primitives are
-customization point objects. For an object one might define multiple types of
-customization point objects, for which different rules apply. Table 2 shows
-the types of customization point objects used in the execution control
-library:
-
-
-
Table N+1: Types of customization point objects in the execution control library [tab:execution.cpos]
-
-
-
Customization point object type
-
Purpose
-
Examples
-
-
core
-
provide core execution functionality, and connection between core components
-
connect, start, execute
-
-
completion functions
-
called by senders to announce the completion of the work (success, error, or cancellation)
-
For function types F1 and F2 denoting R1(Args1...) and R2(Args2...) respectively, MATCHING-SIG(F1,F2) is true if and only if same_as<R1(Args&&...),R2(Args2&&...)> is true.
-
-
-
11.2. Queries and queryables [exec.queryable]
-
11.2.1. General [exec.queryable.general]
-
-
-
A queryable object is a read-only collection of
-key/value pairs where each key is a customization point object known as a query object. A query is an invocation of a query object with a queryable
-object as its first argument and a (possibly empty) set of additional
-arguments. The result of a query expression is valid as long as the
-queryable object is valid. A query imposes syntactic
-and semantic requirements on its invocations.
-
-
Given a subexpression e that refers to a queryable object q, a query
-object F, and a (possibly empty) pack of subexpressions args, the expression F(e,args...) is equal to
-([concepts.equality]) the expression F(c,args...) where c is a const lvalue reference to q.
-
-
The type of a query expression can not be void.
-
-
The expression F(e,args...) is equality-preserving
-([concepts.equality]) and does not modify the function object or the
-arguments.
-
-
Unless otherwise specified, the value returned by the expression F(e,args...) is valid as long as e is valid.
The queryable concept specifies the constraints on the types of queryable
-objects.
-
-
Let e be an object of type E. The type E models queryable if for each
-callable object F and a pack of subexpressions args,
-if requires{F(e,args...)} is true then F(e,args...) meets any semantic requirements imposed by F.
-
-
11.3. Asynchronous operations [async.ops]
-
-
-
An execution resource is a program entity that manages
-a (possibly dynamic) set of execution agents
-([thread.req.lockable.general]), which it uses to execute parallel work on
-behalf of callers. [Example 1: The currently active thread, a
-system-provided thread pool, and uses of an API associated with an external
-hardware accelerator are all examples of execution resources. -- end
-example] Execution resources execute asynchronous operations. An execution
-resource is either valid or invalid.
-
-
An asynchronous operation is a distinct unit of
-program execution that:
-
-
-
is explicitly created;
-
-
can be explicitly started; an
- asynchronous operation can be started once at most;
-
-
if started, eventually completes with a (possibly empty) set of result datums, and in exactly one of
- three modes: success, failure, or cancellation, known as the
- operation’s disposition; an asychronous
- operation can only complete once; a successful completion, also known
- as a value completion, can have an arbitrary
- number of result datums; a failure completion, also known as an error completion, has a single result datum; a
- cancellation completion, also known as a stopped
- completion, has no result datum; an asynchronous operation’s async result is its disposition and its
- (possibly empty) set of result datums.
-
-
can complete on a different execution resource than that on which it
- started; and
-
-
can create and start other asychronous operations called child operations. A child operation is an
- asynchronous operation that is created by the parent operation and, if
- started, completes before the parent operation completes. A parent operation is the asynchronous operation that
- created a particular child operation.
-
-
An asynchronous operation can in fact execute
-synchronously; that is, it can complete during the execution of its start
-operation on the thread of execution that started it.
-
-
An asynchronous operation has associated state known as its operation state.
-
-
An asynchronous operation has an associated environment. An environment is a queryable object ([exec.queryable])
-representing the execution-time properties of the operation’s caller. The caller of an asynchronous operation is
-its parent operation or the function that created it. An asynchronous
-operation’s operation state owns the operation’s environment.
-
-
An asynchronous operation has an associated receiver. A receiver is an aggregation of three handlers for the three
-asynchronous completion dispositions: a value completion handler for a value
-completion, an error completion handler for an error completion, and a
-stopped completion handler for a stopped completion. A receiver has an
-associated environment. An asynchronous operation’s operation state owns the
-operation’s receiver. The environment of an asynchronous operation is equal
-to its receiver’s environment.
-
-
For each completion disposition, there is a completion
-function. A completion function is a customization point object
-([customization.point.object]) that accepts an asynchronous operation’s
-receiver as the first argument and the result datums of the asynchronous
-operation as additional arguments. The value completion function invokes the
-receiver’s value completion handler with the value result datums; likewise
-for the error completion function and the stopped completion function. A
-completion function has an associated type known as its completion tag that names the unqualified type of the
-completion function. A valid invocation of a completion function is called a completion operation.
-
-
The lifetime of an
-asynchronous operation, also known as the operation’s async lifetime, begins when its start operation begins
-executing and ends when its completion operation begins executing. If the
-lifetime of an asynchronous operation’s associated operation state ends
-before the lifetime of the asynchronous operation, the behavior is
-undefined. After an asynchronous operation executes a completion operation,
-its associated operation state is invalid. Accessing any part of an invalid
-operation state is undefined behavior.
-
-
An asynchronous operation shall not execute a completion operation before its
-start operation has begun executing. After its start operation has begun
-executing, exactly one completion operation shall execute. The lifetime of an
-asynchronous operation’s operation state can end during the execution of the
-completion operation.
-
-
A sender is a factory for one or more asynchronous
-operations. Connecting a sender and a
-receiver creates an asynchronous operation. The asynchronous operation’s
-associated receiver is equal to the receiver used to create it, and its
-associated environment is equal to the environment associated with the
-receiver used to create it. The lifetime of an asynchronous operation’s
-associated operation state does not depend on the lifetimes of either the
-sender or the receiver from which it was created. A sender sends its results by way of the asynchronous operation(s)
-it produces, and a receiver receives those results. A sender is either valid or invalid; it becomes invalid
-when its parent sender (see below) becomes invalid.
-
-
A scheduler is an abstraction of an execution
-resource with a uniform, generic interface for scheduling work onto that
-resource. It is a factory for senders whose asynchronous operations execute
-value completion operations on an execution agent belonging to the
-scheduler’s associated execution resource. A schedule-expression obtains such a sender from a
-scheduler. A schedule sender is the result of a
-schedule expression. On success, an asynchronous operation produced by a
-schedule sender executes a value completion operation with an empty set of
-result datums. Multiple schedulers can refer to the same execution resource.
-A scheduler can be valid or invalid. A scheduler becomes invalid when the
-execution resource to which it refers becomes invalid, as do any schedule
-senders obtained from the scheduler, and any operation states obtained from
-those senders.
-
-
An asynchronous operation has one or more associated completion schedulers
-for each of its possible dispositions. A completion
-scheduler is a scheduler whose associated execution resource is used
-to execute a completion operation for an asynchronous operation. A value
-completion scheduler is a scheduler on which an asynchronous operation’s
-value completion operation can execute. Likewise for error completion
-schedulers and stopped completion schedulers.
-
-
A sender has an associated queryable object ([exec.queryable]) known as its attributes that describes various characteristics of
-the sender and of the asynchronous operation(s) it produces. For each
-disposition, there is a query object for reading the associated completion
-scheduler from a sender’s attributes; i.e., a value completion scheduler
-query object for reading a sender’s value completion scheduler, etc. If a
-completion scheduler query is well-formed, the returned completion scheduler
-is unique for that disposition for any asynchronous operation the sender
-creates. A schedule sender is required to have a value completion scheduler
-attribute whose value is equal to the scheduler that produced the schedule
-sender.
-
-
A completion signature is a function type that
-describes a completion operation. An asychronous operation has a finite set
-of possible completion signatures. The completion signature’s return type is
-the completion tag associated with the completion function that executes the
-completion operation. The completion signature’s argument types are the
-types and value categories of the asynchronous operation’s result datums.
-Together, a sender type and an environment type E determine the set of
-completion signatures of an operation state that results from connecting the
-sender with a receiver whose environment has type E. The type of the receiver does not affect an asychronous
-operation’s completion signatures, only the type of the receiver’s
-environment.
-
-
A sender algorithm is a function that takes and/or
-returns a sender. There are three categories of sender algorithms:
-
-
-
A sender factory is a function that takes
-non-senders as arguments and that returns a sender.
-
-
A sender adaptor is a function that constructs and
-returns a parent sender from a set of one or more child senders and a
-(possibly empty) set of additional arguments. An asynchronous operation
-created by a parent sender is a parent to the
-child operations created by the child
-senders.
-
-
A sender consumer is a function that takes one or
-more senders and a (possibly empty) set of additional arguments, and
-whose return type is not the type of a sender.
The exposition-only type variant-or-empty<Ts...> is
- defined as follows:
-
-
-
If sizeof...(Ts) is greater than zero, variant-or-empty<Ts...> names the type variant<Us...> where Us... is the pack decay_t<Ts>... with
-duplicate types removed.
-
-
Otherwise, variant-or-empty<Ts...> names the
-exposition-only class type:
get_env is a customization point object. For some subexpression o of type O, get_env(o) is expression-equivalent to
-
-
-
tag_invoke(std::get_env,const_cast<constO&>(o)) if that expression is
-well-formed.
-
-
-
Mandates: The type of the expression above satisfies queryable ([exec.queryable]).
-
-
-
Otherwise, empty_env{}.
-
-
-
The value of get_env(o) shall be valid while o is valid.
-
-
When passed a sender object, get_env returns the sender’s attributes. When
-passed a receiver, get_env returns the receiver’s environment.
-
-
11.5.2. std::forwarding_query[exec.fwd.env]
-
-
-
std::forwarding_query asks a query object whether it should be forwarded
-through queryable adaptors.
-
-
The name std::forwarding_query denotes a query object. For some query
-object q of type Q, std::forwarding_query(q) is expression-equivalent
-to:
-
-
-
mandate-nothrow-call(tag_invoke,std::forwarding_query,q) if that expression is well-formed.
-
-
-
Mandates: The expression above has type bool and is a core
-constant expressions if q is a core constant expression.
-
-
-
Otherwise, true if derived_from<Q,std::forwarding_query_t> is true.
-
-
Otherwise, false.
-
-
-
For a queryable object o, let FWD-QUERIES(o) be a
-queryable object such that for a query object q and a pack of
-subexpressions as, the expression q(FWD-QUERIES(o),as...) is ill-formed if forwarding_query(q) is false; otherwise,
-it is expression-equivalent to q(o,as...).
-
-
11.5.3. std::get_allocator[exec.get.allocator]
-
-
-
get_allocator asks an object for its associated allocator.
-
-
The name get_allocator denotes a query object. For some subexpression r, get_allocator(r) is expression-equivalent to mandate-nothrow-call(tag_invoke,std::get_allocator,as_const(r)).
-
-
-
Mandates: The type of the expression above
-satisfies Allocator.
-
-
-
std::forwarding_query(std::get_allocator) is true.
-
-
get_allocator() (with no arguments) is expression-equivalent to execution::read(std::get_allocator) ([exec.read]).
-
-
11.5.4. std::get_stop_token[exec.get.stop.token]
-
-
-
get_stop_token asks an object for an associated stop token.
-
-
The name get_stop_token denotes a query object. For some subexpression r, get_stop_token(r) is expression-equivalent to:
-
-
-
mandate-nothrow-call(tag_invoke,std::get_stop_token,as_const(r)), if this expression is well-formed.
-
-
-
Mandates: The type of the expression above satisfies stoppable_token.
-
-
-
Otherwise, never_stop_token{}.
-
-
-
std::forwarding_query(std::get_stop_token) is true.
-
-
get_stop_token() (with no arguments) is expression-equivalent to execution::read(std::get_stop_token) ([exec.read]).
get_scheduler asks an object for its associated scheduler.
-
-
The name get_scheduler denotes a query object. For some
-subexpression r, get_scheduler(r) is expression-equivalent to mandate-nothrow-call(tag_invoke,get_scheduler,as_const(r)).
-
-
-
Mandates: The type of the expression above satisfies scheduler.
-
-
-
std::forwarding_query(std::get_scheduler) is true.
-
-
get_scheduler() (with no arguments) is expression-equivalent to execution::read(get_scheduler) ([exec.read]).
get_delegatee_scheduler asks an object for a scheduler that can be used to delegate work to for the purpose of forward progress delegation.
-
-
The name get_delegatee_scheduler denotes a query object. For some
-subexpression r, get_delegatee_scheduler(r) is expression-equivalent to mandate-nothrow-call(tag_invoke,get_delegatee_scheduler,as_const(r)).
-
-
-
Mandates: The type of the expression above is satisfies scheduler.
-
-
-
std::forwarding_query(std::get_delegatee_scheduler) is true.
-
-
get_delegatee_scheduler() (with no arguments) is expression-equivalent to execution::read(get_delegatee_scheduler) ([exec.read]).
get_forward_progress_guarantee asks a scheduler about the forward progress guarantees of execution agents created by that scheduler.
-
-
The name get_forward_progress_guarantee denotes a query object. For some subexpression s, let S be decltype((s)). If S does not satisfy scheduler, get_forward_progress_guarantee is ill-formed.
-Otherwise, get_forward_progress_guarantee(s) is expression-equivalent to:
-
-
-
mandate-nothrow-call(tag_invoke,get_forward_progress_guarantee,as_const(s)), if this expression is well-formed.
-
-
-
Mandates: The type of the expression above is forward_progress_guarantee.
If get_forward_progress_guarantee(s) for some scheduler s returns forward_progress_guarantee::concurrent, all execution agents created by that scheduler shall provide the concurrent forward progress guarantee. If it returns forward_progress_guarantee::parallel, all execution agents created by that scheduler shall provide at least the parallel forward progress guarantee.
this_thread::execute_may_block_caller asks a scheduler s whether a call execute(s,f) with any invocable f may block the thread where such a call occurs.
-
-
The name this_thread::execute_may_block_caller denotes a query object. For some subexpression s, let S be decltype((s)). If S does not satisfy scheduler, this_thread::execute_may_block_caller is ill-formed. Otherwise, this_thread::execute_may_block_caller(s) is expression-equivalent to:
-
-
-
mandate-nothrow-call(tag_invoke,this_thread::execute_may_block_caller,as_const(s)), if this expression is well-formed.
-
-
-
Mandates: The type of the expression above is bool.
-
-
-
Otherwise, true.
-
-
-
If this_thread::execute_may_block_caller(s) for some scheduler s returns false, no execute(s,f) call with some invocable f shall block the calling thread.
get_completion_scheduler<completion-tag> obtains the
-completion scheduler associated with a completion tag from a sender’s
-attributes.
-
-
The name get_completion_scheduler denotes a query object template. For some
-subexpression q, let Q be decltype((q)). If the template argument Tag in get_completion_scheduler<Tag>(q) is not one of set_value_t, set_error_t, or set_stopped_t, get_completion_scheduler<Tag>(q) is
-ill-formed. Otherwise, get_completion_scheduler<Tag>(q) is
-expression-equivalent to mandate-nothrow-call(tag_invoke,get_completion_scheduler,as_const(q)) if this expression is
-well-formed.
-
-
-
Mandates: The type of the expression above satisfies scheduler.
-
-
-
If, for some sender s and completion function C that has an associated
-completion tag Tag, get_completion_scheduler<Tag>(get_env(s)) is
-well-formed and results in a scheduler sch, and the sender s invokes C(r,args...), for some receiver r that has been connected to s, with
-additional arguments args..., on an execution agent that does not
-belong to the associated execution resource of sch, the behavior is
-undefined.
-
-
The expression forwarding_query(get_completion_scheduler<CPO>) has value true.
-
-
11.6. Schedulers [exec.sched]
-
-
-
The scheduler concept defines the requirements of a scheduler type
-([async.ops]). schedule is a customization point object that accepts a
-scheduler. A valid invocation of schedule is a schedule-expression.
Let S be the type of a scheduler and let E be the type of an execution
-environment for which sender_in<schedule_result_t<S>,E> is true. Then sender_of<schedule_result_t<S>,set_value_t(),E> shall be true.
-
-
None of a scheduler’s copy constructor, destructor, equality comparison, or swap member functions shall exit via an exception.
-
-
None of these member functions, nor a scheduler type’s schedule function,
-shall introduce data races as a result of concurrent invocations of those
-functions from different threads.
-
-
For any two (possibly const) values s1 and s2 of some scheduler type S, s1==s2 shall return true only if both s1 and s2 share the
-same associated execution resource.
-
-
For a given scheduler expression s, the expression get_completion_scheduler<set_value_t>(std::get_env(schedule(s))) shall
-compare equal to s.
-
-
A scheduler type’s destructor shall not block pending completion of any
-receivers connected to the sender objects returned from schedule. The ability to wait for completion of submitted function
-objects can be provided by the associated execution resource of the
-scheduler.
-
-
11.7. Receivers [exec.recv]
-
11.7.1. Receiver concepts [exec.recv.concepts]
-
-
-
A receiver represents the continuation of an asynchronous operation. The receiver concept defines the requirements for a receiver type
-([async.ops]). The receiver_of concept defines the requirements for a
-receiver type that is usable as the first argument of a set of completion
-operations corresponding to a set of completion signatures. The get_env customization point is used to access a receiver’s associated environment.
Remarks: Pursuant to [namespace.std], users can specialize enable_receiver to true for cv-unqualified program-defined types that model receiver, and false for types that do not. Such specializations shall be usable in constant
-expressions ([expr.const]) and have type constbool.
-
-
Let r be a receiver and let op_state be an operation state associated
-with an asynchronous operation created by connecting r with a sender. Let token be a stop token equal to get_stop_token(get_env(r)). token shall
-remain valid for the duration of the asynchronous operation’s lifetime
-([async.ops]). This means that, unless it knows about
-further guarantees provided by the type of receiver r, the implementation
-of op_state can not use token after it executes a completion operation.
-This also implies that any stop callbacks registered on token must be
-destroyed before the invocation of the completion operation.
-
-
11.7.2. execution::set_value[exec.set.value]
-
-
-
set_value is a value completion function ([async.ops]). Its associated
-completion tag is set_value_t. The expression set_value(R,Vs...) for
-some subexpression R and pack of subexpressions Vs is ill-formed if R is an lvalue or a const rvalue. Otherwise, it is expression-equivalent to mandate-nothrow-call(tag_invoke,set_value,R,Vs...).
-
-
11.7.3. execution::set_error[exec.set.error]
-
-
-
set_error is an error completion function. Its associated completion tag is set_error_t. The expression set_error(R,E) for some subexpressions R and E is ill-formed if R is an lvalue or a const rvalue. Otherwise, it is
-expression-equivalent to mandate-nothrow-call(tag_invoke,set_error,R,E).
-
-
11.7.4. execution::set_stopped[exec.set.stopped]
-
-
-
set_stopped is a stopped completion function. Its associated completion tag
-is set_stopped_t. The expression set_stopped(R) for some subexpression R is ill-formed if R is an lvalue or a const rvalue. Otherwise, it is
-expression-equivalent to mandate-nothrow-call(tag_invoke,set_stopped,R).
-
-
11.8. Operation states [exec.opstate]
-
-
-
The operation_state concept defines the requirements of an operation state
-type ([async.ops]).
If an operation_state object is moved during the lifetime of its
-asynchronous operation ([async.ops]), the behavior is undefined.
-
-
Library-provided operation state types are non-movable.
-
-
11.8.1. execution::start[exec.opstate.start]
-
-
-
The name start denotes a customization point object that starts
-([async.ops]) the asynchronous operation associated with the operation state
-object. The expression start(O) for some subexpression O is ill-formed
-if O is an rvalue. Otherwise, it is expression-equivalent to:
-
mandate-nothrow-call(tag_invoke,start,O)
-
-
-
If the function selected by tag_invoke does not start the asynchronous
-operation associated with the operation state O, the behavior of calling start(O) is undefined.
-
-
11.9. Senders [exec.snd]
-
11.9.1. Sender concepts [exec.snd.concepts]
-
-
-
The sender concept defines the requirements for a sender type
-([async.ops]). The sender_in concept defines the requirements for a sender
-type that can create asynchronous operations given an associated environment
-type. The sender_to concept defines the requirements for a sender type
-that can connect with a specific receiver type. The get_env customization
-point object is used to access a sender’s associated attributes. The connect customization point object is used to connect ([async.ops]) a
-sender and a receiver to produce an operation state.
A type Sigs satisfies and models the exposition-only concept valid-completion-signatures if it names a specialization
-of the completion_signatures class template.
-
-
Remarks: Pursuant to [namespace.std], users can specialize enable_sender to true for cv-unqualified program-defined types that model sender, and false for types that do not. Such specializations shall be usable in constant
-expressions ([expr.const]) and have type constbool.
-
-
The sender_of concept defines the requirements for a sender type that
-completes with the completion signature specified for the given completion
-function.
For a type T, SET-VALUE-SIG(T) names the type set_value_t() if T is cvvoid; otherwise, it names the type set_value_t(T).
-
-
Library-provided sender types:
-
-
-
Always expose an overload of a customization of connect that accepts an rvalue sender.
-
-
Only expose an overload of a customization of connect that
- accepts an lvalue sender if they model copy_constructible.
-
-
Model copy_constructible if they satisfy copy_constructible.
-
-
-
11.9.2. Awaitable helpers [exec.awaitables]
-
-
-
The sender concepts recognize awaitables as senders. For this clause
-([exec]), an awaitable is an expression that would be
-well-formed as the operand of a co_await expression within a given
-context.
-
-
For a subexpression c, let GET-AWAITER(c,p) be
-expression-equivalent to the series of transformations and conversions
-applied to c as the operand of an await-expression in a coroutine,
-resulting in lvalue e as described by [expr.await]/3.2-4, where p is an lvalue refering to the coroutine’s promise type, P. This includes the invocation of the promise type’s await_transform member if any, the invocation of the operatorco_await picked by overload resolution if any, and any necessary implicit
-conversions and materializations.
-
I have opened cwg#250 to give these
-transformations a term-of-art so we can more easily refer to it here.
-
-
Let is-awaitable be the following exposition-only
-concept:
await-suspend-result<T> is true if and only if one
- of the following is true:
-
-
-
T is void, or
-
-
T is bool, or
-
-
T is a specialization of coroutine_handle.
-
-
-
For a subexpression c such that decltype((c)) is type C, and
-an lvalue p of type P, await-result-type<C,P> names the type decltype(GET-AWAITER(c,p).await_resume()).
-
-
Let with-await-transform be the exposition-only class template:
get_completion_signatures is a customization point object. Let s be an
-expression such that decltype((s)) is S, and let e be an expression
-such that decltype((e)) is E. Then get_completion_signatures(s,e) is
-expression-equivalent to:
-
-
-
tag_invoke_result_t<get_completion_signatures_t,S,E>{} if that
-expression is well-formed,
-
-
-
Mandates:valid-completion-signatures<Sigs>, where Sigs names the type tag_invoke_result_t<get_completion_signatures_t,S,E>.
-
-
-
Otherwise, remove_cvref_t<S>::completion_signatures{} if that expression is well-formed,
-
-
-
Mandates:valid-completion-signatures<Sigs>,
-where Sigs names the type remove_cvref_t<S>::completion_signatures.
-
-
-
Otherwise, if is-awaitable<S,env-promise<E>> is true, then:
-
completion_signatures<
- SET-VALUE-SIG(await-result-type<S,env-promise<E>>),// see [exec.snd.concepts]
- set_error_t(exception_ptr),
- set_stopped_t()>{}
-
-
-
Otherwise, get_completion_signatures(s,e) is ill-formed.
-
-
-
Let r be an rvalue receiver of type R, and let S be the type of a
-sender such that sender_in<S,env_of_t<R>> is true. Let Sigs... be the
-template arguments of the completion_signatures specialization named by completion_signatures_of_t<S,env_of_t<R>>. Let CSO be
-a completion function. If sender S or its operation state cause the
-expression CSO(r,args...) to be potentially evaluated
-([basic.def.odr]) then there shall be a signature Sig in Sigs... such
-that MATCHING-SIG(tag_t<CSO>(decltype(args)...),Sig) is true ([exec.general]).
-
-
11.9.4. execution::connect[exec.connect]
-
-
-
connect connects ([async.op]) a sender with a receiver.
-
-
The name connect denotes a customization point object. For subexpressions s and r, let S be decltype((s)) and R be decltype((r)), and let DS and DR be the decayed types of S and R, respectively.
-
-
Let connect-awaitable-promise be the following class:
If S does not satisfy sender or if R does not satisfy receiver, connect(s,r) is ill-formed. Otherwise, the expression connect(s,r) is
-expression-equivalent to:
-
-
-
tag_invoke(connect,s,r) if connectable-with-tag-invoke<S,R> is modeled.
-
-
-
Mandates: The type of the tag_invoke expression above
-satisfies operation_state.
-
-
-
Otherwise, connect-awaitable(s,r) if that expression is
-well-formed.
-
-
Otherwise, connect(s,r) is ill-formed.
-
-
-
11.9.5. Sender factories [exec.factories]
-
11.9.5.1. execution::schedule[exec.schedule]
-
-
-
schedule obtains a schedule-sender ([async.ops]) from a scheduler.
-
-
The name schedule denotes a customization point object. For some
-subexpression s, the expression schedule(s) is expression-equivalent to:
-
-
-
tag_invoke(schedule,s), if that expression is valid. If the function
-selected by tag_invoke does not return a sender whose set_value completion scheduler is equivalent to s, the behavior of calling schedule(s) is undefined.
-
-
-
Mandates: The type of the tag_invoke expression above
-satisfies sender.
just is a factory for senders whose asynchronous operations complete
-synchronously in their start operation with a value completion operation. just_error is a factory for senders whose asynchronous operations complete
-synchronously in their start operation with an error completion operation. just_stopped is a factory for senders whose asynchronous operations
-complete synchronously in their start operation with a stopped completion
-operation.
The name just denotes a customization point object. For some pack of
-subexpressions vs, let Vs be the template paramter pack decltype((vs)). just(vs...) is expression-equivalent to just-sender<set_value_t,remove_cvref_t<Vs>...>({vs...}).
-
-
The name just_error denotes a customization point object. For some
-subexpression err, let Err be decltype((err)). just_error(err) is expression-equivalent to just-sender<set_error_t,remove_cvref_t<Err>>({err}).
-
-
Then name just_stopped denotes a customization point object. just_stopped() is expression-equivalent to just-sender<set_stopped_t>().
transfer_just is a factory for senders whose asynchronous operations
-execute value completion operations on an execution agent belonging to
-the execution resource associated with a specified scheduler.
-
-
The name transfer_just denotes a customization point object. For some
-subexpression s and pack of subexpressions vs, let S be decltype((s)) and let Vs be the template parameter pack decltype((vs)).... If S does not satisfy scheduler, or any type V in Vs does not satisfy movable-value, transfer_just(s,vs...) is ill-formed. Otherwise, transfer_just(s,vs...) is
-expression-equivalent to:
-
-
-
tag_invoke(transfer_just,s,vs...), if that expression is valid. Let as be a pack of rvalue subexpressions of types decay_t<Vs>... refering to objects direct-initilized from vs. If the function
-selected by tag_invoke does not return a sender whose asynchronous
-operations execute value completion operations on an execution agent
-belonging to the execution resource associated with s, with value result
-datums as, the behavior of calling transfer_just(s,vs...) is
-undefined.
-
-
-
Mandates:sender_of<R,set_value_t(decay_t<Vs>...),E>,
-where R is the type of the tag_invoke expression above, and E is the type of an environment.
-
-
-
Otherwise, transfer(just(vs...),s).
-
-
-
11.9.5.4. execution::read[exec.read]
-
-
-
read is a factory for a sender whose asynchronous operation completes
-synchronously in its start operation with a value completion result equal to
-a value read from the receiver’s associated environment.
-
-
read is a customization point object of the unspecified class type:
-
template<classTag>
- structread-sender;// exposition only
-
-structread-t{// exposition only
- template<classTag>
- constexprread-sender<Tag>operator()(Tag)constnoexcept{
- return{};
- }
-};
-
-
-
read-sender is the exposition-only class template:
-
template<classTag>
- structread-sender{// exposition only
- usingis_sender=unspecified;
- template<classR>
- structoperation-state{// exposition only
- Rr_;// exposition only
-
- friendvoidtag_invoke(start_t,operation-state&s)noexcept{
- TRY-SET-VALUE(std::move(s.r_),Tag{}(get_env(s.r_)));
- }
- };
-
- template<receiverR>
- friendoperation-state<decay_t<R>>tag_invoke(connect_t,read-sender,R&&r){
- return{std::forward<R>(r)};
- }
-
- template<classEnv>
- requirescallable<Tag,Env>
- friendautotag_invoke(get_completion_signatures_t,read-sender,Env)
- ->completion_signatures<
- set_value_t(call-result-t<Tag,Env>),set_error_t(exception_ptr)>;// not defined
-
- template<classEnv>
- requiresnothrow-callable<Tag,Env>
- friendautotag_invoke(get_completion_signatures_t,read-sender,Env)
- ->completion_signatures<set_value_t(call-result-t<Tag,Env>)>;// not defined
-
- friendempty_envtag_invoke(get_env_t,constread-sender&)noexcept{
- return{};
- }
- };
-
-
where TRY-SET-VALUE(r,e), for two subexpressions r and e,
-is equivalent to:
if e is potentially-throwing; or set_value(r,e) otherwise.
-
-
11.9.6. Sender adaptors [exec.adapt]
-
11.9.6.1. General [exec.adapt.general]
-
-
-
Subclause [exec.adapt] specifies a set of sender adaptors.
-
-
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 that would observe or modify any of the arguments
-of the adaptor before the returned sender is connected with a receiver using connect, and start is called on the resulting operation state. This
-requirement applies to any function that is selected by the implementation
-of the sender adaptor.
-
-
Unless otherwise specified, a parent sender ([async.ops]) with a single child
-sender s has an associated attribute object equal to FWD-QUERIES(get_env(s)) ([exec.fwd.env]). Unless
-otherwise specified, a parent sender with more than one child senders has an
-associated attributes object equal to empty_env{}. These
-requirements apply to any function that is selected by the implementation of
-the sender adaptor.
-
-
Unless otherwise specified, when a parent sender is connected to a receiver r, any receiver used to connect a child sender has an associated
-environment equal to FWD-QUERIES(get_env(r)). This
-requirements applies to any sender returned from a function that is selected
-by the implementation of such sender adaptor.
-
-
For any sender type, receiver type, operation state type, queryable type, or
-coroutine promise type that is part of the implementation of any sender
-adaptor in this subclause and that is a class template, the template
-arguments do not contribute to the associated entities
-([basic.lookup.argdep]) of a function call where a specialization of the
-class template is an associated entity.
If a sender returned from a sender adaptor specified in this subsection is
-specified to include set_error_t(E) among its set of completion signatures
-where decay_t<E> names the type exception_ptr, but the implementation
-does not potentially evaluate an error completion operation with an exception_ptr argument, the implementation is allowed to omit the exception_ptr error completion signature from the set.
A pipeable sender adaptor closure object is a function object that accepts one or more sender arguments and returns a sender. For a sender adaptor closure object C and an expression S such that decltype((S)) models sender, the following
-expressions are equivalent and yield a sender:
-
C(S)
-S|C
-
-
Given an additional pipeable sender adaptor closure object D, the expression C|D produces another pipeable sender adaptor closure object E:
-
E is a perfect forwarding call wrapper ([func.require]) with the following properties:
-
-
-
Its target object is an object d of type decay_t<decltype((D))> direct-non-list-initialized with D.
-
-
It has one bound argument entity, an object c of type decay_t<decltype((C))> direct-non-list-initialized with C.
-
-
Its call pattern is d(c(arg)), where arg is the argument used in a function call expression of E.
-
-
The expression C|D is well-formed if and only if the initializations of the state entities of E are all well-formed.
-
-
An object t of type T is a pipeable sender adaptor closure object if T models derived_from<sender_adaptor_closure<T>>, T has no other base
-classes of type sender_adaptor_closure<U> for any other type U, and T does not model sender.
-
-
The template parameter D for sender_adaptor_closure can be an incomplete type. Before any expression of type cvD appears as
-an operand to the | operator, D shall be complete and model derived_from<sender_adaptor_closure<D>>. The behavior of an expression involving an
-object of type cvD as an operand to the | operator is undefined if overload resolution selects a program-defined operator| function.
-
-
A pipeable sender adaptor object is a customization point object that accepts a sender as its first argument and returns a 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 adaptor accepts more than one argument, then let s be an expression such that decltype((s)) models sender,
-let args... be arguments such that adaptor(s,args...) is a well-formed expression as specified in the rest of this subclause
-([exec.adapt.objects]), and let BoundArgs be a pack that denotes decay_t<decltype((args))>.... The expression adaptor(args...) produces a pipeable sender adaptor closure object f that is a perfect forwarding call wrapper with the following properties:
-
-
-
Its target object is a copy of adaptor.
-
-
Its bound argument entities bound_args consist of objects of types BoundArgs... direct-non-list-initialized with std::forward<decltype((args))>(args)..., respectively.
-
-
Its call pattern is adaptor(r,bound_args...), where r is the argument used in a function call expression of f.
-
-
The expression adaptor(args...) is well-formed if and only if the initializations of the bound argument entities of the result, as specified above,
- are all well-formed.
-
-
11.9.6.3. execution::on[exec.on]
-
-
-
on adapts an input sender into a sender that will start on an execution
-agent belonging to a particular scheduler’s associated execution resource.
-
-
Let replace-scheduler(e,sch) be an expression denoting an object e' such that get_scheduler(e) returns a copy of sch, and tag_invoke(tag,e',args...) is expression-equivalent to tag(e,args...) for all arguments args... and for all tag whose type satisfies forwarding-query and is not get_scheduler_t.
-
-
The name on denotes a customization point object. For some subexpressions sch and s, let Sch be decltype((sch)) and S be decltype((s)). If Sch does not satisfy scheduler, or S does not satisfy sender, on is ill-formed. Otherwise, the expression on(sch,s) is expression-equivalent to:
-
-
-
tag_invoke(on,sch,s), if that expression is valid. If the function selected above does not return a sender which starts s on an execution agent of the associated execution resource of sch when
-started, the behavior of calling on(sch,s) is undefined.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies sender.
-
-
-
Otherwise, constructs a sender s1. When s1 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r such that:
-
-
-
When set_value(r) is called, it calls connect(s,r2), where r2 is as specified below, which results in op_state3. It calls start(op_state3). If any of these throws an exception, it calls set_error on out_r, passing current_exception() as the second argument.
-
-
set_error(r,e) is expression-equivalent to set_error(out_r,e).
-
-
set_stopped(r) is expression-equivalent to set_stopped(out_r).
-
-
get_env(r) is expression-equivalent to get_env(out_r).
-
-
-
Calls schedule(sch), which results in s2. It then calls connect(s2,r), resulting in op_state2.
-
-
op_state2 is wrapped by a new operation state, op_state1, that is returned to the caller.
-
-
r2 is a receiver that wraps a reference to out_r and forwards all
-completion operations to it. In addition, get_env(r2) returns replace-scheduler(e,sch).
-
-
When start is called on op_state1, it calls start on op_state2.
-
-
The lifetime of op_state2, once constructed, lasts until either op_state3 is constructed or op_state1 is destroyed, whichever comes first. The lifetime of op_state3, once constructed, lasts until op_state1 is destroyed.
-
-
-
Given subexpressions s1 and e, where s1 is a sender returned from on or a copy of such, let S1 be decltype((s1)).
-Let E' be decltype((replace-scheduler(e,sch))).
-Then the type of tag_invoke(get_completion_signatures,s1,e) shall be:
where no-value-completions<As...> names the type completion_signatures<> for any set of types As....
-
-
-
11.9.6.4. execution::transfer[exec.transfer]
-
-
-
transfer adapts a sender into a sender with a different associated set_value completion scheduler. [Note: it results in a transition between different execution resources when executed. --end note]
-
-
The name transfer denotes a customization point object. For some subexpressions sch and s, let Sch be decltype((sch)) and S be decltype((s)). If Sch does not satisfy scheduler, or S does not satisfy sender, transfer is ill-formed. Otherwise, the expression transfer(s,sch) is expression-equivalent to:
-
-
-
tag_invoke(transfer,get_completion_scheduler<set_value_t>(get_env(s)),s,sch), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies sender.
-
-
-
Otherwise, tag_invoke(transfer,s,sch), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies sender.
-
-
-
Otherwise, schedule_from(sch,s).
-
-
If the function selected above does not return a sender which is a result of
-a call to schedule_from(sch,s2), where s2 is a sender which
-sends values equivalent to those sent by s, the behavior of calling transfer(s,sch) is undefined.
-
-
For a sender t returned from transfer(s,sch), get_env(t) shall
-return a queryable object q such that get_completion_scheduler<CPO>(q) returns
-a copy of sch, where CPO is either set_value_t or set_stopped_t. The get_completion_scheduler<set_error_t> query is not implemented, as the scheduler
-cannot be guaranteed in case an error is thrown while trying to schedule work on
-the given scheduler object. For all other query objects Q whose type satisfies forwarding-query, the expression Q(q,args...) shall be equivalent to Q(get_env(s),args...).
schedule_from schedules work dependent on the completion of a sender onto a scheduler’s associated execution resource. [Note: schedule_from is not meant to be used in user code; it is used in the implementation of transfer. -end note]
-
-
The name schedule_from denotes a customization point object. For some subexpressions sch and s, let Sch be decltype((sch)) and S be decltype((s)). If Sch does not satisfy scheduler, or S does not satisfy sender, schedule_from is ill-formed. Otherwise, the expression schedule_from(sch,s) is expression-equivalent to:
-
-
-
tag_invoke(schedule_from,sch,s), if that expression is valid. If the
-function selected by tag_invoke does not return a sender that
-completes on an execution agent belonging to the associated execution
-resource of sch and completing with the same async result
-([async.ops]) as s, the behavior of calling schedule_from(sch,s) is
-undefined.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies sender.
-
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r such that when a receiver completion operation Tag(r,args...) is called, it decay-copies args... into op_state (see below) as args'... and constructs a receiver r2 such that:
-
-
-
When set_value(r2) is called, it calls Tag(out_r,std::move(args')...).
-
-
set_error(r2,e) is expression-equivalent to set_error(out_r,e).
-
-
set_stopped(r2) is expression-equivalent to set_stopped(out_r).
-
-
It then calls schedule(sch), resulting in a sender s3. It then calls connect(s3,r2), resulting in an operation state op_state3. It then calls start(op_state3). If any of these throws an exception,
-it catches it and calls set_error(out_r,current_exception()). If any of these expressions would be ill-formed, then Tag(r,args...) is ill-formed.
-
-
Calls connect(s,r) resulting in an operation state op_state2. If this expression would be ill-formed, connect(s2,out_r) is ill-formed.
-
-
Returns an operation state op_state that contains op_state2. When start(op_state) is called, calls start(op_state2). The lifetime of op_state3 ends when op_state is destroyed.
-
-
-
Given subexpressions s2 and e, where s2 is a sender returned from schedule_from or a copy of such, let S2 be decltype((s2)) and let E be decltype((e)). Then the type of tag_invoke(get_completion_signatures,s2,e) shall be:
-
make_completion_signatures<
- copy_cvref_t<S2,S>,
- E,
- make_completion_signatures<
- schedule_result_t<Sch>,
- E,
- potenially-throwing-completions,
- no-completions>,
- value-completions,
- error-completions>;
-
-
where potentially-throwing-completions, no-completions, value-completions,
-and error-completions are defined as follows:
For a sender t returned from schedule_from(sch,s), get_env(t) shall
-return a queryable object q such that get_completion_scheduler<CPO>(q) returns
-a copy of sch, where CPO is either set_value_t or set_stopped_t. The get_completion_scheduler<set_error_t> query is not implemented, as the scheduler
-cannot be guaranteed in case an error is thrown while trying to schedule work on
-the given scheduler object. For all other query objects Q whose type satisfies forwarding_query, the expression Q(q,args...) shall be equivalent to Q(get_env(s),args...).
-
-
11.9.6.6. execution::then[exec.then]
-
-
-
then attaches an invocable as a continuation for an input sender’s value
-completion operation.
-
-
The name then denotes a customization point object. For some
-subexpressions s and f, let S be decltype((s)), let F be the
-decayed type of f, and let f' be an xvalue refering to an object
-decay-copied from f. If S does not satisfy sender, or F does not model movable-value, then is
-ill-formed. Otherwise, the expression then(s,f) is
-expression-equivalent to:
-
-
-
tag_invoke(then,get_completion_scheduler<set_value_t>(get_env(s)),s,f), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies sender.
-
-
-
Otherwise, tag_invoke(then,s,f), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies sender.
-
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r such that:
-
-
-
When set_value(r,args...) is called, let v be the
-expression invoke(f',args...). If decltype(v) is void,
-calls set_value(out_r); otherwise, it calls set_value(out_r,v). If any of these throw an
-exception, it catches it and calls set_error(out_r,current_exception()). If any of these expressions would be
-ill-formed, the expression set_value(r,args...) is
-ill-formed.
-
-
set_error(r,e) is expression-equivalent to set_error(out_r,e).
-
-
set_stopped(r) is expression-equivalent to set_stopped(out_r).
-
-
-
Returns an expression-equivalent to connect(s,r).
-
-
Let compl-sig-t<Tag,Args...> name the type Tag() if Args... is a template paramter pack containing the
-single type void; otherwise, Tag(Args...). Given
-subexpressions s2 and e where s2 is a sender returned from then or a copy of such, let S2 be decltype((s2)) and let E be decltype((e)). The type of tag_invoke(get_completion_signatures,s2,e) shall be equivalent
-to:
and set-error-signature is an alias for completion_signatures<set_error_t(exception_ptr)> if any of the types
-in the type-list named by value_types_of_t<copy_cvref_t<S2,S>,E,potentially-throwing,type-list> are true_type; otherwise, completion_signatures<>, where potentially-throwing is the template alias:
If the function selected above does not return a sender that invokes f with the value result datums of s using f's return value as the sender’s value completion, and forwards the non-value completion operations unchanged, the behavior of calling then(s,f) is undefined.
-
-
11.9.6.7. execution::upon_error[exec.upon.error]
-
-
-
upon_error maps an input sender’s error completion operation into a value
-completion operation using the provided invocable.
-
-
The name upon_error denotes a customization point object. For
-some subexpressions s and f, let S be decltype((s)), let F be the
-decayed type of f, and let f' be an xvalue refering to an object
-decay-copied from f. If S does not satisfy sender, or F does not model movable-value, upon_error is
-ill-formed. Otherwise, the expression upon_error(s,f) is
-expression-equivalent to:
-
-
-
tag_invoke(upon_error,get_completion_scheduler<set_error_t>(get_env(s)),s,f), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies sender.
-
-
-
Otherwise, tag_invoke(upon_error,s,f), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies sender.
-
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r such that:
-
-
-
set_value(r,args...) is expression-equivalent to set_value(out_r,args...).
-
-
When set_error(r,e) is called, let v be the
-expression invoke(f',e). If decltype(v) is void, calls set_value(out_r); otherwise, it calls set_value(out_r,v). If any of these throw an
-exception, it catches it and calls set_error(out_r,current_exception()). If any of these expressions would be
-ill-formed, the expression set_error(r,e) is
-ill-formed.
-
-
set_stopped(r) is expression-equivalent to set_stopped(out_r).
-
-
-
Returns an expression-equivalent to connect(s,r).
-
-
Let compl-sig-t<Tag,Args...> name the type Tag() if Args... is a template paramter pack containing the
-single type void; otherwise, Tag(Args...). Given
-subexpressions s2 and e where s2 is a sender returned from upon_error or a copy of such, let S2 be decltype((s2)) and let E be decltype((e)). The type of tag_invoke(get_completion_signatures,s2,e) shall be equivalent
-to:
and set-error-signature is an alias for completion_signatures<set_error_t(exception_ptr)> if any of the types
-in the type-list named by error_types_of_t<copy_cvref_t<S2,S>,E,potentially-throwing> are true_type; otherwise, completion_signatures<>, where potentially-throwing is the template alias:
If the function selected above does not return a sender which invokes f with the error result datum of s using f's return value as the sender’s value completion, and forwards the non-error completion operations unchanged, the behavior of calling upon_error(s,f) is undefined.
upon_stopped maps an input sender’s stopped completion operation into a
-value completion operation using the provided invocable.
-
-
The name upon_stopped denotes a customization point object. For
-some subexpressions s and f, let S be decltype((s)), let F be the
-decayed type of f, and let f' be an xvalue refering to an object
-decay-copied from f. If S does not satisfy sender, or F does not model both movable-value and invocable, upon_stopped is ill-formed. Otherwise, the expression upon_stopped(s,f) is expression-equivalent to:
-
-
-
tag_invoke(upon_stopped,get_completion_scheduler<set_stopped_t>(get_env(s)),s,f), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies sender.
-
-
-
Otherwise, tag_invoke(upon_stopped,s,f), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies sender.
-
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r such that:
-
-
-
set_value(r,args...) is expression-equivalent to set_value(out_r,args...).
-
-
set_error(r,e) is expression-equivalent to set_error(out_r,e).
-
-
When set_stopped(r) is called, let v be the
-expression invoke(f'). If v has type void, calls set_value(out_r); otherwise, calls set_value(out_r,v). If any of these throw an
-exception, it catches it and calls set_error(out_r,current_exception()). If any of these expressions would be
-ill-formed, the expression set_stopped(r) is
-ill-formed.
-
-
-
Returns an expression-equivalent to connect(s,r).
-
-
Let compl-sig-t<Tag,Args...> name the type Tag() if Args... is a template paramter pack containing the
-single type void; otherwise, Tag(Args...). Given
-subexpressions s2 and e where s2 is a sender returned from upon_stopped or a copy of such, let S2 be decltype((s2)) and let E be decltype((e)). The type of tag_invoke(get_completion_signatures,s2,e) shall be equivalent
-to:
where set-stopped-completions names the type completion_signatures<compl-sig-t<set_value_t,invoke_result_t<F>>, and set-error-signature names the type completion_signatures<set_error_t(exception_ptr)> if is_nothrow_invocable_v<F> is true, or completion_signatures<> otherwise.
-
-
-
If the function selected above does not return a sender that invokes f when s executes a stopped completion, using f's return value as the
-sender’s the value completion, and propagates s's other completion
-operations unchanged, the behavior of calling upon_stopped(s,f) is
-undefined.
let_value transforms a sender’s value completion into a new child
-asynchronous operation. let_error transforms a sender’s error completion
-into a new child asynchronous operation. let_stopped transforms a sender’s
-stopped completion into a new child asynchronous operation.
-
-
The names let_value, let_error, and let_stopped denote customization point objects.
-Let the expression let-cpo be one of let_value, let_error, or let_stopped.
-For subexpressions s and f, let S be decltype((s)), let F be the decayed type of f, and let f' be an xvalue that refers to an object decay-copied from f.
-If S does not satisfy sender, the expression let-cpo(s,f) is ill-formed.
-If F does not satisfy invocable, the expression let_stopped(s,f) is ill-formed.
-Otherwise, the expression let-cpo(s,f) is expression-equivalent to:
-
-
-
tag_invoke(let-cpo,get_completion_scheduler<set_value_t>(get_env(s)),s,f), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies sender.
-
-
-
Otherwise, tag_invoke(let-cpo,s,f), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies sender.
-
-
-
Otherwise, given a receiver out_r and an lvalue out_r' refering to an object decay-copied from out_r.
-
-
-
For let_value, let set-cpo be set_value.
-For let_error, let set-cpo be set_error.
-For let_stopped, let set-cpo be set_stopped.
-Let completion-function be one of set_value, set_error, or set_stopped.
-
-
Let r be an rvalue of a receiver type R such that:
-
-
-
When set-cpo(r,args...) is called, the receiver r decay-copies args... into op_state2 as args'..., then calls invoke(f',args'...), resulting in a sender s3.
-It then calls connect(s3,std::move(out_r')), resulting in an operation state op_state3. op_state3 is saved as a part of op_state2.
-It then calls start(op_state3).
-If any of these throws an exception, it catches it and calls set_error(std::move(out_r'),current_exception()).
-If any of these expressions would be ill-formed, set-cpo(r,args...) is ill-formed.
-
-
completion-function(r,args...) is expression-equivalent to completion-function(std::move(out_r'),args...), when completion-function is different from set-cpo.
-
-
-
let-cpo(s,f) returns a sender s2 such that:
-
-
-
If the expression connect(s,r) is ill-formed, connect(s2,out_r) is ill-formed.
-
-
Otherwise, let op_state2 be the result of connect(s,r). connect(s2,out_r) returns an operation state op_state that stores op_state2. start(op_state) is expression-equivalent to start(op_state2).
-
-
-
Given subexpressions s2 and e, where s2 is a sender returned
-from let-cpo(s,f) or a copy of such, let S2 be decltype((s2)), let E be decltype((e)), and let DS be copy_cvref_t<S2,S>. Then the type of tag_invoke(get_completion_signatures,s2,e) is specified as
-follows:
-
-
-
If sender_in<DS,E> is false, the expression tag_invoke(get_completion_signatures,s2,e) is ill-formed.
-
-
Otherwise, let Sigs... be the set of template arguments of the completion_signatures specialization named by completion_signatures_of_t<DS,E>,
-let Sigs2... be the set of function types in Sigs... whose return type
-is set-cpo, and let Rest... be the set of function types
-in Sigs... but not Sigs2....
-
-
For each Sig2i in Sigs2..., let Vsi... be the set of function
-arguments in Sig2i and let S3i be invoke_result_t<F,decay_t<Vsi>&...>. If S3i is ill-formed, or if sender_in<S3i,E> is not satisfied,
-then the expression tag_invoke(get_completion_signatures,s2,e) is ill-formed.
-
-
Otherwise, let Sigs3i... be the
-set of template arguments of the completion_signatures specialization named by completion_signatures_of_t<S3i,E>. Then the type of tag_invoke(get_completion_signatures,s2,e) shall be equivalent to completion_signatures<Sigs30...,Sigs31...,...Sigs3n-1...,Rest...,set_error_t(exception_ptr)>, where n is sizeof...(Sigs2).
-
-
-
-
If let-cpo(s,f) does not return a sender that invokes f when set-cpo is called, and makes its completion dependent on the completion of a sender returned by f, and propagates the other completion operations sent by s, the behavior of calling let-cpo(s,f) is undefined.
-
-
11.9.6.10. execution::bulk[exec.bulk]
-
-
-
bulk runs a task repeatedly for every index in an index space.
-
-
The name bulk denotes a customization point object. For some
-subexpressions s, shape, and f, let S be decltype((s)), Shape be decltype((shape)), and F be decltype((f)). If S does not satisfy sender or Shape does not satisfy integral, bulk is ill-formed. Otherwise, the expression bulk(s,shape,f) is expression-equivalent to:
-
-
-
tag_invoke(bulk,get_completion_scheduler<set_value_t>(get_env(s)),s,shape,f), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies sender.
-
-
-
Otherwise, tag_invoke(bulk,s,shape,f), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies sender.
-
-
-
Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r:
-
-
-
When set_value(r,args...) is called, calls f(i,args...) for each i of type Shape from 0 to shape, then calls set_value(out_r,args...). If any of these throws an exception, it catches it and calls set_error(out_r,current_exception()).
-
-
When set_error(r,e) is called, calls set_error(out_r,e).
-
-
When set_stopped(r) is called, calls set_stopped(out_r,e).
-
-
-
Calls connect(s,r), which results in an operation state op_state2.
-
-
Returns an operation state op_state that contains op_state2. When start(op_state) is called, calls start(op_state2).
-
-
Given subexpressions s2 and e where s2 is a sender returned
-from bulk or a copy of such, let S2 be decltype((s2)), let E be decltype((e)), let DS be copy_cvref_t<S2,S>, let Shape be decltype((shape)) and let nothrow-callable be the alias template:
If any of the types in the type-list named by value_types_of_t<DS,E,nothrow-callable,type-list> are false_type, then the type of tag_invoke(get_completion_signatures,s2,e) shall be
-equivalent to:
Otherwise, the type of tag_invoke(get_completion_signatures,s2,e) shall be equivalent to completion_signatures_of_t<DS,E>.
-
-
-
-
If the function selected above does not return a sender that invokes f(i,args...) for each i of type Shape from 0 to shape where args is a pack of subexpressions refering to the value completion
-result datums of the input sender, or does not execute a value
-completion operation with said datums, the behavior of calling bulk(s,shape,f) is undefined.
-
-
-
11.9.6.11. execution::split[exec.split]
-
-
-
split adapts an arbitrary sender into a sender that can be connected multiple times.
-
-
Let split-env be the type of an environment such that,
-given an instance e, the expression get_stop_token(e) is well-formed and
-has type stop_token.
-
-
The name split denotes a customization point object. For some
-subexpression s, let S be decltype((s)). If sender_in<S,split-env> or constructible_from<decay_t<env_of_t<S>>,env_of_t<S>> is false, split is ill-formed. Otherwise, the expression split(s) is expression-equivalent to:
-
-
-
tag_invoke(split,get_completion_scheduler<set_value_t>(get_env(s)),s),
-if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies sender.
-
-
-
Otherwise, tag_invoke(split,s), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies sender.
-
-
-
Otherwise, constructs a sender s2, which:
-
-
-
Creates an object sh_state that contains a stop_source, a list of
-pointers to operation states awaiting the completion of s, and that
-also reserves space for storing:
-
-
-
the operation state that results from connecting s with r described below, and
-
-
the sets of values and errors with which s can complete, with
-the addition of exception_ptr.
-
-
the result of decay-copying get_env(s).
-
-
-
Constructs a receiver r such that:
-
-
-
When set_value(r,args...) is called, decay-copies
-the expressions args... into sh_state. It then notifies all
-the operation states in sh_state's list of operation states
-that the results are ready. If any exceptions are thrown, the
-exception is caught and set_error(r,current_exception()) is called instead.
-
-
When set_error(r,e) is called, decay-copies e into sh_state. It then notifies the operation states in sh_state's list of operation states that the results are ready.
-
-
When set_stopped(r) is called, notifies the
-operation states in sh_state's list of operation states that
-the results are ready.
-
-
get_env(r) is an expression e of type split-env such that get_stop_token(e) is well-formed
-and returns the results of calling get_token() on sh_state's
-stop source.
-
-
-
Calls get_env(s) and decay-copies the result into sh_state.
-
-
Calls connect(s,r), resulting in an operation state op_state2. op_state2 is saved in sh_state.
-
-
When s2 is connected with a receiver out_r of type OutR, it
-returns an operation state object op_state that contains:
-
-
-
An object out_r' of type OutR decay-copied from out_r,
-
-
A reference to sh_state,
-
-
A stop callback of type optional<stop_token_of_t<env_of_t<OutR>>::callback_type<stop-callback-fn>>,
-where stop-callback-fn is the unspecified
-class type:
If one of r's completion functions has executed, then let Tag be the completion function that was
-called. Calls Tag(out_r',args2...),
-where args2... is a pack of const lvalues referencing the
-subobjects of sh_state that have been saved by the original
-call to Tag(r,args...) and returns.
-
-
Otherwise, it emplace constructs the stop callback optional with
-the arguments get_stop_token(get_env(out_r')) and stop-callback-fn{stop-src}, where stop-src refers to the stop source of sh_state.
-
-
Otherwise, it adds a pointer to op_state to the list of
-operation states in sh_state. If op_state is the first such
-state added to the list:
-
-
-
If stop-src.stop_requested() is true,
- all of the operation states in sh_state's list of operation
- states are notified as if set_stopped(r) had
- been called.
-
-
Otherwise, start(op_state2) is called.
-
-
-
-
When r completes it will notify op_state that the result are
-ready. Let Tag be whichever
-completion function was called on receiver r. op_state's
-stop callback optional is reset. Then Tag(std::move(out_r'),args2...) is called,
-where args2... is a pack of const lvalues referencing the subobjects of sh_state that have been saved by the original call to Tag(r,args...).
-
-
Ownership of sh_state is shared by s2 and by every op_state that results from connecting s2 to a receiver.
-
-
-
Given subexpressions s2 where s2 is a sender returned from split or a copy of such, get_env(s2) shall return an lvalue reference to the
-object in sh_state that was initialized with the result of get_env(s).
-
-
Given subexpressions s2 and e where s2 is a sender returned
-from split or a copy of such, let S2 be decltype((s2)) and let E be decltype((e)). The type of tag_invoke(get_completion_signatures,s2,e) shall be equivalent
-to:
Let s be a sender expression, r be an instance of the receiver type
-described above, s2 be a sender returned
-from split(s) or a copy of such, r2 is the receiver
-to which s2 is connected, and args is the pack of subexpressions
-passed to r's completion function CSO when s completes. s2 shall invoke CSO(r2,args2...) where args2 is a pack of const lvalue references to objects decay-copied from args, or by calling set_error(r2,e2) for some subexpression e2. The objects passed to r2's completion operation shall
-be valid until after the completion of the invocation of r2's completion
-operation.
-
-
-
11.9.6.12. execution::when_all[exec.when.all]
-
-
-
when_all and when_all_with_variant both adapt multiple input senders into
-a sender that completes when all input senders have completed. when_all only accepts senders with a single value completion signature and on success
-concatenates all the input senders' value result datums into its own value
-completion operation. when_all_with_variant(s...) is semantically
-equivilant to when_all(into_variant(s)...), where s is a pack of
-subexpressions of sender types.
-
-
The name when_all denotes a customization point object. For some subexpressions si..., let Si... be decltype((si)).... The expression when_all(si...) is ill-formed if any of the following is true:
-
-
-
If the number of subexpressions si... is 0, or
-
-
If any type Si does not satisfy sender.
-
-
Otherwise, the expression when_all(si...) is expression-equivalent to:
-
-
-
tag_invoke(when_all,si...), if
-that expression is valid. If the function selected by tag_invoke does
-not return a sender that sends a concatenation of values sent by si... when they all complete with set_value, the behavior of calling when_all(si...) is undefined.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies sender.
-
-
-
Otherwise, constructs a sender w of type W. When w is connected
-with some receiver out_r of type OutR, it returns an operation state op_state specified as below:
-
-
-
For each sender si, constructs a receiver ri such that:
-
-
-
If set_value(ri,ti...) is called for every ri, op_state's associated stop callback optional is reset and set_value(out_r,t0...,t1...,...,tn-1...) is called, where n the number of subexpressions in si....
-
-
Otherwise, set_error or set_stopped was called for at least one receiver ri. If the first such to complete did so with the call set_error(ri,e), request_stop is called on op_state's associated stop source. When all child operations have completed, op_state's associated stop callback optional is reset and set_error(out_r,e) is called.
-
-
Otherwise, request_stop is called on op_state's associated stop source. When all child operations have completed, op_state's associated stop callback optional is reset and set_stopped(out_r) is called.
-
-
For each receiver ri, get_env(ri) is an expression e such that get_stop_token(e) is well-formed and returns the results of calling get_token() on op_state's associated stop source, and for which tag_invoke(tag,e,args...) is expression-equivalent to tag(get_env(out_r),args...) for all arguments args... and all tag whose type satisfies forwarding-query and is not get_stop_token_t.
-
-
-
For each sender si, calls connect(si,ri), resulting in operation states child_opi.
-
-
Returns an operation state op_state that contains:
-
-
-
Each operation state child_opi,
-
-
A stop source of type in_place_stop_source,
-
-
A stop callback of type optional<stop_token_of_t<env_of_t<OutR>>::callback_type<stop-callback-fn>>, where stop-callback-fn is the unspecified class type:
Emplace constructs the stop callback optional with the arguments get_stop_token(get_env(out_r)) and stop-callback-fn{stop-src}, where stop-src refers to the stop source of op_state.
-
-
Then, it checks to see if stop-src.stop_requested() is true. If so, it calls set_stopped(out_r).
-
-
Otherwise, calls start(child_opi) for each child_opi.
-
-
-
Given subexpressions s2 and e where s2 is a sender returned
-from when_all or a copy of such, let S2 be decltype((s2)), let E be decltype((e)), and let Ss... be the decayed types of the
-arguments to the when_all expression that created s2. Let WE be a type such that stop_token_of_t<WE> is in_place_stop_token and tag_invoke_result_t<Tag,WE,As...> names the type, if any, of call-result-t<Tag,E,As...> for all types As... and all types Tag besides get_stop_token_t. The type of tag_invoke(get_completion_signatures,s2,e) shall be as follows:
-
-
-
For each type Si in Ss..., let DSi name the type copy_cvref_t<S2,Si>. If for
-any type DSi, the type completion_signatures_of_t<DSi,WE> is ill-formed, the expression of tag_invoke(get_completion_signatures,s2,e) is
-ill-formed.
-
-
Otherwise, for each type DSi, let Sigsi... be the set of template
-arguments in the specialization of completion_signatures named
-by completion_signatures_of_t<DSi,WE>, and let Ci be the
-count of function types in Sigsi... for which the return
-type is set_value_t. If any Ci is two or greater, then the
-expression tag_invoke(get_completion_signatures,s2,e) is
-ill-formed.
-
-
Otherwise, let Sigs2i... be the set of
-function types in Sigsi... whose
-return types are notset_value_t, and let Ws... be
-the unique set of types in [Sigs20...,Sigs21...,...Sigs2n-1...,set_stopped_t()], where n is sizeof...(Ss). If any Ci is 0, then the type of tag_invoke(get_completion_signatures,s2,e) shall be completion_signatures<Ws...>.
-
-
Otherwise, let Vi... be the
-function argument types of the single type in Sigsi... for which the return
-type is set_value_t. Then the type of tag_invoke(get_completion_signatures,s2,e) shall be completion_signatures<Ws...,set_value_t(decay_t<V0>&&...,decay_t<V1>&&...,...decay_t<Vn-1>&&...)>.
-
-
-
-
-
The name when_all_with_variant denotes a customization point object. For
-some subexpressions s..., let S be decltype((s)). If any type Si in S... does not satisfy sender, when_all_with_variant is ill-formed. Otherwise, the expression when_all_with_variant(s...) is expression-equivalent to:
-
-
-
tag_invoke(when_all_with_variant,s...), if that expression
-is valid. If the function selected by tag_invoke does not return a
-sender that, when connected with a receiver of type R, sends the types into-variant-type<S,env_of_t<R>>... when they
-all complete with set_value, the behavior of calling when_all(si...) is undefined.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies sender.
-
-
-
Otherwise, when_all(into_variant(s)...).
-
-
-
For a sender s2 returned from when_all or when_all_with_variant, get_env(s2) shall return an instance of a class equivalent to empty_env.
transfer_when_all and transfer_when_all_with_variant both adapt multiple
-input senders into a sender that completes when all input senders have
-completed, ensuring the input senders complete on the specified scheduler. transfer_when_all only accepts senders with a single value completion
-signature and on success concatenates all the input senders' value result
-datums into its own value completion operation; transfer_when_all(scheduler,input-senders...) is
-semantically equivalent to transfer(when_all(input-senders...),scheduler). transfer_when_all_with_variant(scheduler,input-senders...) is semantically equivilant to transfer_when_all(scheduler,into_variant(intput-senders)...). These
-customizable composite algorithms can allow for more efficient
-customizations in some cases.
-
-
The name transfer_when_all denotes a customization point object. For some subexpressions sch and s..., let Sch be decltype(sch) and S be decltype((s)). If Sch does not satisfy scheduler, or any type Si in S... does not satisfy sender, transfer_when_all is ill-formed. Otherwise, the expression transfer_when_all(sch,s...) is expression-equivalent to:
-
-
-
tag_invoke(transfer_when_all,sch,s...), if that expression
-is valid. If the function selected by tag_invoke does not return a
-sender that sends a concatenation of values sent by s... when they all
-complete with set_value, or does not send its completion operation,
-other than ones resulting from a scheduling error, on an execution agent
-belonging to the associated execution resource of sch, the behavior of
-calling transfer_when_all(sch,s...) is undefined.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies sender.
-
-
-
Otherwise, transfer(when_all(s...),sch).
-
-
-
The name transfer_when_all_with_variant denotes a customization
-point object. For some subexpressions sch and s..., let Sch be decltype((sch)) and let S be decltype((s)). If any type Si in S... does not satisfy sender, transfer_when_all_with_variant is
-ill-formed. Otherwise, the expression transfer_when_all_with_variant(sch,s...) is expression-equivalent
-to:
-
-
-
tag_invoke(transfer_when_all_with_variant,s...), if that
-expression is valid. If the function selected by tag_invoke does not
-return a sender that, when connected with a receiver of type R, sends
-the types into-variant-type<S,env_of_t<R>>... when they all complete with set_value, the behavior
-of calling transfer_when_all_with_variant(sch,s...) is undefined.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies sender.
For a sender t returned from transfer_when_all(sch,s...), get_env(t) shall
-return a queryable object q such that get_completion_scheduler<CPO>(q) returns
-a copy of sch, where CPO is either set_value_t or set_stopped_t. The get_completion_scheduler<set_error_t> query is not implemented, as the scheduler
-cannot be guaranteed in case an error is thrown while trying to schedule work on
-the given scheduler object.
into_variant is a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy sender, into_variant(s) is ill-formed. Otherwise, into_variant(s) returns
-a sender s2. When s2 is connected with some receiver out_r, it:
-
-
-
Constructs a receiver r:
-
-
-
If set_value(r,ts...) is called, calls set_value(out_r,into-variant-type<S,env_of_t<decltype((r))>>(decayed-tuple<decltype(ts)...>(ts...))). If this expression throws an exception, calls set_error(out_r,current_exception()).
-
-
set_error(r,e) is expression-equivalent to set_error(out_r,e).
-
-
set_stopped(r) is expression-equivalent to set_stopped(out_r).
-
-
-
Calls connect(s,r), resulting in an operation state op_state2.
-
-
Returns an operation state op_state that contains op_state2. When start(op_state) is called, calls start(op_state2).
-
-
Given subexpressions s2 and e, where s2 is a sender returned from into_variant or a copy of such, let S2 be decltype((s2)) and E be decltype((e)).
-Let into-variant-set-value be the class template:
Let INTO-VARIANT-ERROR-SIGNATURES(S,E) be completion_signatures<set_error_t(exception_ptr)> if any of the types in the type-list named by value_types_of_t<S,E,into-variant-is-nothrow<S,E>::templateapply,type-list> are false_type; otherwise, completion_signatures<>.
-
The type of tag_invoke(get_completion_signatures_t{},s2,e) shall be equivalent to:
stopped_as_optional maps an input sender’s stopped completion operation into the value completion operation as an empty optional. The input sender’s value completion operation is also converted into an optional. The result is a sender that never completes with stopped, reporting cancellation by completing with an empty optional.
-
-
The name stopped_as_optional denotes a customization point object. For some subexpression s, let S be decltype((s)). Let get-env-sender be an expression such that, when it is connected with a receiver r, start on the resulting operation state completes immediately by calling set_value(r,get_env(r)). The expression stopped_as_optional(s) is expression-equivalent to:
stopped_as_error maps an input sender’s stopped completion operation into
-an error completion operation as a custom error type. The result is a sender
-that never completes with stopped, reporting cancellation by completing with
-an error.
-
-
The name stopped_as_error denotes a customization point object. For some subexpressions s and e, let S be decltype((s)) and let E be decltype((e)). If the type S does not satisfy sender or if the type E doesn’t satisfy movable-value, stopped_as_error(s,e) is ill-formed. Otherwise, the expression stopped_as_error(s,e) is expression-equivalent to:
ensure_started eagerly starts the execution of a sender, returning a sender
-that is usable as intput to additional sender algorithms.
-
-
Let ensure-started-env be the type of an execution
-environment such that, given an instance e, the expression get_stop_token(e) is well-formed and has type stop_token.
-
-
The name ensure_started denotes a customization point object.
-For some subexpression s, let S be decltype((s)). If sender_in<S,ensure-started-env> or constructible_from<decay_t<env_of_t<S>>,env_of_t<S>> is false, ensure_started(s) is ill-formed. Otherwise, the
-expression ensure_started(s) is expression-equivalent to:
-
-
-
tag_invoke(ensure_started,get_completion_scheduler<set_value_t>(get_env(s)),s), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies sender.
-
-
-
Otherwise, tag_invoke(ensure_started,s), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above satisfies sender.
-
-
-
Otherwise, constructs a sender s2, which:
-
-
-
Creates an object sh_state that contains a stop_source, an
-initially null pointer to an operation state awaitaing completion,
-and that also reserves space for storing:
-
-
-
the operation state that results from connecting s with r described below, and
-
-
the sets of values and errors with which s can complete, with
-the addition of exception_ptr.
-
-
the result of decay-copying get_env(s).
-
-
s2 shares ownership of sh_state with r described below.
-
-
Constructs a receiver r such that:
-
-
-
When set_value(r,args...) is called, decay-copies
-the expressions args... into sh_state. It then checks sh_state to see if there is an operation state awaiting
-completion; if so, it notifies the operation state that the
-results are ready. If any exceptions are thrown, the exception
-is caught and set_error(r,current_exception()) is
-called instead.
-
-
When set_error(r,e) is called, decay-copies e into sh_state. If there is an operation state awaiting completion,
-it then notifies the operation state that the results are ready.
-
-
When set_stopped(r) is called, it then notifies any
-awaiting operation state that the results are ready.
-
-
get_env(r) is an expression e of type ensure-started-env such that get_stop_token(e) is well-formed
-and returns the results of calling get_token() on sh_state's
-stop source.
-
-
r shares ownership of sh_state with s2. After r has been completed, it releases its ownership of sh_state.
-
-
-
Calls get_env(s) and decay-copies the result into sh_state.
-
-
Calls connect(s,r), resulting in an operation state op_state2. op_state2 is saved in sh_state. It then calls start(op_state2).
-
-
When s2 is connected with a receiver out_r of type OutR, it
-returns an operation state object op_state that contains:
-
-
-
An object out_r' of type OutR decay-copied from out_r,
-
-
A reference to sh_state,
-
-
A stop callback of type optional<stop_token_of_t<env_of_t<OutR>>::callback_type<stop-callback-fn>>,
-where stop-callback-fn is the unspecified
-class type:
s2 transfers its ownership of sh_state to op_state.
-
-
When start(op_state) is called:
-
-
-
If r has already been completed, then let CF be whichever completion function
-was used to complete r. Calls CF(out_r',args2...), where args2... is a
-pack of xvalues referencing the subobjects of sh_state that have
-been saved by the original call to CF(r,args...) and returns.
-
-
Otherwise, it emplace constructs the stop callback optional with
-the arguments get_stop_token(get_env(out_r')) and stop-callback-fn{stop-src}, where stop-src refers to the stop source of sh_state.
-
-
Then, it checks to see if stop-src.stop_requested() is true. If so, it
-calls set_stopped(out_r').
-
-
Otherwise, it sets sh_state operation state pointer to the
-address of op_state, registering itself as awaiting the result
-of the completion of r.
-
-
-
When r completes it will notify op_state that the result are
-ready. Let CF be whichever
-completion function was used to complete r. op_state's stop
-callback optional is reset. Then CF(std::move(out_r'),args2...) is called,
-where args2... is a pack of xvalues referencing the subobjects of sh_state that have been saved by the original call to CF(r,args...).
-
-
[Note: If sender s2 is destroyed without being connected to a
-receiver, or if it is connected but the operation state is destroyed
-without having been started, then when r completes and it releases its shared ownership of sh_state, sh_state will be destroyed and the results of the operation are
-discarded. -- end note]
-
-
-
Given a subexpression s, let s2 be the result of ensure_started(s).
-The result of get_env(s2) shall return an lvalue reference to the
-object in sh_state that was initialized with the result of get_env(s).
-
-
Given subexpressions s2 and e where s2 is a sender returned
-from ensure_started or a copy of such, let S2 be decltype((s2)) and let E be decltype((e)). The type of tag_invoke(get_completion_signatures,s2,e) shall be equivalent
-to:
Let s be a sender expression, r be an instance of the receiver type
- described above, s2 be a sender returned
- from ensure_started(s) or a copy of such, r2 is the receiver
- to which s2 is connected, and args is the pack of subexpressions
- passed to r's completion function CSO when s completes. s2 shall invoke CSO(r2,args2...) where args2 is a pack of xvalue references to objects decay-copied from args, or by calling set_error(r2,e2) for some subexpression e2. The objects passed to r2's completion operation shall
- be valid until after the completion of the invocation of r2's completion
- operation.
start_detached eagerly starts a sender without the caller needing to manage the lifetimes of any objects.
-
-
The name start_detached denotes a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy sender, start_detached is ill-formed. Otherwise, the expression start_detached(s) is expression-equivalent to:
-
-
-
tag_invoke(start_detached,get_completion_scheduler<set_value_t>(get_env(s)),s), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above is void.
-
-
-
Otherwise, tag_invoke(start_detached,s), if that expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above is void.
-
-
-
Otherwise:
-
-
-
Let R be the type of a receiver, let r be an rvalue of type R, and let cr be a
-lvalue reference to constR such that:
-
-
-
The expression set_value(r) is not potentially-throwing and has no effect,
-
-
For any subexpression e, the expression set_error(r,e) is expression-equivalent
-to terminate(),
-
-
The expression set_stopped(r) is not potentially-throwing and has no effect, and
-
-
The expression get_env(cr) is expression-equivalent to empty_env{}.
-
-
-
Calls connect(s,r), resulting in an operation state op_state, then calls start(op_state).
-
-
-
If the function selected above does not eagerly start the sender s after
-connecting it with a receiver that ignores value and stopped completion
-operations and calls terminate() on error completions, the behavior of
-calling start_detached(s) is undefined.
-
-
11.9.7.2. this_thread::sync_wait[exec.sync.wait]
-
-
-
this_thread::sync_wait and this_thread::sync_wait_with_variant 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. sync_wait requires that the input sender has exactly one value completion signature.
-
-
For any receiver r created by an implementation of sync_wait and sync_wait_with_variant, the expressions get_scheduler(get_env(r)) and get_delegatee_scheduler(get_env(r)) shall be well-formed. For a receiver
-created by the default implementation of this_thread::sync_wait, these
-expressions shall return a scheduler to the same thread-safe,
-first-in-first-out queue of work such that tasks scheduled to the queue
-execute on the thread of the caller of sync_wait. [Note: The
-scheduler for an instance of run_loop that is a local variable
-within sync_wait is one valid implementation. -- end note]
-
-
The templates sync-wait-type and sync-wait-with-variant-type are used to determine the
-return types of this_thread::sync_wait and this_thread::sync_wait_with_variant. Let sync-wait-env be the type of the expression get_env(r) where r is an instance of the
-receiver created by the default implementation of sync_wait.
The name this_thread::sync_wait denotes a customization point object. For
-some subexpression s, let S be decltype((s)). If sender_in<S,sync-wait-env> is false,
-or the number of the arguments completion_signatures_of_t<S,sync-wait-env>::value_types passed into the Variant template
-parameter is not 1, this_thread::sync_wait(s) is ill-formed. Otherwise, this_thread::sync_wait(s) is expression-equivalent to:
-
-
-
tag_invoke(this_thread::sync_wait,get_completion_scheduler<set_value_t>(get_env(s)),s), if this expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above is sync-wait-type<S,sync-wait-env>.
-
-
-
Otherwise, tag_invoke(this_thread::sync_wait,s), if this expression is valid and its type is.
-
-
-
Mandates: The type of the tag_invoke expression above is sync-wait-type<S,sync-wait-env>.
-
-
-
Otherwise:
-
-
-
Constructs a receiver r.
-
-
Calls connect(s,r), resulting in an operation state op_state, then calls start(op_state).
-
-
Blocks the current thread until a completion operation of r is executed. When it is:
-
-
-
If set_value(r,ts...) has been called, returns sync-wait-type<S,sync-wait-env>{decayed-tuple<decltype(ts)...>{ts...}}. If that expression exits exceptionally, the exception is propagated to the caller of sync_wait.
-
-
If set_error(r,e) has been called, let E be the decayed type of e. If E is exception_ptr, calls std::rethrow_exception(e). Otherwise, if the E is error_code, throws system_error(e). Otherwise, throws e.
-
-
If set_stopped(r) has been called, returns sync-wait-type<S,sync-wait-env>{}.
-
-
-
-
-
The name this_thread::sync_wait_with_variant denotes a customization point
-object. For some subexpression s, let S be the type of into_variant(s). If sender_in<S,sync-wait-env> is false, this_thread::sync_wait_with_variant(s) is ill-formed. Otherwise, this_thread::sync_wait_with_variant(s) is expression-equivalent to:
-
-
-
tag_invoke(this_thread::sync_wait_with_variant,get_completion_scheduler<set_value_t>(get_env(s)),s), if this expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above is sync-wait-with-variant-type<S,sync-wait-env>.
-
-
-
Otherwise, tag_invoke(this_thread::sync_wait_with_variant,s), if this expression is valid.
-
-
-
Mandates: The type of the tag_invoke expression above is sync-wait-with-variant-type<S,sync-wait-env>.
execute creates fire-and-forget tasks on a specified scheduler.
-
-
The name execute denotes a customization point object. For some subexpressions sch and f, let Sch be decltype((sch)) and F be decltype((f)). If Sch does not satisfy scheduler or F does not satisfy invocable, execute is ill-formed. Otherwise, execute is expression-equivalent to:
-
-
-
tag_invoke(execute,sch,f), if that expression is valid. If
-the function selected by tag_invoke does not invoke the function f (or an object decay-copied from f) on an execution agent belonging to
-the associated execution resource of sch, or if it does not call std::terminate if an error occurs after control is returned to the
-caller, the behavior of calling execute is undefined.
-
-
-
Mandates: The type of the tag_invoke expression above is void.
-
-
-
Otherwise, start_detached(then(schedule(sch),f)).
-
-
-
11.11. Sender/receiver utilities [exec.utils]
-
-
-
This section makes use of the following exposition-only entities:
-
// [Editorial note: copy_cvref_t as in [[P1450R3]] -- end note]
-// Mandates: is_base_of_v<T, remove_reference_t<U>> is true
-template<classT,classU>
- copy_cvref_t<U&&,T>c-style-cast(U&&u)noexceptrequiresdecays-to<T,T>{
- return(copy_cvref_t<U&&,T>)std::forward<U>(u);
- }
-
-
-
- [Note: The C-style cast in
- c-style-cast
- is to disable accessibility checks. -- end note]
-
template<
- class-typeDerived,
- receiverBase=unspecified>// arguments are not associated entities ([lib.tmpl-heads])
- classreceiver_adaptor;
-
-
-
-
receiver_adaptor simplifies the implementation of one receiver type in terms of another. It defines tag_invoke overloads that forward to named members if they exist, and to the adapted receiver otherwise.
-
-
If Base is an alias for the unspecified default template argument, then:
-
-
-
Let HAS-BASE be false, and
-
-
Let GET-BASE(d) be d.base().
-
-
otherwise, let:
-
-
-
Let HAS-BASE be true, and
-
-
Let GET-BASE(d) be c-style-cast<receiver_adaptor<Derived,Base>>(d).base().
-
-
Let BASE-TYPE(D) be the type of GET-BASE(declval<D>()).
-
-
receiver_adaptor<Derived,Base> is equivalent to the following:
[Note:receiver_adaptor provides tag_invoke overloads on behalf of
-the derived class Derived, which is incomplete when receiver_adaptor is
-instantiated.]
Let SET-VALUE be the expression std::move(self).set_value(std::forward<As>(as)...).
-
-
Constraints: Either SET-VALUE is a valid expression or typenameDerived::set_value denotes a type and callable<set_value_t,BASE-TYPE(Derived),As...> is true.
-
-
Mandates:SET-VALUE, if that expression is valid, is not potentially-throwing.
Let SET-ERROR be the expression std::move(self).set_error(std::forward<E>(e)).
-
-
Constraints: Either SET-ERROR is a valid expression or typenameDerived::set_error denotes a type and callable<set_error_t,BASE-TYPE(Derived),E> is true.
-
-
Mandates:SET-ERROR, if that expression is valid, is not potentially-throwing.
Let SET-STOPPED be the expression std::move(self).set_stopped().
-
-
Constraints: Either SET-STOPPED is a valid expression or typenameDerived::set_stopped denotes a type and callable<set_stopped_t,BASE-TYPE(Derived)> is true.
-
-
Mandates:SET-STOPPED, if that expression is valid, is not potentially-throwing.
-
-
Effects: Equivalent to:
-
-
-
If SET-STOPPED is a valid expression, SET-STOPPED;
Constraints: Either self.get_env() is a valid expression or typenameDerived::get_env denotes a type and callable<get_env_t,BASE-TYPE(constDerived&)> is true.
-
-
Effects: Equivalent to:
-
-
-
If self.get_env() is a valid expression, self.get_env();
-
-
Otherwise, std::get_env(GET-BASE(self)).
-
-
-
Remarks: The expression in the noexcept clause is:
-
-
-
If self.get_env() is a valid expression, noexcept(self.get_env());
completion_signatures is a type that encodes a set of completion signatures
-([async.ops]).
-
-
[Example:
-
classmy_sender{
- usingcompletion_signatures=
- completion_signatures<
- set_value_t(),
- set_value_t(int,float),
- set_error_t(exception_ptr),
- set_error_t(error_code),
- set_stopped_t()>;
-};
-
-// Declares my_sender to be a sender that can complete by calling
-// one of the following for a receiver expression R:
-// set_value(R)
-// set_value(R, int{...}, float{...})
-// set_error(R, exception_ptr{...})
-// set_error(R, error_code{...})
-// set_stopped(R)
-
-
-- end example]
-
-
This section makes use of the following exposition-only entities:
Let Fns... be a template parameter pack of the arguments of the completion_signatures specialization named by completion_signatures_of_t<S,E>, let TagFns be a
-template parameter pack of the function types in Fns whose return types
-are Tag, and let Tsn be a template parameter
-pack of the function argument types in the n-th type
-in TagFns. Then, given two variadic templates Tuple and Variant, the type gather-signatures<Tag,S,E,Tuple,Variant> names the type META-APPLY(Variant,META-APPLY(Tuple,Ts0...),META-APPLY(Tuple,Ts1...),...META-APPLY(Tuple,Tsm-1...)), where m is the size of the parameter pack TagFns and META-APPLY(T,As...) is
-equivalent to:
make_completion_signatures is an alias template used to adapt the
-completion signatures of a sender. It takes a sender, and environment, and
-several other template arguments that apply modifications to the sender’s
-completion signatures to generate a new specialization of completion_signatures.
-
-
[Example:
-
// Given a sender S and an environment Env, adapt S’s completion
-// signatures by lvalue-ref qualifying the values, adding an additional
-// exception_ptr error completion if its not already there, and leaving the
-// other completion signatures alone.
-template<class...Args>
- usingmy_set_value_t=
- completion_signatures<
- set_value_t(add_lvalue_reference_t<Args>...)>;
-
-usingmy_completion_signatures=
- make_completion_signatures<
- S,Env,
- completion_signatures<set_error_t(exception_ptr)>,
- my_set_value_t>;
-
-
-- end example]
-
-
This section makes use of the following exposition-only entities:
SetValue shall name an alias template such that for any template
-parameter pack As..., the type SetValue<As...> is either ill-formed
-or else valid-completion-signatures<SetValue<As...>> is satisfied.
-
-
SetError shall name an alias template such that for any type Err, SetError<Err> is either ill-formed or else valid-completion-signatures<SetError<Err>> is satisfied.
-
-
Then:
-
-
-
Let Vs... be a pack of the types in the type-list named
-by value_types_of_t<Sndr,Env,SetValue,type-list>.
-
-
Let Es... be a pack of the types in the type-list named by error_types_of_t<Sndr,Env,error-list>, where error-list is an
-alias template such that error-list<Ts...> names type-list<SetError<Ts>...>.
-
-
Let Ss name the type completion_signatures<> if sends_stopped<Sndr,Env> is false; otherwise, SetStopped.
-
-
Then:
-
-
-
If any of the above types are ill-formed, then make_completion_signatures<Sndr,Env,AddlSigs,SetValue,SetError,SetStopped> is ill-formed,
-
-
Otherwise, make_completion_signatures<Sndr,Env,AddlSigs,SetValue,SetError,SetStopped> names the type completion_signatures<Sigs...> where Sigs... is the unique set of types in all the template arguments
-of all the completion_signatures specializations in [AddlSigs,Vs...,Es...,Ss].
-
-
-
11.12. Execution contexts [exec.ctx]
-
-
-
This section specifies some execution resources on which work can be scheduled.
-
-
11.12.1. run_loop[exec.run.loop]
-
-
-
A run_loop is an execution resource on which work can be scheduled. It maintains a simple, thread-safe first-in-first-out queue of work. Its run() member function removes elements from the queue and executes them in a loop on whatever thread of execution calls run().
-
-
A run_loop instance has an associated count that corresponds to the number of work items that are in its queue. Additionally, a run_loop has an associated state that can be one of starting, running, or finishing.
-
-
Concurrent invocations of the member functions of run_loop, other than run and its destructor, do not introduce data races. The member functions pop_front, push_back, and finish execute atomically.
-
-
[Note: Implementations are encouraged to use an intrusive queue of operation states to hold the work units to make scheduling allocation-free. — end note]
-
classrun_loop{
- // [exec.run.loop.types] Associated types
- classrun-loop-scheduler;// exposition only
- classrun-loop-sender;// exposition only
- structrun-loop-opstate-base{// exposition only
- virtualvoidexecute()=0;
- run_loop*loop_;
- run-loop-opstate-base*next_;
- };
- template<receiver_of<completion_signatures<set_value_t()>>R>
- usingrun-loop-opstate=unspecified;// exposition only
-
- // [exec.run.loop.members] Member functions:
- run-loop-opstate-base*pop_front();// exposition only
- voidpush_back(run-loop-opstate-base*);// exposition only
-
- public:
- // [exec.run.loop.ctor] construct/copy/destroy
- run_loop()noexcept;
- run_loop(run_loop&&)=delete;
- ~run_loop();
-
- // [exec.run.loop.members] Member functions:
- run-loop-schedulerget_scheduler();
- voidrun();
- voidfinish();
-};
-
-
-
11.12.1.1. Associated types [exec.run.loop.types]
-
classrun-loop-scheduler;
-
-
-
-
run-loop-scheduler is an unspecified type that models the scheduler concept.
-
-
Instances of run-loop-scheduler remain valid until the end of the lifetime of the run_loop instance from which they were obtained.
-
-
Two instances of run-loop-scheduler compare equal if and only if they were obtained from the same run_loop instance.
-
-
Let sch be an expression of type run-loop-scheduler. The expression schedule(sch) is not potentially-throwing and has type run-loop-sender.
-
-
classrun-loop-sender;
-
-
-
-
run-loop-sender is an unspecified type such that sender_of<run-loop-sender,set_value_t()> is true. Additionally, the types reported by its error_types associated type is exception_ptr, and the value of its sends_stopped trait is true.
-
-
An instance of run-loop-sender remains valid until the end of the lifetime of its associated run_loop instance.
-
-
Let s be an expression of type run-loop-sender, let r be an expression such that decltype(r) models the receiver_of concept, and let C be either set_value_t or set_stopped_t. Then:
-
-
-
The expression connect(s,r) has type run-loop-opstate<decay_t<decltype(r)>> and is potentially-throwing if and only if the initialiation of decay_t<decltype(r)> from r is potentially-throwing.
-
-
The expression get_completion_scheduler<C>(get_env(s)) is not potentially-throwing, has type run-loop-scheduler, and compares equal to the run-loop-scheduler instance from which s was obtained.
-
-
-
template<receiver_of<completion_signatures<set_value_t()>>R>// arguments are not associated entities ([lib.tmpl-heads])
- structrun-loop-opstate;
-
-
-
-
run-loop-opstate<R> inherits unambiguously from run-loop-opstate-base.
-
-
Let o be a non-const lvalue of type run-loop-opstate<R>, and let REC(o) be a non-const lvalue reference to an instance of type R that was initialized with the expression r passed to the invocation of connect that returned o. Then:
-
-
-
The object to which REC(o) refers remains valid for the lifetime of the object to which o refers.
-
-
The type run-loop-opstate<R> overrides run-loop-opstate-base::execute() such that o.execute() is equivalent to the following:
as_awaitable transforms an object into one that is awaitable within a particular coroutine. This section makes use of the following exposition-only entities:
where ENV-OF(P) names the type env_of_t<P> if that type
-is well-formed, or empty_env otherwise.
-
-
-
Alias template single-sender-value-type is defined as follows:
-
-
-
If value_types_of_t<S,E,Tuple,Variant> would have the form Variant<Tuple<T>>, then single-sender-value-type<S,E> is an alias for type decay_t<T>.
-
-
Otherwise, if value_types_of_t<S,E,Tuple,Variant> would have the form Variant<Tuple<>> or Variant<>, then single-sender-value-type<S,E> is an alias for type void.
-
-
Otherwise, single-sender-value-type<S,E> is ill-formed.
-
-
-
The type sender-awaitable<S,P> is equivalent to the following:
Let r be an rvalue expression of type awaitable-receiver, let cr be a const lvalue that refers to r, let vs... be an arbitrary function parameter pack of types Vs..., and let err be an arbitrary expression of type Err. Then:
-
-
-
If constructible_from<result_t,Vs...> is satisfied, the expression set_value(r,vs...) is equivalent to:
err if decay_t<Err> names the same type as exception_ptr,
-
-
Otherwise, make_exception_ptr(system_error(err)) if decay_t<Err> names the same type as error_code,
-
-
Otherwise, make_exception_ptr(err).
-
-
-
The expression set_stopped(r) is equivalent to static_cast<coroutine_handle<>>(r.continuation_.promise().unhandled_stopped()).resume().
-
-
For any expression tag whose type satisfies forwarding-query and for any pack of subexpressions as, tag_invoke(tag,get_env(cr),as...) is expression-equivalent to tag(get_env(as_const(cr.continuation_.promise())),as...) when that expression is well-formed.
-
-
-
sender-awaitable::sender-awaitable(S&&s,P&p)
-
-
-
Effects: initializes state_ with connect(std::forward<S>(s),awaitable-receiver{&result_,coroutine_handle<P>::from_promise(p)}).
as_awaitable is a customization point object. For some subexpressions e and p where p is an lvalue, E names the type decltype((e)) and P names the type decltype((p)), as_awaitable(e,p) is expression-equivalent to the following:
-
-
-
tag_invoke(as_awaitable,e,p) if that expression is well-formed.
-
-
-
Mandates:is-awaitable<A,P> is true, where A is the type of the tag_invoke expression above.
-
-
-
Otherwise, e if is-awaitable<E,U> is true, where U is an unspecified class type that
-lacks a member named await_transform. The
-condition is not is-awaitable<E,P> as that
-creates the potential for constraint recursion.
-
-
-
Preconditions:is-awaitable<E,P> is true and the expression co_awaite in a coroutine with promise
-type U is expression-equivalent to the same
-expression in a coroutine with promise type P.
-
-
-
Otherwise, sender-awaitable{e,p} if awaitable-sender<E,P> is true.
with_awaitable_senders, when used as the base class of a coroutine promise type, makes senders awaitable in that coroutine type.
-
In addition, it provides a default implementation of unhandled_stopped() such that if a sender completes by calling set_stopped, it is treated as if an uncatchable "stopped" exception were thrown from the await-expression. In practice, the coroutine is never resumed, and the unhandled_stopped of the coroutine caller’s promise type is called.
ISO/IEC JTC1/SC22/WG21 14882: Programming Language — C++
-
Audience:
-
SG1, LEWG
-
-
-
-
-
-
-
-
1. Introduction
-
This paper proposes a self-contained design for a Standard C++ framework for managing asynchronous execution on generic execution resources. It is based on the ideas in A Unified Executors Proposal for C++ 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 needs 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 of concurrency primitives (std::atomic, std::mutex, std::counting_semaphore, etc) and lower level building blocks (std::thread, etc), we lack a Standard vocabulary and framework for asynchrony and parallelism that C++ programmers desperately need. std::async/std::future/std::promise, 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 resources.
-
-
Encapsulate common asynchronous patterns in customizable and reusable algorithms, so users don’t have to invent things themselves.
-
-
Make it easy to be correct by construction.
-
-
Support the diversity of execution resources and execution agents, because not all execution agents are created equal; some are less capable than others, but not less important.
-
-
Allow everything to be customized by an execution resource, including transfer to other execution resources, but don’t require that execution resources customize everything.
-
-
Care about all reasonable use cases, domains and platforms.
-
-
Errors must be propagated, but error handling must not present a burden.
-
-
Support cancellation, which is not an error.
-
-
Have clear and concise answers for where things execute.
-
-
Be able to manage and terminate the lifetimes of objects asynchronously.
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.19.1 execution::schedule, which returns a sender that completes on the scheduler. A sender describes asynchronous work and sends a signal (value, error, or stopped) to some recipient(s) when that work completes.
-
-
We use sender algorithms to produce senders and compose asynchronous work. § 4.20.2 execution::then is a sender adaptor that takes an input sender and a std::invocable, and calls the std::invocable on the signal sent by the input sender. The sender returned by then sends the result of that invocation. In this case, the input sender came from schedule, so its void, meaning it won’t send us a value, so our std::invocable takes no parameters. But we return an int, which will be sent to the next recipient.
-
-
Now, we add another operation to the chain, again using § 4.20.2 execution::then. This time, we get sent a value - the int from the previous step. We add 42 to it, and then return the result.
-
-
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.21.2 this_thread::sync_wait, which will either return a std::optional<std::tuple<...>> with the value sent by the last sender, or an empty std::optional if the last sender sent a stopped signal, or it throws an exception if the last sender sent an error.
This example builds an asynchronous computation of an inclusive scan:
-
-
-
It scans a sequence of doubles (represented as the std::span<constdouble>input) and stores the result in another sequence of doubles (represented as std::span<double>output).
-
-
It takes a scheduler, which specifies what execution resource the scan should be launched on.
-
-
It also takes a tile_count parameter that controls the number of execution agents that will be spawned.
-
-
First we need to allocate temporary storage needed for the algorithm, which we’ll do with a std::vector, partials. We need one double of temporary storage for each execution agent we create.
-
-
Next we’ll create our initial sender with § 4.19.2 execution::just and § 4.20.1 execution::transfer. These senders will send the temporary storage, which we’ve moved into the sender. The sender has a completion scheduler of sch, which means the next item in the chain will use sch.
-
-
Senders and sender adaptors support composition via operator|, similar to C++ ranges. We’ll use operator| to attach the next piece of work, which will spawn tile_count execution agents using § 4.20.9 execution::bulk (see § 4.12 Most sender adaptors are pipeable for details).
-
-
Each agent will call a std::invocable, passing it two arguments. The first is the agent’s index (i) in the § 4.20.9 execution::bulk operation, in this case a unique integer in [0,tile_count). The second argument is what the input sender sent - the temporary storage.
-
-
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 std::inclusive_scan over our elements. We store the scan result for our last element, which is the sum of all of our elements, in our temporary storage partials.
-
-
After all computation in that initial § 4.20.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 partials. We’ll do that with a single execution agent which will execute after the § 4.20.9 execution::bulk completes. We create that execution agent with § 4.20.2 execution::then.
-
-
§ 4.20.2 execution::then takes an input sender and an std::invocable and calls the std::invocable with the value sent by the input sender. Inside our std::invocable, we call std::inclusive_scan on partials, which the input senders will send to us.
-
-
Then we return partials, which the next phase will need.
-
-
Finally we do another § 4.20.9 execution::bulk of the same shape as before. In this § 4.20.9 execution::bulk, we will use the scanned values in partials to integrate the sums from other tiles into our elements, completing the inclusive scan.
-
-
async_inclusive_scan returns a sender that sends the output std::span<double>. A consumer of the algorithm can chain additional work that uses the scan result. At the point at which async_inclusive_scan returns, the computation may not have completed. In fact, it may not have even started.
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:
-
-
-
async_read 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 a std::span<std::byte>, and a handle to an I/O context. It will asynchronously read into the input buffer, up to the size of the std::span. It returns a sender which will send the number of bytes read once the read completes.
-
-
async_read_array 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 a dynamic_buffer object that owns the data that was sent.
-
-
dynamic_buffer is an aggregate struct that contains a std::unique_ptr<std::byte[]> and a size.
-
-
The first thing we do inside of async_read_array is create a sender that will send a new, empty dynamic_array object using § 4.19.2 execution::just. We can attach more work to the pipeline using operator| composition (see § 4.12 Most sender adaptors are pipeable for details).
-
-
We need the lifetime of this dynamic_array object to last for the entire pipeline. So, we use let_value, which takes an input sender and a std::invocable that must return a sender itself (see § 4.20.4 execution::let_* for details). let_value sends the value from the input sender to the std::invocable. Critically, the lifetime of the sent object will last until the sender returned by the std::invocable completes.
-
-
Inside of the let_valuestd::invocable, we have the rest of our logic. First, we want to initiate an async_read of the buffer size. To do that, we need to send a std::span pointing to buf.size. We can do that with § 4.19.2 execution::just.
Next, we pipe a std::invocable that will be invoked after the async_read completes using § 4.20.2 execution::then.
-
-
That std::invocable gets sent the number of bytes read.
-
-
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 std::span<std::byte> to the storage for the data from the std::invocable. This will be sent to the next recipient in the pipeline.
-
-
And that recipient will be another async_read, which will read the data.
-
-
Once the data has been read, in another § 4.20.2 execution::then, we confirm that we read the right number of bytes.
-
-
Finally, we move out of and return our dynamic_buffer object. It will get sent by the sender returned by async_read_array. We can attach more things to that sender to use the data in the buffer.
-
-
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 async_recv() operation for a Windows Socket.
-
structoperation_base:WSAOVERALAPPED{
- usingcompletion_fn=void(operation_base*op,DWORDbytesTransferred,interrorCode)noexcept;
-
- // Assume IOCP event loop will call this when this OVERLAPPED structure is dequeued.
- completion_fn*completed;
-};
-
-template<typenameReceiver>
-structrecv_op:operation_base{
- recv_op(SOCKETs,void*data,size_tlen,Receiverr)
- :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);
- }
-
- friendvoidtag_invoke(std::execution::start_t,recv_op&self)noexcept{
- // Avoid even calling WSARecv() if operation already cancelled
- autost=std::execution::get_stop_token(
- std::execution::get_env(self.receiver));
- if(st.stop_requested()){
- std::execution::set_stopped(std::move(self.receiver));
- return;
- }
-
- // Store and cache result here in case it changes during execution
- constboolstopPossible=st.stop_possible();
- if(!stopPossible){
- self.ready.store(true,std::memory_order_relaxed);
- }
-
- // Launch the operation
- DWORDbytesTransferred=0;
- DWORDflags=0;
- intresult=WSARecv(self.sock,&self.buffer,1,&bytesTransferred,&flags,
- static_cast<WSAOVERLAPPED*>(&self), NULL);
- if(result==SOCKET_ERROR){
- interrorCode=WSAGetLastError();
- if(errorCode!=WSA_IO_PENDING){
- if(errorCode==WSA_OPERATION_ABORTED){
- std::execution::set_stopped(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();
-
- BOOLok=WSAGetOverlappedResult(self.sock,(WSAOVERLAPPED*)&self,&bytesTransferred,FALSE,&flags);
- if(ok){
- std::execution::set_value(std::move(self.receiver),bytesTransferred);
- }else{
- interrorCode=WSAGetLastError();
- std::execution::set_error(std::move(self.receiver),
- std::error_code(errorCode,std::system_category()));
- }
- }
- }
- }
-
- structcancel_cb{
- recv_op&op;
-
- voidoperator()()noexcept{
- CancelIoEx((HANDLE)op.sock,(OVERLAPPED*)(WSAOVERLAPPED*)&op);
- }
- };
-
- staticvoidon_complete(operation_base*op,DWORDbytesTransferred,interrorCode)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()));
- }
- }
- }
-
- Receiverreceiver;
- SOCKETsock;
- WSABUFbuffer;
- std::optional<typenamestop_callback_type_t<Receiver>
- ::templatecallback_type<cancel_cb>>stopCallback;
- std::atomic<bool>ready{false};
-};
-
-structrecv_sender{
- usingsender_concept=std::execution::sender_t;
- SOCKETsock;
- void*data;
- size_tlen;
-
- template<typenameReceiver>
- friendrecv_op<Receiver>tag_invoke(std::execution::connect_t,
- constrecv_sender&s,
- Receiverr){
- returnrecv_op<Receiver>{s.sock,s.data,s.len,std::move(r)};
- }
-};
-
-recv_senderasync_recv(SOCKETs,void*data,size_tlen){
- returnrecv_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.
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.
In this code, NN::async_read_some and NN::async_write_some are asynchronous socket-based networking APIs that return senders. EX::repeat_effect_until, EX::let_value, and EX::then 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 repeat_effect_until 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.
This code builds a then 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 (set_error and set_stopped), as well as all receiver queries,
-are passed through unchanged.
-
In detail, it does the following:
-
-
-
Defines a receiver in terms of execution::receiver_adaptor that aggregates
-another receiver and an invocable that:
-
-
-
Defines a constrained tag_invoke overload for transforming the value
-channel.
-
-
Defines another constrained overload of tag_invoke that passes all other
-customizations through unchanged.
-
-
The tag_invoke overloads are actually implemented by execution::receiver_adaptor; they dispatch either to named members, as
-shown above with _then_receiver::set_value, or to the adapted receiver.
-
-
Defines a sender that aggregates another sender and the invocable, which defines a tag_invoke customization for std::execution::connect that wraps the incoming receiver in the receiver from (1) and passes it and the incoming sender to std::execution::connect, returning the result. It also defines a tag_invoke customization of get_completion_signatures that declares the sender’s completion signatures when executed within a particular environment.
-
-
1.5.2. retry
-
usingnamespacestd;
-namespaceexec=execution;
-
-template<classFrom,classTo>
-concept_decays_to=same_as<decay_t<From>,To>;
-
-// _conv needed so we can emplace construct non-movable types into
-// a std::optional.
-template<invocableF>
- requiresis_nothrow_move_constructible_v<F>
-struct_conv{
- Ff_;
- explicit_conv(Ff)noexcept:f_((F&&)f){}
- operatorinvoke_result_t<F>()&&{
- return((F&&)f_)();
- }
-};
-
-template<classS,classR>
-struct_op;
-
-// pass through all customizations except set_error, which retries the operation.
-template<classS,classR>
-struct_retry_receiver
- :exec::receiver_adaptor<_retry_receiver<S,R>>{
- _op<S,R>*o_;
-
- R&&base()&&noexcept{return(R&&)o_->r_;}
- constR&base()const&noexcept{returno_->r_;}
-
- explicit_retry_receiver(_op<S,R>*o):o_(o){}
-
- voidset_error(auto&&)&&noexcept{
- o_->_retry();// This causes the op to be retried
- }
-};
-
-// Hold the nested operation state in an optional so we can
-// re-construct and re-start it if the operation fails.
-template<classS,classR>
-struct_op{
- Ss_;
- Rr_;
- optional<
- exec::connect_result_t<S&,_retry_receiver<S,R>>>o_;
-
- _op(Ss,Rr):s_((S&&)s),r_((R&&)r),o_{_connect()}{}
- _op(_op&&)=delete;
-
- auto_connect()noexcept{
- return_conv{[this]{
- returnexec::connect(s_,_retry_receiver<S,R>{this});
- }};
- }
- void_retry()noexcepttry{
- o_.emplace(_connect());// potentially-throwing
- exec::start(*o_);
- }catch(...){
- exec::set_error((R&&)r_,std::current_exception());
- }
- friendvoidtag_invoke(exec::start_t,_op&o)noexcept{
- exec::start(*o.o_);
- }
-};
-
-template<classS>
-struct_retry_sender{
- usingsender_concept=exec::sender_t;
- Ss_;
- explicit_retry_sender(Ss):s_((S&&)s){}
-
- template<class...Ts>
- using_value_t=
- exec::completion_signatures<exec::set_value_t(Ts...)>;
- template<class>
- using_error_t=exec::completion_signatures<>;
-
- // Declare the signatures with which this sender can complete
- template<classEnv>
- friendautotag_invoke(exec::get_completion_signatures_t,const_retry_sender&,Env)
- ->exec::transform_completion_signatures_of<S&,Env,
- exec::completion_signatures<exec::set_error_t(std::exception_ptr)>,
- _value_t,_error_t>;
-
- template<exec::receiverR>
- friend_op<S,R>tag_invoke(exec::connect_t,_retry_sender&&self,Rr){
- return{(S&&)self.s_,(R&&)r};
- }
-
- frienddecltype(auto)tag_invoke(exec::get_env_t,const_retry_sender&self)noexcept{
- returnget_env(self.s_);
- }
-};
-
-template<exec::senderS>
-exec::senderautoretry(Ss){
- return_retry_sender{(S&&)s};
-}
-
-
The retry algorithm takes a multi-shot sender and causes it to repeat on error, passing
-through values and stopped signals. Each time the input sender is restarted, a new receiver
-is connected and the resulting operation state is stored in an optional, which allows us
-to reinitialize it multiple times.
-
This example does the following:
-
-
-
Defines a _conv utility that takes advantage of C++17’s guaranteed copy elision to
-emplace a non-movable type in a std::optional.
-
-
Defines a _retry_receiver 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 set_error, which causes a _retry() function to be called instead.
-
-
Defines an operation state that aggregates the input sender and receiver, and declares
-storage for the nested operation state in an optional. Constructing the operation
-state constructs a _retry_receiver with a pointer to the (under construction) operation
-state and uses it to connect to the aggregated sender.
-
-
Starting the operation state dispatches to start on the inner operation state.
-
-
The _retry() 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.
-
-
After reinitializing the inner operation state, _retry() calls start on it, causing
-the failed operation to be rescheduled.
-
-
Defines a _retry_sender that implements the connect customization point to return
-an operation state constructed from the passed-in sender and receiver.
-
-
_retry_sender also implements the get_completion_signatures customization point to describe the ways this sender may complete when executed in a particular execution resource.
-
-
1.6. Examples: Schedulers
-
In this section we look at some schedulers of varying complexity.
The inline scheduler is a trivial scheduler that completes immediately and synchronously on
-the thread that calls std::execution::start on the operation state produced by its sender.
-In other words, start(connect(schedule(inline-scheduler),receiver)) is
-just a fancy way of saying set_value(receiver), with the exception of the fact that start wants to be passed an lvalue.
-
Although not a particularly useful scheduler, it serves to illustrate the basics of
-implementing one. The inline_scheduler:
-
-
-
Customizes execution::schedule to return an instance of the sender type _sender.
-
-
The _sender type models the sender concept and provides the metadata
-needed to describe it as a sender of no values
-and that never calls set_error or set_stopped. This
-metadata is provided with the help of the execution::completion_signatures utility.
-
-
The _sender type customizes execution::connect to accept a receiver of no
-values. It returns an instance of type _op that holds the receiver by
-value.
-
-
The operation state customizes std::execution::start to call std::execution::set_value on the receiver.
-
-
1.6.2. Single thread scheduler
-
This example shows how to create a scheduler for an execution resource that consists of a single
-thread. It is implemented in terms of a lower-level execution resource called std::execution::run_loop.
The single_thread_context 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 execution::run_loop 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 this_thread::sync_wait, 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. Examples: Server theme
-
In this section we look at some examples of how one would use senders to implement an HTTP server. The examples ignore the low-level details of the HTTP server and looks at how senders can be combined to achieve the goals of the project.
-
General application context:
-
-
-
server application that processes images
-
-
execution resources:
-
-
-
1 dedicated thread for network I/O
-
-
N worker threads used for CPU-intensive work
-
-
M threads for auxiliary I/O
-
-
optional GPU context that may be used on some types of servers
-
-
-
all parts of the applications can be asynchronous
-
-
no locks shall be used in user code
-
-
1.7.1. Composability with execution::let_*
-
Example context:
-
-
-
we are looking at the flow of processing an HTTP request and sending back the response
-
-
show how one can break the (slightly complex) flow into steps with execution::let_* functions
-
-
different phases of processing HTTP requests are broken down into separate concerns
-
-
each part of the processing might use different execution resources (details not shown in this example)
-
-
error handling is generic, regardless which component fails; we always send the right response to the clients
-
-
Goals:
-
-
-
show how one can break more complex flows into steps with let_* functions
-
-
exemplify the use of let_value, let_error, let_stopped, and just algorithms
-
-
namespaceex=std::execution;
-
-// Returns a sender that yields an http_request object for an incoming request
-ex::senderautoschedule_request_start(read_requests_ctxctx){...}
-// Sends a response back to the client; yields a void signal on success
-ex::senderautosend_response(consthttp_response&resp){...}
-// Validate that the HTTP request is well-formed; forwards the request on success
-ex::senderautovalidate_request(consthttp_request&req){...}
-
-// Handle the request; main application logic
-ex::senderautohandle_request(consthttp_request&req){
- //...
- returnex::just(http_response{200,result_body});
-}
-
-// Transforms server errors into responses to be sent to the client
-ex::senderautoerror_to_response(std::exception_ptrerr){
- try{
- std::rethrow_exception(err);
- }catch(conststd::invalid_argument&e){
- returnex::just(http_response{404,e.what()});
- }catch(conststd::exception&e){
- returnex::just(http_response{500,e.what()});
- }catch(...){
- returnex::just(http_response{500,"Unknown server error"});
- }
-}
-// Transforms cancellation of the server into responses to be sent to the client
-ex::senderautostopped_to_response(){
- returnex::just(http_response{503,"Service temporarily unavailable"});
-}
-//...
-// The whole flow for transforming incoming requests into responses
-ex::senderautosnd=
- // get a sender when a new request comes
- schedule_request_start(the_read_requests_ctx)
- // make sure the request is valid; throw if not
- |ex::let_value(validate_request)
- // process the request in a function that may be using a different execution resource
- |ex::let_value(handle_request)
- // If there are errors transform them into proper responses
- |ex::let_error(error_to_response)
- // If the flow is cancelled, send back a proper response
- |ex::let_stopped(stopped_to_response)
- // write the result back to the client
- |ex::let_value(send_response)
- // done
- ;
-// execute the whole flow asynchronously
-ex::start_detached(std::move(snd));
-
-
The example shows how one can separate out the concerns for interpreting requests, validating requests, running the main logic for handling the request, generating error responses, handling cancellation and sending the response back to the client.
-They are all different phases in the application, and can be joined together with the let_* functions.
-
All our functions return execution::sender objects, so that they can all generate success, failure and cancellation paths.
-For example, regardless where an error is generated (reading request, validating request or handling the response), we would have one common block to handle the error, and following error flows is easy.
-
Also, because of using execution::sender objects at any step, we might expect any of these steps to be completely asynchronous; the overall flow doesn’t care.
-Regardless of the execution resource in which the steps, or part of the steps are executed in, the flow is still the same.
-
1.7.2. Moving between execution resources with execution::on and execution::transfer
-
Example context:
-
-
-
reading data from the socket before processing the request
-
-
reading of the data is done on the I/O context
-
-
no processing of the data needs to be done on the I/O context
-
-
Goals:
-
-
-
show how one can change the execution resource
-
-
exemplify the use of on and transfer algorithms
-
-
namespaceex=std::execution;
-
-size_tlegacy_read_from_socket(intsock,char*buffer,size_tbuffer_len){}
-voidprocess_read_data(constchar*read_data,size_tread_len){}
-//...
-
-// A sender that just calls the legacy read function
-autosnd_read=ex::just(sock,buf,buf_len)|ex::then(legacy_read_from_socket);
-// The entire flow
-autosnd=
- // start by reading data on the I/O thread
- ex::on(io_sched,std::move(snd_read))
- // do the processing on the worker threads pool
- |ex::transfer(work_sched)
- // process the incoming data (on worker threads)
- |ex::then([buf](intread_len){process_read_data(buf,read_len);})
- // done
- ;
-// execute the whole flow asynchronously
-ex::start_detached(std::move(snd));
-
-
The example assume that we need to wrap some legacy code of reading sockets, and handle execution resource switching.
-(This style of reading from socket may not be the most efficient one, but it’s working for our purposes.)
-For performance reasons, the reading from the socket needs to be done on the I/O thread, and all the processing needs to happen on a work-specific execution resource (i.e., thread pool).
-
Calling execution::on will ensure that the given sender will be started on the given scheduler.
-In our example, snd_read is going to be started on the I/O scheduler.
-This sender will just call the legacy code.
-
The completion-signal will be issued in the I/O execution resource, so we have to move it to the work thread pool.
-This is achieved with the help of the execution::transfer algorithm.
-The rest of the processing (in our case, the last call to then) will happen in the work thread pool.
-
The reader should notice the difference between execution::on and execution::transfer.
-The execution::on algorithm will ensure that the given sender will start in the specified context, and doesn’t care where the completion-signal for that sender is sent.
-The execution::transfer algorithm will not care where the given sender is going to be started, but will ensure that the completion-signal of will be transferred to the given context.
-
1.8. What this proposal is not
-
This paper is not a patch on top of A Unified Executors Proposal for C++; 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 A Unified Executors Proposal for C++; 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.9. Design changes from P0443
-
-
-
The executor concept has been removed and all of its proposed functionality
-is now based on schedulers and senders, as per SG1 direction.
-
-
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.
This paper extends the sender traits/typed sender design to support typed
-senders whose value/error types depend on type information provided late via
-the receiver.
-
-
Support for untyped senders is dropped; the typed_sender concept is renamed sender; sender_traits is replaced with completion_signatures_of_t.
-
-
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.
-
-
Some additional utilities are added:
-
-
-
run_loop: An execution resource that provides a multi-producer,
-single-consumer, first-in-first-out work queue.
-
-
receiver_adaptor: A utility for algorithm authors for defining one
-receiver type in terms of another.
-
-
completion_signatures and transform_completion_signatures:
-Utilities for describing the ways in which a sender can complete in a
-declarative syntax.
-
-
-
1.10. 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.10.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.10.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 Halo: coroutine Heap Allocation eLision Optimization: the joint response, 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.10.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.11. Field experience
-
1.11.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 (with slight variations -- the design of P2300 has evolved due to LEWG feedback), many of this paper’s algorithms (some under different names), and much more besides.
-
Libunifex has several concrete schedulers in addition to the run_loop suggested here (where it is called manual_event_loop). 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 resource 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.11.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 resource. In HPX, algorithms can migrate execution between execution resources, 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 Simberg 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 resources.
-
-
The adaptors. Things like let_value are really nice additions.
-
-
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 f2 in the continuation will always be ready here f1.then([](future<T>f2){...}) is enough of a reason to separate the channels. All the other obvious reasons apply as well of course.
-
-
For futures we have a thing called hpx::dataflow which is an optimized version of when_all(...).then(...) which avoids intermediate allocations. With the sender/receiver when_all(...)|then(...) we get that "for free".
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 complete 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. It is
-fit for broad use and for contribution to libc++.
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.11.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:
get_env and the associated environment utilities are moved back into std::execution from std::.
-
-
make_completion_signatures is renamed transform_completion_signatures_of and is expressed in terms of the new transform_completion_signatures,
-which takes an input set of completion signatures instead of a sender and an
-environment.
-
-
Add a requirement on queryable objects that if tag_invoke(query,env,args...) is well-formed, then query(env,args...) is
-expression-equivalent to it. This is necessary to properly specify how to
-join two environments in the presence of queries that have defaults.
-
-
The sender_in<Sndr,Env> concept requires that E satisfies queryable.
-
-
Senders of more than one value are now co_await-able in coroutines, the
-result of which is a std::tuple of the values (which is suitable as the
-initializer of a structured binding).
-
-
Enhancements:
-
-
-
The exposition-only class template basic-sender is greatly
-enhanced, and the sender algorithms are respecified in term of it.
-
-
enable_sender and enable_receiver traits now have default
-implementations that look for nested sender_concept and receiver_concept types, respectively.
-
-
2.2. R7
-
The changes since R6 are as follows:
-
Fixes:
-
-
-
Make it valid to pass non-variadic templates to the exposition-only alias
-template gather-signatures, fixing the definitions of value_types_of_t, error_types_of_t, and the exposition-only alias
-template sync-wait-type.
-
-
Removed the query forwarding from receiver_adaptor that was
-inadvertantly left over from a previous edit.
-
-
When adapting a sender to an awaitable with as_awaitable, the sender’s
-value result datum is decayed before being stored in the exposition-only variant.
-
-
Correctly specify the completion signatures of the schedule_from algorithm.
-
-
The sender_of concept no longer distinguishes between a sender of a
-type T and a sender of a type T&&.
-
-
The just and just_error sender factories now reject C-style arrays
-instead of silently decaying them to pointers.
-
-
Enhancements:
-
-
-
The sender and receiver concepts get explicit opt-in traits called enable_sender and enable_receiver, respectively. The traits have
-default implementations that look for nested is_sender and is_receiver types, respectively.
-
-
get_attrs is removed and get_env is used in its place.
-
-
The exposition-only type empty-env is made normative
-and is renamed empty_env.
-
-
get_env gets a fall-back implementation that simply returns empty_env{} if a tag_invoke overload is not found.
-
-
get_env is required to be insensitive to the cvref-qualification of its
-argument.
-
-
get_env, empty_env, and env_of_t are moved into the std:: namespace.
Fix typo in the specification of in_place_stop_source about the relative
-lifetimes of the tokens and the source that produced them.
-
-
get_completion_signatures tests for awaitability with a promise type
-similar to the one used by connect for the sake of consistency.
-
-
A coroutine promise type is an environment provider (that is, it implements get_env()) rather than being directly queryable. The previous draft was
-inconsistent about that.
-
-
Enhancements:
-
-
-
Sender queries are moved into a separate queryable "attributes" object
-that is accessed by passing the sender to get_attrs() (see below). The sender concept is reexpressed to require get_attrs() and separated
-from a new sender_in<Snd,Env> concept for checking whether a type is
-a sender within a particular execution environment.
-
-
The placeholder types no_env and dependent_completion_signatures<> are no longer needed and are dropped.
-
-
ensure_started and split are changed to persist the result of
-calling get_attrs() on the input sender.
-
-
Reorder constraints of the scheduler and receiver concepts to avoid constraint recursion
-when used in tandem with poorly-constrained, implicitly convertible types.
-
-
Re-express the sender_of concept to be more ergonomic and general.
-
-
Make the specification of the alias templates value_types_of_t and error_types_of_t, and the variable template sends_done more concise by
-expressing them in terms of a new exposition-only alias template gather-signatures.
-
-
2.3.1. Environments and attributes
-
In earlier revisions, receivers, senders, and schedulers all were directly
-queryable. In R4, receiver queries were moved into a separate "environment"
-object, obtainable from a receiver with a get_env accessor. In R6, the
-sender queries are given similar treatment, relocating to a "attributes"
-object obtainable from a sender with a get_attrs accessor. This was done
-to solve a number of design problems with the split and ensure_started algorithms; _e.g._, see NVIDIA/stdexec#466.
-
Schedulers, however, remain directly queryable. As lightweight handles
-that are required to be movable and copyable, there is little reason to
-want to dispose of a scheduler and yet persist the scheduler’s queries.
-
This revision also makes operation states directly queryable, even though
-there isn’t yet a use for such. Some early prototypes of cooperative bulk
-parallel sender algorithms done at NVIDIA suggest the utility of
-forwardable operation state queries. The authors chose to make opstates
-directly queryable since the opstate object is itself required to be kept
-alive for the duration of asynchronous operation.
-
2.4. R5
-
The changes since R4 are as follows:
-
Fixes:
-
-
-
start_detached requires its argument to be a void sender (sends no values
-to set_value).
-
-
Enhancements:
-
-
-
Receiver concepts refactored to no longer require an error channel for exception_ptr or a stopped channel.
-
-
sender_of concept and connect customization point additionally require
-that the receiver is capable of receiving all of the sender’s possible
-completions.
-
-
get_completion_signatures is now required to return an instance of either completion_signatures or dependent_completion_signatures.
-
-
make_completion_signatures made more general.
-
-
receiver_adaptor handles get_env as it does the set_* members; that is, receiver_adaptor will look for a member named get_env() in the derived
-class, and if found dispatch the get_env_t tag invoke customization to it.
-
-
just, just_error, just_stopped, and into_variant have been respecified
-as customization point objects instead of functions, following LEWG guidance.
-
-
2.5. R4
-
The changes since R3 are as follows:
-
Fixes:
-
-
-
Fix specification of get_completion_scheduler on the transfer, schedule_from and transfer_when_all algorithms; the completion scheduler cannot be guaranteed
-for set_error.
-
-
The value of sends_stopped for the default sender traits of types that are
-generally awaitable was changed from false to true to acknowledge the
-fact that some coroutine types are generally awaitable and may implement the unhandled_stopped() protocol in their promise types.
-
-
Fix the incorrect use of inline namespaces in the <execution> header.
-
-
Shorten the stable names for the sections.
-
-
sync_wait now handles std::error_code specially by throwing a std::system_error on failure.
-
-
Fix how ADL isolation from class template arguments is specified so it
-doesn’t constrain implmentations.
-
-
Properly expose the tag types in the header <execution> synopsis.
-
-
Enhancements:
-
-
-
Support for "dependently-typed" senders, where the completion signatures -- and
-thus the sender metadata -- depend on the type of the receiver connected
-to it. See the section dependently-typed
-senders below for more information.
-
-
Add a read(query) sender factory for issuing a query
-against a receiver and sending the result through the value channel. (This is
-a useful instance of a dependently-typed sender.)
-
-
Add completion_signatures utility for declaratively defining a typed
-sender’s metadata.
-
-
Add make_completion_signatures utility for specifying a sender’s completion
-signatures by adapting those of another sender.
-
-
Drop support for untyped senders and rename typed_sender to sender.
-
-
set_done is renamed to set_stopped. All occurances of "done" in
-indentifiers replaced with "stopped"
-
-
Add customization points for controlling the forwarding of scheduler,
-sender, receiver, and environment queries through layers of adaptors;
-specify the behavior of the standard adaptors in terms of the new
-customization points.
-
-
Add get_delegatee_scheduler query to forward a scheduler that can be used
-by algorithms or by the scheduler to delegate work and forward progress.
-
-
Add schedule_result_t alias template.
-
-
More precisely specify the sender algorithms, including precisely what their
-completion signatures are.
-
-
stopped_as_error respecified as a customization point object.
-
-
tag_invoke respecified to improve diagnostics.
-
-
2.5.1. Dependently-typed senders
-
Background:
-
In the sender/receiver model, as with coroutines, contextual information about
-the current execution is most naturally propagated from the consumer to the
-producer. In coroutines, that means information like stop tokens, allocators and
-schedulers are propagated from the calling coroutine to the callee. In
-sender/receiver, that means that that contextual information is associated with
-the receiver and is queried by the sender and/or operation state after the
-sender and the receiver are connect-ed.
-
Problem:
-
The implication of the above is that the sender alone does not have all the
-information about the async computation it will ultimately initiate; some of
-that information is provided late via the receiver. However, the sender_traits mechanism, by which an algorithm can introspect the value and error types the
-sender will propagate, only accepts a sender parameter. It does not take into
-consideration the type information that will come in late via the receiver. The
-effect of this is that some senders cannot be typed senders when they
-otherwise could be.
-
Example:
-
To get concrete, consider the case of the "get_scheduler()" sender: when connect-ed and start-ed, it queries the receiver for its associated
-scheduler and passes it back to the receiver through the value channel. That
-sender’s "value type" is the type of the receiver’s scheduler. What then
-should sender_traits<get_scheduler_sender>::value_types report for the get_scheduler()'s value type? It can’t answer because it doesn’t know.
-
This causes knock-on problems since some important algorithms require a typed
-sender, such as sync_wait. To illustrate the problem, consider the following
-code:
-
namespaceex=std::execution;
-
-ex::senderautotask=
- ex::let_value(
- ex::get_scheduler(),// Fetches scheduler from receiver.
- [](autocurrent_sched){
- // Lauch some nested work on the current scheduler:
- returnex::on(current_sched,nestedwork...);
- });
-
-std::this_thread::sync_wait(std::move(task));
-
-
The code above is attempting to schedule some work onto the sync_wait's run_loop execution resource. But let_value only returns a typed sender when
-the input sender is typed. As we explained above, get_scheduler() is not
-typed, so task is likewise not typed. Since task isn’t typed, it cannot be
-passed to sync_wait which is expecting a typed sender. The above code would
-fail to compile.
-
Solution:
-
The solution is conceptually quite simple: extend the sender_traits mechanism
-to optionally accept a receiver in addition to the sender. The algorithms can
-use sender_traits<Sender,Receiver> to inspect the
-async operation’s completion-signals. The typed_sender concept would also need
-to take an optional receiver parameter. This is the simplest change, and it
-would solve the immediate problem.
-
Design:
-
Using the receiver type to compute the sender traits turns out to have pitfalls
-in practice. Many receivers make use of that type information in their
-implementation. It is very easy to create cycles in the type system, leading to
-inscrutible errors. The design pursued in R4 is to give receivers an associated environment object -- a bag of key/value pairs -- and to move the contextual
-information (schedulers, etc) out of the receiver and into the environment. The sender_traits template and the typed_sender concept, rather than taking a
-receiver, take an environment. This is a much more robust design.
-
A further refinement of this design would be to separate the receiver and the
-environment entirely, passing then as separate arguments along with the sender to connect. This paper does not propose that change.
-
Impact:
-
This change, apart from increasing the expressive power of the sender/receiver abstraction, has the following impact:
-
-
-
Typed senders become moderately more challenging to write. (The new completion_signatures and transform_completion_signatures utilities are added
-to ease this extra burden.)
-
-
Sender adaptor algorithms that previously constrained their sender arguments
-to satisfy the typed_sender concept can no longer do so as the receiver is
-not available yet. This can result in type-checking that is done later, when connect is ultimately called on the resulting sender adaptor.
-
-
Operation states that own receivers that add to or change the environment
-are typically larger by one pointer. It comes with the benefit of far fewer
-indirections to evaluate queries.
-
-
"Has it been implemented?"
-
Yes, the reference implementation, which can be found at
-https://github.com/NVIDIA/stdexec, has implemented this
-design as well as some dependently-typed senders to confirm that it works.
-
Implementation experience
-
Although this change has not yet been made in libunifex, the most widely adopted sender/receiver implementation, a similar design can be found in Folly’s coroutine support library. In Folly.Coro, it is possible to await a special awaitable to obtain the current coroutine’s associated scheduler (called an executor in Folly).
-
For instance, the following Folly code grabs the current executor, schedules a task for execution on that executor, and starts the resulting (scheduled) task by enqueueing it for execution.
-
// From Facebook’s Folly open source library:
-template<classT>
-folly::coro::Task<void>CancellableAsyncScope::co_schedule(folly::coro::Task<T>&&task){
- this->add(std::move(task).scheduleOn(co_awaitco_current_executor));
- co_return;
-}
-
-
Facebook relies heavily on this pattern in its coroutine code. But as described
-above, this pattern doesn’t work with R3 of std::execution because of the lack
-of dependently-typed schedulers. The change to sender_traits in R4 rectifies that.
-
Why now?
-
The authors are loathe to make any changes to the design, however small, at this
-stage of the C++23 release cycle. But we feel that, for a relatively minor
-design change -- adding an extra template parameter to sender_traits and typed_sender -- the returns are large enough to justify the change. And there
-is no better time to make this change than as early as possible.
-
One might wonder why this missing feature not been added to sender/receiver
-before now. The designers of sender/receiver have long been aware of the need.
-What was missing was a clean, robust, and simple design for the change, which we
-now have.
-
Drive-by:
-
We took the opportunity to make an additional drive-by change: Rather than
-providing the sender traits via a class template for users to specialize, we
-changed it into a sender query: get_completion_signatures(sender,env). That function’s return type is used as the sender’s traits.
-The authors feel this leads to a more uniform design and gives sender authors a
-straightforward way to make the value/error types dependent on the cv- and
-ref-qualification of the sender if need be.
-
Details:
-
Below are the salient parts of the new support for dependently-typed senders in
-R4:
-
-
-
Receiver queries have been moved from the receiver into a separate environment
-object.
-
-
Receivers have an associated environment. The new get_env CPO retrieves a
-receiver’s environment. If a receiver doesn’t implement get_env, it returns
-an unspecified "empty" environment -- an empty struct.
-
-
sender_traits now takes an optional Env parameter that is used to
-determine the error/value types.
-
-
The primary sender_traits template is replaced with a completion_signatures_of_t alias implemented in terms of a new get_completion_signatures CPO that dispatches
-with tag_invoke. get_completion_signatures takes a sender and an optional
-environment. A sender can customize this to specify its value/error types.
-
-
Support for untyped senders is dropped. The typed_sender concept has been
-renamed to sender and now takes an optional environment.
-
-
The environment argument to the sender concept and the get_completion_signatures CPO defaults to no_env. All environment queries fail (are ill-formed) when
-passed an instance of no_env.
-
-
A type S is required to satisfy sender<S> to be
-considered a sender. If it doesn’t know what types it will complete with
-independent of an environment, it returns an instance of the placeholder
-traits dependent_completion_signatures.
-
-
If a sender satisfies both sender<S> and sender<S,Env>, then the completion signatures
-for the two cannot be different in any way. It is possible for an
-implementation to enforce this statically, but not required.
-
-
All of the algorithms and examples have been updated to work with
-dependently-typed senders.
-
-
2.6. R3
-
The changes since R2 are as follows:
-
Fixes:
-
-
-
Fix specification of the on algorithm to clarify lifetimes of
-intermediate operation states and properly scope the get_scheduler query.
-
-
Fix a memory safety bug in the implementation of connect-awaitable.
-
-
Fix recursive definition of the scheduler concept.
-
-
Enhancements:
-
-
-
Add run_loop execution resource.
-
-
Add receiver_adaptor utility to simplify writing receivers.
-
-
Require a scheduler’s sender to model sender_of and provide a completion scheduler.
-
-
Specify the cancellation scope of the when_all algorithm.
-
-
Make as_awaitable a customization point.
-
-
Change connect's handling of awaitables to consider those types that are awaitable owing to customization of as_awaitable.
-
-
Add value_types_of_t and error_types_of_t alias templates; rename stop_token_type_t to stop_token_of_t.
-
-
Add a design rationale for the removal of the possibly eager algorithms.
-
-
Expand the section on field experience.
-
-
2.7. R2
-
The changes since R1 are as follows:
-
-
-
Remove the eagerly executing sender algorithms.
-
-
Extend the execution::connect customization point and the sender_traits<> template to recognize awaitables as typed_senders.
-
-
Add utilities as_awaitable() and with_awaitable_senders<> so a coroutine type can trivially make senders awaitable with a coroutine.
-
-
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 bulk algorithm.
-
-
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 execution::just_error and execution::just_stopped.
-
-
Add sender adaptors execution::stopped_as_optional and execution::stopped_as_error.
-
-
Document more production uses of sender/receiver at scale.
-
-
Various fixes of typos and bugs.
-
-
2.8. 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 unschedule adaptor.
-
-
Various fixes of typos and bugs.
-
-
2.9. R0
-
Initial revision.
-
3. Design - introduction
-
The following three 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 A Unified Executors Proposal for C++: std::execution; however, for brevity, the std:: part of this name is
- omitted. When you see execution::foo, treat that as std::execution::foo.
-
-
Universal references and explicit calls to std::move/std::forward are
- omitted in code samples and signatures for simplicity; assume universal
- references and perfect forwarding unless stated otherwise.
-
-
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 callable 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 callable 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 resources describe the place of execution
-
An execution resource 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 resources
-
A scheduler is a lightweight handle that represents a strategy for
-scheduling work onto an execution resource. Since execution resources 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, schedule, which returns a sender that
-will complete on an execution resource determined by the scheduler. Logic that
-you want to run on that context can be placed in the receiver’s
-completion-signalling method.
-
execution::schedulerautosch=thread_pool.scheduler();
-execution::senderautosnd=execution::schedule(sch);
-// snd is a sender (see below) describing the creation of a new execution resource
-// on the execution resource associated with sch
-
-
Note that a particular scheduler type may provide other kinds of scheduling operations
-which are supported by its associated execution resource. It is not limited to scheduling
-purely using the execution::schedule API.
-
Future papers will propose additional scheduler concepts that extend scheduler to add other capabilities. For example:
-
-
-
A time_scheduler concept that extends scheduler to support time-based
-scheduling. Such a concept might provide access to schedule_after(sched,duration), schedule_at(sched,time_point) and now(sched) APIs.
-
-
Concepts that extend scheduler to support opening, reading and writing files
-asynchronously.
-
-
Concepts that extend scheduler to support connecting, sending data and
-receiving data over the network asynchronously.
-
-
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.
execution::schedulerautosch=thread_pool.scheduler();
-execution::senderautosnd=execution::schedule(sch);
-execution::senderautocont=execution::then(snd,[]{
- std::fstreamfile{"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 execution::connect) senders and return a sender.
-
-
sender consumers, algorithms that take (and potentially execution::connect) senders and do not return a sender.
-
-
4.5. Senders can propagate completion schedulers
-
One of the goals of executors is to support a diverse set of execution resources, 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 resource 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.
-
A Unified Executors Proposal for C++ 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 (Towards C++23 executors: A proposal for an initial set of algorithms) 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 resource (such as a CPU thread pool) from another execution resource (such as a GPU or a task framework), which assumes that all execution agents are as capable as a std::thread (which they aren’t).
-
-
forcibly interleaving two adjacent execution graph nodes that are both executing on one execution resource (such as a GPU) with glue code that runs on another execution resource (such as a CPU), which is prohibitively expensive for some execution resources (such as CUDA or SYCL).
-
-
having to customise most or all sender algorithms to support an execution resource, 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 on sender algorithm from Towards C++23 executors: A proposal for an initial set of algorithms, we are proposing a way for senders to advertise what scheduler (and by extension what execution resource) they will complete on.
-Any given sender may have completion schedulers for some or all of the signals (value, error, or stopped) 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
-
get_completion_scheduler is a query that retrieves the completion scheduler for a specific completion-signal from a sender’s environment.
-For a sender that lacks a completion scheduler query for a given signal, calling get_completion_scheduler 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 resource represented by a scheduler returned from this function.
-See § 4.5 Senders can propagate completion schedulers for more details.
-
execution::schedulerautocpu_sched=new_thread_scheduler{};
-execution::schedulerautogpu_sched=cuda::scheduler();
-
-execution::senderautosnd0=execution::schedule(cpu_sched);
-execution::schedulerautocompletion_sch0=
- execution::get_completion_scheduler<execution::set_value_t>(get_env(snd0));
-// completion_sch0 is equivalent to cpu_sched
-
-execution::senderautosnd1=execution::then(snd0,[]{
- std::cout<<"I am running on cpu_sched!\n";
-});
-execution::schedulerautocompletion_sch1=
- execution::get_completion_scheduler<execution::set_value_t>(get_env(snd1));
-// completion_sch1 is equivalent to cpu_sched
-
-execution::senderautosnd2=execution::transfer(snd1,gpu_sched);
-execution::senderautosnd3=execution::then(snd2,[]{
- std::cout<<"I am running on gpu_sched!\n";
-});
-execution::schedulerautocompletion_sch3=
- execution::get_completion_scheduler<execution::set_value_t>(get_env(snd3));
-// completion_sch3 is equivalent to gpu_sched
-
-
4.6. Execution resource transitions are explicit
-
A Unified Executors Proposal for C++ does not contain any mechanisms for performing an execution resource transition. The only sender algorithm that can create a sender that will move execution to a specific execution resource is execution::schedule, which does not take an input sender.
-That means that there’s no way to construct sender chains that traverse different execution resources. 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 resource transitions must be explicit; running user code anywhere but where they defined it to run must be considered a bug.
-
The execution::transfer sender adaptor performs a transition from one execution resource to another:
-
execution::schedulerautosch1=...;
-execution::schedulerautosch2=...;
-
-execution::senderautosnd1=execution::schedule(sch1);
-execution::senderautothen1=execution::then(snd1,[]{
- std::cout<<"I am running on sch1!\n";
-});
-
-execution::senderautosnd2=execution::transfer(then1,sch2);
-execution::senderautothen2=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 std::unique_ptr that it will be transferring ownership of to the
-operation-state returned by a call to execution::connect so that the operation has access to
-this resource. In such a sender, calling execution::connect 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 execution::connect only has overloads for
-an rvalue-qualified sender. Callers must pass the sender as an rvalue to the
-call to execution::connect, 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 execution::connect to accept an lvalue reference to the sender. Callers can
-indicate that they want the sender to remain valid after the call to execution::connect by passing an lvalue reference to the sender to call these
-overloads. Multi-shot senders should also define overloads of execution::connect 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 execution::connect.
-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 execution::connect. 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 execution::connect on the input sender, such as in this_thread::sync_wait or execution::start_detached.
-
Some multi-use sender algorithms may require that an input sender be copy-constructible but will only call execution::connect 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 execution::connect 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 split sender adaptor facilitates connecting to a sender multiple times, regardless of whether it is single-shot or multi-shot:
-
autosome_algorithm(execution::senderauto&&input){
- execution::senderautomulti_shot=split(input);
- // "multi_shot" is guaranteed to be multi-shot,
- // regardless of whether "input" was multi-shot or not
-
- returnwhen_all(
- then(multi_shot,[]{std::cout<<"First continuation\n";}),
- then(multi_shot,[]{std::cout<<"Second continuation\n";})
- );
-}
-
-
4.9. 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 execution::connect() before calling execution::start(), 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 when_all(ops...) algorithm should cancel other operations as soon as one operation fails
-
-
a first_successful(ops...) algorithm should cancel the other operations as soon as one operation completes successfuly
-
-
a generic timeout(src,duration) algorithm needs to be able to cancel the src operation after the timeout duration has elapsed.
-
-
a stop_when(src,trigger) algorithm should cancel src if trigger completes first and cancel trigger if src completes first
-
-
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:
In this example, if we take the operation returned by query_server_b(query), this operation will
-receive a stop-request when any of the following happens:
-
-
-
first_successful algorithm will send a stop-request if query_server_a(query) completes successfully
-
-
when_all algorithm will send a stop-request if the load_file("some_file.jpg") operation completes with an error or stopped result.
-
-
timeout algorithm will send a stop-request if the operation does not complete within 5 seconds.
-
-
stop_when algorithm will send a stop-request if the user clicks on the "Cancel" button in the user-interface.
-
-
The parent operation consuming the composed_cancellation_example() sends a stop-request
-
-
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.
At a high-level, the facilities proposed by this paper for supporting cancellation include:
-
-
-
Add std::stoppable_token and std::stoppable_token_for concepts that generalise the interface of std::stop_token type to allow other types with different implementation strategies.
-
-
Add std::unstoppable_token concept for detecting whether a stoppable_token can never receive a stop-request.
-
-
Add std::in_place_stop_token, std::in_place_stop_source and std::in_place_stop_callback<CB> types that provide a more efficient implementation of a stop-token for use in structured concurrency situations.
-
-
Add std::never_stop_token for use in places where you never want to issue a stop-request
-
-
Add std::execution::get_stop_token() CPO for querying the stop-token to use for an operation from its receiver’s execution environment.
-
-
Add std::execution::stop_token_of_t<T> for querying the type of a stop-token returned from 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 execution::get_stop_token() CPO.
-This CPO takes a single parameter, which is the execution environment of the receiver passed to execution::connect, and returns a std::stoppable_token that the operation can use to check for stop-requests for that operation.
-
As the caller of execution::connect typically has control over the receiver
-type it passes, it is able to customise the std::execution::get_env() CPO for that
-receiver to return an execution environment that hooks the execution::get_stop_token() CPO to return a stop-token that the receiver has
-control over and that it can use to communicate a stop-request to the operation
-once it has started.
-
4.9.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’s execution environment does not customise the execution::get_stop_token() CPO then invoking the CPO on that receiver’s
-environment will invoke the default implementation which returns std::never_stop_token. This is a special stoppable_token type that is
-statically known to always return false from the stop_possible() 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 execution::get_stop_token(), 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.9.3. Cancellation is inherently racy
-
Usually, an operation will attach a stop-callback at some point inside the call to execution::start() 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 execution::start() 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 execution::start() implementation.
-
An implementation of execution::start() 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 execution::start 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 execution::start() call (which is still executing).
This paper currently includes the design for cancellation as proposed in Composable cancellation for sender-based async operations - "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.10. 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 ensure_started 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.10.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 execution::connect?" or similarly, "What happens if a call
-to execution::connect is made, but the returned operation state is destroyed
-before execution::start 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 when_all(eager_op_1(),eager_op_2()). Imagine eager_op_1() starts an
-asynchronous operation successfully, but then eager_op_2() throws. For
-lazy senders, that failure happens in the context of the when_all 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 before when_all has been called.
-
It then becomes the responsibility, not of the algorithm, but of the end
-user to handle the exception and ensure that eager_op_1() is joined before
-allowing the exception to propagate. If they fail to do that, they incur
-undefined behavior.
-
-
Detach from the computation - let the operation continue in the background -
-like an implicit call to std::thread::detach(). 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::shared_ptr, to ensure resources are kept alive until the operations
-complete, and can make clean shutdown nigh impossible.
-
-
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 threads 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 std::terminate if an eager sender is destructed
-without joining it. This is the approach that std::thread destructor
-takes.
-
-
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 std::jthread uses for its
-destructor. It reduces the risk of deadlock but does not eliminate it.
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 execution::start 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 sequence that puts async operations in sequence by starting an operation only
-after the preceding one has completed. In an expression like sequence(a(),then(src,[]{b();}),c()), one may reasonably assume that a(), b() and c() 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.
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 execution::get_stop_token is an std::unstoppable_token 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.10.4. Eager senders cannot access execution resource 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.11. Schedulers advertise their forward progress guarantees
-
To decide whether a scheduler (and its associated execution resource) 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, get_forward_progress_guarantee, which returns one of the enumerators of a new enum type, forward_progress_guarantee. Each enumerator of forward_progress_guarantee corresponds to one of the aforementioned
-guarantees.
-
4.12. Most sender adaptors are pipeable
-
To facilitate an intuitive syntax for composition, most sender adaptors are pipeable; they can be composed (piped) together with operator|.
-This mechanism is similar to the operator| 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.
-
a|b will pass the sender a as the first argument to the pipeable sender adaptor b. Pipeable sender adaptors support partial application of the parameters after the first. For example, all of the following are equivalent:
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:
Certain sender adaptors are not 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.
-
-
-
execution::when_all and execution::when_all_with_variant: 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::on: 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 to 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.13. 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 R that is a range of senders, consider the following in a coroutine that returns an async generator type:
This transforms each element of the asynchronous sequence R with the function fn on demand, as the data arrives. The result is a new asynchronous sequence of the transformed values.
-
Now imagine that R is the simple expression views::iota(0)|views::transform(execution::just). 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 [fn(0), fn(1), fn(2), ...].
-
Far more interesting would be if R 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.14. 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:
- returnread_socket_async(socket,span{buff})
- // For success (partial and full), specify the next action:
- |execution::let_value([](error_codeerr,size_tbytes_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(autosnd:read_socket_async(socket,span{buff})){
- try{
- if(optional<size_t>bytes_read=
- co_awaitexecution::stopped_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.15. 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 sender 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 await_transform trickery from a promise type to make them awaitable.)
-
For an example, imagine a coroutine type called task<T> that knows nothing
-about senders. It doesn’t implement any of the sender customization points.
-Despite that fact, and despite the fact that the this_thread::sync_wait algorithm is constrained with the sender concept, the following would compile
-and do what the user wants:
-
task<int>doSomeAsyncWork();
-
-intmain(){
- // OK, awaitable types satisfy the requirements for senders:
- autoo=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.16. 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 execution::with_awaitable_senders 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 retry algorithm:
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 with_awaitable_senders 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 into_variant sender algorithm -- before co_await-ing that sender.
-
4.17. Cancellation of a sender can unwind a stack of coroutines
-
When looking at the sender-based retry 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 execution::set_stopped?
-
When your task type’s promise inherits from with_awaitable_senders, what happens is this: the coroutine behaves as if an uncatchable exception had been thrown from the co_await 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 with_awaitable_senders, or more generally implement a member function called unhandled_stopped, the exception unwinds the chain of coroutines as if an exception were thrown except that it bypasses catch(...) clauses.
-
In order to "catch" this uncatchable stopped exception, one of the calling coroutines in the stack would have to await a sender that maps the stopped channel into either a value or an error. That is achievable with the execution::let_stopped, execution::upon_stopped, execution::stopped_as_optional, or execution::stopped_as_error sender adaptors. For instance, we can use execution::stopped_as_optional to "catch" the stopped signal and map it into an empty optional as shown below:
-
if(autoopt=co_awaitexecution::stopped_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 connect-ing an awaitable and a receiver, the adaptation layer awaits the awaitable within a coroutine that implements unhandled_stopped in its promise type. The effect of this is that an "uncatchable" stopped exception propagates seamlessly out of awaitables, causing execution::set_stopped to be called on the receiver.
-
Obviously, unhandled_stopped is a library extension of the coroutine promise interface. Many promise types will not implement unhandled_stopped. When an uncatchable stopped exception tries to propagate through such a coroutine, it is treated as an unhandled exception and terminate is called. The solution, as described above, is to use a sender adaptor to handle the stopped exception before awaiting it. It goes without saying that any future Standard Library coroutine types ought to implement unhandled_stopped. The author of Add lazy coroutine (coroutine task) type, which proposes a standard coroutine task type, is in agreement.
-
4.18. 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 execution::par, 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.
This function would return an object of an unspecified 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 policy while using scheduler to create any work to be run. The expression may be ill-formed if scheduler 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 executing_on 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 void and (2) where a customization point pairing a sender with an execution policy would similarly be used to
-obtain an object of unspecified type to be provided as the first argument to the algorithm.
-
4.19. User-facing sender factories
-
A sender factory is an algorithm that takes no senders as parameters and returns a sender.
execution::schedulerautosch1=get_system_thread_pool().scheduler();
-
-execution::senderautosnd1=execution::schedule(sch1);
-// snd1 describes the creation of a new task on the system thread pool
-
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 set_value.
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 set_error. 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 set_error.
-
4.19.4. execution::just_stopped
-
execution::senderautojust_stopped();
-
-
Returns a sender with no completion schedulers, which completes immediately by calling the receiver’s set_stopped.
Returns a sender that reaches into a receiver’s environment and pulls out the current value associated with the customization point denoted by Tag. It then sends the value read back to the receiver through the value channel. For instance, get_scheduler() (with no arguments) is a sender that asks the receiver for the currently suggested scheduler and passes it to the receiver’s set_value completion-signal.
-
This can be useful when scheduling nested dependent work. The following sender pulls the current schduler into the value channel and then schedules more work onto it.
-
execution::senderautotask=
- execution::get_scheduler()
- |execution::let_value([](autosched){
- returnexecution::on(sched,somenestedworkhere);
- });
-
-this_thread::sync_wait(std::move(task));// wait for it to finish
-
-
This code uses the fact that sync_wait associates a scheduler with the receiver that it connects with task. get_scheduler() reads that scheduler out of the receiver, and passes it to let_value's receiver’s set_value function, which in turn passes it to the lambda. That lambda returns a new sender that uses the scheduler to schedule some nested work onto sync_wait's scheduler.
-
4.20. User-facing sender adaptors
-
A sender adaptor is an algorithm that takes one or more senders, which it may execution::connect, 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.1 execution::start_detached and § 4.21.2 this_thread::sync_wait start senders.
execution::schedulerautocpu_sched=get_system_thread_pool().scheduler();
-execution::schedulerautogpu_sched=cuda::scheduler();
-
-execution::senderautocpu_task=execution::schedule(cpu_sched);
-// cpu_task describes the creation of a new task on the system thread pool
-
-execution::senderautogpu_task=execution::transfer(cpu_task,gpu_sched);
-// gpu_task describes the transition of the task graph described by cpu_task to the gpu
-
then 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.
-
then is guaranteed to not begin executing function until the returned sender is started.
-
execution::senderautoinput=get_input();
-execution::senderautosnd=execution::then(input,[](auto...args){
- std::print(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.
upon_error and upon_stopped are similar to then, but where then works with values sent by the input sender, upon_error works with errors, and upon_stopped is invoked when the "stopped" signal is sent.
let_value is very similar to then: 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 then sends exactly what that function ends up returning - let_value requires that the function return a sender, and the sender returned by let_value 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.
-
let_value is guaranteed to not begin executing function until the returned sender is started.
-
let_error and let_stopped are similar to let_value, but where let_value works with values sent by the input sender, let_error works with errors, and let_stopped is invoked when the "stopped" signal is sent.
Returns a sender which, when started, will start the provided sender on an execution agent belonging to the execution resource associated with the provided scheduler. This returned sender has no completion schedulers.
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.
Returns a sender that maps the value channel from a T to an optional<decay_t<T>>, and maps the stopped channel to a value of an empty optional<decay_t<T>>.
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 function will begin executing until the returned sender is started. Each invocation of function 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 bulk execute with the same guarantee. The number of execution agents used by bulk is not specified. This allows a scheduler to execute some invocations of the function in parallel.
-
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.
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.
when_all 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 set_value 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 when_all. It completes inline on the execution resource on which the last input sender completes, unless stop is requested before when_all is started, in which case it completes inline within the call to start.
-
when_all_with_variant does the same, but it adapts all the input senders using into_variant, and so it does not constrain the input arguments as when_all does.
execution::schedulerautosched=thread_pool.scheduler();
-
-execution::senderautosends_1=...;
-execution::senderautosends_abc=...;
-
-execution::senderautoboth=execution::when_all(sched,
- sends_1,
- sends_abc
-);
-
-execution::senderautofinal=execution::then(both,[](auto...args){
- std::cout<<std::format("the two args: {}, {}",args...);
-});
-// when final executes, it will print "the two args: 1, abc"
-
Once ensure_started returns, it is known that the provided sender has been connected and start 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 resources. 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 execution::connect() is called, or if execution::connect() is called but the
-returned operation-state is destroyed before execution::start() 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 std::shared_ptr 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.21. User-facing sender consumers
-
A sender consumer is an algorithm that takes one or more senders, which it may execution::connect, as parameters, and does not return a sender.
this_thread::sync_wait is a sender consumer that submits the work described by the provided sender for execution, similarly to ensure_started, except that it blocks the current std::thread or thread of main 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.19.1 execution::schedule and § 4.19.2 execution::just are meant to enter the domain of senders, sync_wait 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, sync_wait throws that error as an exception, or rethrows the original exception if the error is of type std::exception_ptr.
-
If the provided sender sends the "stopped" signal instead of values, sync_wait returns an empty optional.
-
For an explanation of the requires clause, see § 5.8 All senders are typed. That clause also explains another sender consumer, built on top of sync_wait: sync_wait_with_variant.
-
Note: This function is specified inside std::this_thread, and not inside execution. This is because sync_wait 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 std::this_thread, 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 std::this_fiber::sync_wait would be provided. We also expect that runtimes with execution agents that use different synchronization mechanisms than std::thread's will provide their own flavors of sync_wait as well (assuming their execution agents have the means
-to block in a non-deadlock manner).
-
4.22. 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.
A receiver is a callback that supports more than one channel. In fact, it supports three of them:
-
-
-
set_value, which is the moral equivalent of an operator() or a function
-call, which signals successful completion of the operation its execution
-depends on;
-
-
set_error, 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; and
-
-
set_stopped, which signals that the operation completed without succeeding
-(set_value) and without failing (set_error). 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.
-
-
Once an async operation has been started exactly one of these functions must be invoked
-on a receiver before it is destroyed.
-
While the receiver interface may look novel, it is in fact very similar to the
-interface of std::promise, which provides the first two signals as set_value and set_exception, 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.
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: start, 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 execution::ensure_started and this_thread::sync_wait, 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.
execution::connect is a customization point which connects senders with receivers, resulting in an operation state that will ensure that if start is called that one of the completion operations will be called on the receiver passed to connect.
-
execution::senderautosnd=someinputsender;
-execution::receiverautorcv=somereceiver;
-execution::operation_stateautostate=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 resource, and that execution resource will eventually call one of the
-// completion operations on 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 then, 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 execution::then(sender,invocable) to be equivalent to:
-
-
-
sender.then(invocable), if that expression is well-formed; otherwise
-
-
then(sender,invocable), performed in a context where this call always performs ADL, if that expression is well-formed; otherwise
-
-
a default implementation of then, which returns a sender adaptor, and then define the exact semantics of said adaptor.
-
-
However, this definition is problematic. Imagine another sender adaptor, bulk, 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 bulk to have specialized behavior, which invokes a kernel of more than one thread (with its size defined by the call to bulk); therefore, we would like to customize bulk for CUDA senders to achieve this. However, there’s no reason for CUDA kernels to
-necessarily customize the then sender adaptor, as the generic implementation is perfectly sufficient. This creates a problem, though; consider the following snippet:
-
execution::schedulerautocuda_sch=cuda_scheduler{};
-
-execution::senderautoinitial=execution::schedule(cuda_sch);
-// the type of initial is a type defined by the cuda_scheduler
-// let’s call it cuda::schedule_sender<>
-
-execution::senderautonext=execution::then(cuda_sch,[]{return1;});
-// the type of next is a standard-library unspecified sender adaptor
-// that wraps the cuda sender
-// let’s call it execution::then_sender_adaptor<cuda::schedule_sender<>>
-
-execution::senderautokernel_sender=execution::bulk(next,shape,[](inti){...});
-
-
How can we specialize the bulk sender adaptor for our wrapped schedule_sender? 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):
However, if the input sender is not just a then_sender_adaptor like in the example above, but another sender that overrides bulk 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 execution::<sender-algorithm>(sender,args...), for any given sender algorithm that accepts a sender as its first argument, should be
-equivalent to:
-
-
-
tag_invoke(<sender-algorithm>,get_completion_scheduler<Tag>(get_env(sender)),sender,args...), if that expression is well-formed; otherwise
-
-
tag_invoke(<sender-algorithm>,sender,args...), if that expression is well-formed; otherwise
-
-
a default implementation, if there exists a default implementation of the given sender algorithm.
-
-
where Tag is one of set_value, set_error, or set_stopped. For most sender algorithms, the completion scheduler for set_value would be used, but for some (like upon_error or let_stopped), one of the others would be used.
-
For sender algorithms which accept concepts other than sender as their first argument, we propose that the customization scheme remains as it has been in A Unified Executors Proposal for C++ so far, except it should also use tag_invoke.
-
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.20.12 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 resource before a receiver is connected to a sender, and execution::start 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 resource, 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 resource. 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 resources 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.20.12 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.20.9 execution::bulks of compatible shapes, and merge them together into a single submission of a GPU kernel.
-
5.7. Execution resource transitions are two-step
-
Because execution::transfer 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 resource, 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 transfer must be controllable by the target scheduler. Besides, the target
-scheduler may itself represent a specialized execution resource, 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 resources
-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 transfer.
-
To allow for such customization from both ends, we propose the inclusion of a secondary transitioning sender adaptor, called schedule_from. This adaptor is a form of schedule, 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 transfer, to ensure that both schedulers have a say in how the transitions are made. Any scheduler that specializes transfer(snd,sch) shall ensure that the
-return value of their customization is equivalent to schedule_from(sch,snd2), where snd2 is a successor of snd that sends values equivalent to those sent by snd.
-
The default implementation of transfer(snd,sched) is schedule_from(sched,snd).
-
5.8. All senders are typed
-
All senders must 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 execution::then is hard.
-
The mechanism for this advertisement is similar to the one in A Unified Executors Proposal for C++; the
-way to query the types is through completion_signatures_of_t<S,[Env]>::value_types<tuple_like,variant_like>.
-
completion_signatures_of_t::value_types 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 when_all). 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.21.2 this_thread::sync_wait: it returns a tuple of values sent by the
-sender passed to it, wrapped in std::optional to handle the set_stopped signal. However, this assumes that those values can be represented as a tuple,
-like here:
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 value_types of a sender which sends Types... to be as follows:
If senders could only ever send one specific set of values, this would probably need to be the required form of value_types 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) set_value 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, A Unified Executors Proposal for C++ also includes a second template parameter to value_types, one that represents a variant-like type. If we permit such senders, we would almost certainly need to require that the canonical form of value_types 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 Types1..., Types2..., ..., TypesN... to be as follows:
This, however, introduces a couple of complications:
-
-
-
A just(1) sender would also need to follow this structure, so the correct type for storing the value sent by it would be std::variant<std::tuple<int>> 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 where value_types 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.
-
-
As a consequence of (1): because sync_wait needs to store the above type, it can no longer return just a std::tuple<int> for just(1); it has to return std::variant<std::tuple<int>>. 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 of sync_wait must be the same across all sender types.
-
-
One possible solution to (2) above is to place a requirement on sync_wait that it can only accept senders which send only a single set of values, therefore removing the need for std::variant to appear in its API; because of this, we propose to expose both sync_wait, which is a simple, user-friendly version of the sender consumer, but requires that value_types have only one possible variant, and sync_wait_with_variant, which accepts any sender, but returns an optional whose value type is the variant of all the
-possible tuples sent by the input sender:
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 (A Unified Executors Proposal for C++ and Towards C++23 executors: A proposal for an initial set of algorithms) 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: sync_wait. We imagine that if, in the future, C++ was to
-gain fibers support, we would want to also have std::this_fiber::sync_wait, in addition to std::this_thread::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.
In short, instead of using globally reserved names, tag_invoke uses the type of the customization point object itself as the mechanism to find customizations. It globally reserves only a single name - tag_invoke - which itself is used the same way that
-ranges-style customization points are used. All other customization points are defined in terms of tag_invoke. For example, the customization for std::this_thread::sync_wait(s) will call tag_invoke(std::this_thread::sync_wait,s), instead of attempting
-to invoke s.sync_wait(), and then sync_wait(s) if the member call is not valid.
-
Using tag_invoke 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<typenameTag,typename...Args>
-friendautotag_invoke(Tag&&tag,wrapper&self,Args&&...args){
- returnstd::forward<Tag>(tag)(self.subobject,std::forward<Args>(args)...);
-}
-
-// but override one of them with a specific value
-friendautotag_invoke(specific_customization_point_t,wrapper&self){
- returnself.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 A Unified Executors Proposal for C++ 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 tag_invoke, dispatching to manually constructed vtables containing pointers to specialized implementations for the wrapped objects. For an example of such a polymorphic wrapper, see unifex::any_unique (example).
7.1.1.1. The std::terminate function [except.terminate]
-
At the end of the bulleted list in the Note in paragraph 1, add a new bullet as follows:
-
-
-
-
-
when a callback invocation exits via an exception when requesting stop on a std::stop_source or a std::in_place_stop_source ([stopsource.mem], [stopsource.inplace.mem]), or in
-the constructor of std::stop_callback or std::in_place_stop_callback ([stopcallback.cons], [stopcallback.inplace.cons]) when a callback invocation exits
-via an exception.
-
-
-
-
8. Library introduction [library]
-
Add the header <execution> to Table 23: C++ library
-headers [tab:headers.cpp]
-
At the end of [expos.only.entity], add the following:
-
-
-
-
-
An object dst is said to be decay-copied from a
-subexpression src if the type of dst is decay_t<decltype((src))>,
-and dst is copy-initialized from src.
-
-
-
-
In subclause [conforming], after [lib.types.movedfrom],
-add the following new subclause with suggested stable name [lib.tmpl-heads].
-
-
- 16.4.6.17 Class template-heads
-
-
-
If a class template’s template-head is marked with "arguments are not
-associated entities"", any template arguments do not contribute to the
-associated entities ([basic.lookup.argdep]) of a function call where a
-specialization of the class template is an associated entity. In such a case,
-the class template can be implemented as an alias template referring to a
-templated class, or as a class template where the template arguments
-themselves are templated classes.
-
-
[Example:
-
template<classT>// arguments are not associated entities
-structS{};
-
-namespaceN{
- intf(auto);
- structA{};
-}
-
-intx=f(S<N::A>{});// error: N::f not a candidate
-
-
The template S specified above can be implemented as
Insert this subclause as a new subclause, between Searchers [func.search] and Class template hash[unord.hash].
-
-
-
-
-
Given a subexpression E, let REIFY(E) be expression-equivalent to
-a glvalue with the same type and value as E as if by identity()(E).
-
-
The name std::tag_invoke denotes a customization point object [customization.point.object].
-Given subexpressions T and A..., the expression std::tag_invoke(T,A...) is
-expression-equivalent [defns.expression-equivalent] to tag_invoke(REIFY(T),REIFY(A)...) with overload resolution performed in a context in which unqualified lookup for tag_invoke finds only the declaration
-
voidtag_invoke();
-
-
-
[Note: Diagnosable ill-formed cases above result in substitution failure when std::tag_invoke(T,A...) appears in the immediate context of a template instantiation. —end note]
Insert this subclause as a new subclause between Header <stop_token> synopsis [thread.stoptoken.syn] and Class stop_token[stoptoken].
-
-
-
-
-
The stoppable_token concept checks for the basic interface of a stop token
-that is copyable and allows polling to see if stop has been requested and
-also whether a stop request is possible. For a stop token type T and a type CB that is callable with no arguments, the type T::callback_type<CB> is
-valid and denotes 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. The stoppable_token_for concept checks for a stop token type compatible
-with a given callback type. The unstoppable_token concept checks for a stop
-token type that does not allow stopping.
- LWG directed me to replace T::stop_possible() with t.stop_possible() because
-of the recent constexpr changes in P2280R2. However, even with those changes, a nested
-requirement like requires(!t.stop_possible()), where t is an argument in the requirement-parameter-list, is ill-formed according to [expr.prim.req.nested/p2]:
-
-
A local parameter shall only appear as an unevaluated operand within the constraint-expression.
Let t and u be distinct, valid objects of type T. The type T models stoppable_token only if:
-
-
-
If t.stop_possible() evaluates to false then, if t and u reference the same logical shared stop state, u.stop_possible() shall also subsequently evaluate to false and u.stop_requested() shall also subsequently evaluate to false.
-
-
If t.stop_requested() evaluates to true then, if t and u reference the same logical shared stop state, u.stop_requested() shall also subsequently evaluate to true and u.stop_possible() shall also subsequently evaluate to true.
-
-
-
Let t and u be distinct, valid objects of type T and let init be an
-object of type Initializer. Then for some type CB, the type T models stoppable_token_for<CB,Initializer> only if:
Direct non-list initializing an object cb of type T::callback_type<CB> from t,init shall, if t.stop_possible() is true, construct an
-instance, callback, of type CB, direct-initialized with init,
-and register callback with t'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.
-
-
-
If t.stop_requested() evaluates to true at the time callback is
-registered then callback can be invoked on the thread executing cb's constructor.
-
-
If callback is invoked then, if t and u reference the same shared stop
-state, an evaluation of u.stop_requested() will be true if the beginning of the invocation of callback strongly-happens-before the evaluation of u.stop_requested().
-
-
[Note: If t.stop_possible() evaluates to false then the construction of cb is not required to construct and initialize callback. --end note]
-
-
-
Construction of a T::callback_type<CB> instance shall only throw exceptions thrown by the initialization of the CB instance from the value of type Initializer.
-
-
Destruction of the T::callback_type<CB> object, cb, removes callback from the shared stop state such that callback will not be invoked after the destructor returns.
-
-
-
If callback is currently being invoked on another thread then the destructor of cb will block until the invocation of callback returns such that the return from the invocation of callback strongly-happens-before the destruction of callback.
-
-
Destruction of a callback cb shall not block on the completion of the invocation of some other callback registered with the same shared stop state.
-
-
-
-
-
-
10.1.3. Class stop_token[stoptoken]
-
10.1.3.1. General [stoptoken.general]
-
Modify the synopsis of class stop_token in subclause General [stoptoken.general] as follows:
Insert a new subclause, Class never_stop_token[stoptoken.never], after subclause Class template stop_callback[stopcallback], as a new subclause of Stop tokens [thread.stoptoken].
-
10.1.4.1. General [stoptoken.never.general]
-
-
-
The class never_stop_token provides an implementation of the unstoppable_token concept. It provides a stop token interface, but also provides static information that a stop is never possible nor requested.
10.1.5. Class in_place_stop_token[stoptoken.inplace]
-
Insert a new subclause, Class in_place_stop_token[stoptoken.inplace], after the subclause added above, as a new subclause of Stop tokens [thread.stoptoken].
-
10.1.5.1. General [stoptoken.inplace.general]
-
-
-
The class in_place_stop_token provides an interface for querying whether a stop request has been made (stop_requested) or can ever be made (stop_possible) using an associated in_place_stop_source object ([stopsource.inplace]).
-An in_place_stop_token can also be passed to an in_place_stop_callback ([stopcallback.inplace]) constructor to register a callback to be called when a stop request has been made from an associated in_place_stop_source.
10.1.5.2. Constructors, copy, and assignment [stoptoken.inplace.cons]
-
in_place_stop_token()noexcept;
-
-
-
-
Effects: initializes source_ with nullptr.
-
-
voidswap(stop_token&rhs)noexcept;
-
-
-
-
Effects: Exchanges the values of source_ and rhs.source_.
-
-
10.1.5.3. Members [stoptoken.inplace.mem]
-
[[nodiscard]]boolstop_requested()constnoexcept;
-
-
-
-
Effects: Equivalent to: returnsource_!=nullptr&&source_->stop_requested();
-
-
[Note: The behavior of stop_requested() is undefined unless the call
-strongly happens before the start of the destructor of the associated in_place_stop_source, if any ([basic.life]). --end note]
-
-
[[nodiscard]]boolstop_possible()constnoexcept;
-
-
-
-
Effects: Equivalent to: returnsource_!=nullptr;
-
-
[Note: The behavior of stop_possible() is implementation-defined unless
-the call strongly happens before the end of the storage duration of the
-associated in_place_stop_source object, if any ([basic.stc.general]). --end note]
10.1.6. Class in_place_stop_source[stopsource.inplace]
-
Insert a new subclause, Class in_place_stop_source[stopsource.inplace], after the subclause added above, as a new subclause of Stop tokens [thread.stoptoken].
-
10.1.6.1. General [stopsource.inplace.general]
-
-
-
The class in_place_stop_source implements the semantics of making a stop request, without the need for a dynamic allocation of a shared state.
-A stop request made on a in_place_stop_source object is visible to all associated in_place_stop_token ([stoptoken.inplace]) objects.
-Once a stop request has been made it cannot be withdrawn (a subsequent stop request has no effect).
-All uses of in_place_stop_token objects associated with a given in_place_stop_source object must happen before the start of the destructor of that in_place_stop_source object.
An instance of in_place_stop_source maintains a list of registered callback invocations.
-The registration of a callback invocation either succeeds or fails. When an invocation
-of a callback is registered, the following happens atomically:
-
-
-
The stop state is checked. If stop has not been requested, the callback invocation is
-added to the list of registered callback invocations, and registration has succeeded.
-
-
Otherwise, registration has failed.
-
-
When an invocation of a callback is unregistered, the invocation is atomically removed
-from the list of registered callback invocations. The removal is not blocked by the concurrent
-execution of another callback invocation in the list. If the callback invocation
-being unregistered is currently executing, then:
-
-
-
If the execution of the callback invocation is happening concurrently on another thread,
-the completion of the execution strongly happens before ([intro.races]) the end of the
-callback’s lifetime.
-
-
Otherwise, the execution is happening on the current thread. Removal of the
-callback invocation does not block waiting for the execution to complete.
-
-
-
10.1.6.2. Constructors, copy, and assignment [stopsource.inplace.cons]
-
in_place_stop_source()noexcept;
-
-
-
-
Effects: Initializes a new stop state inside *this.
Returns: A new associated in_place_stop_token object.
-
-
[[nodiscard]]boolstop_requested()constnoexcept;
-
-
-
-
Returns: true if the stop state inside *this has received a stop request; otherwise, false.
-
-
boolrequest_stop()noexcept;
-
-
-
-
Effects: Atomically determines whether the stop state inside *this has received a stop request, and if not, makes a stop request.
-The determination and making of the stop request are an atomic read-modify-write operation ([intro.races]).
-If the request was made, the registered invocations are executed and the evaluations of the invocations are indeterminately sequenced.
-If an invocation of a callback exits via an exception then terminate is invoked ([except.terminate]).
-
-
Postconditions: stop_requested() is true.
-
-
Returns: true if this call made a stop request; otherwise false.
-
-
10.1.7. Class template in_place_stop_callback[stopcallback.inplace]
-
Insert a new subclause, Class template in_place_stop_callback[stopcallback.inplace], after the subclause added above, as a new subclause of Stop tokens [thread.stoptoken].
Mandates: in_place_stop_callback is instantiated with an argument for the template parameter Callback that satisfies both invocable and destructible.
-
-
Preconditions: in_place_stop_callback is instantiated with an argument for the template parameter Callback that models both invocable and destructible.
-
-
Recommended practice: Implementations should use the storage of the in_place_stop_callback objects to store the state necessary for their association with an in_place_stop_source object.
-
-
10.1.7.2. Constructors and destructor [stopcallback.inplace.cons]
Constraints: Callback and C satisfy constructible_from<Callback,C>.
-
-
Preconditions: Callback and C model constructible_from<Callback,C>.
-
-
Effects: Initializes callback_ with std::forward<C>(cb).
-Any in_place_stop_source associated with st becomes associated with *this.
-Registers ([stopsource.inplace.general]) the callback invocation std::forward<Callback>(callback_)() with the associated in_place_stop_source, if any. If the registration fails, evaluates
-the callback invocation.
-
-
Throws: Any exception thrown by the initialization of callback_.
-
-
Remarks: If evaluating std::forward<Callback>(callback_)() exits via an exception, then terminate is invoked ([except.terminate]).
-
-
~in_place_stop_callback();
-
-
-
-
Effects: Unregisters ([stopsource.inplace.general]) the callback invocation from
-the associated in_place_stop_source object, if any.
-
-
Remarks: A program has undefined behavior if the start of this destructor does
-not strongly happen before the start of the destructor of the associated in_place_stop_source object, if any.
-
-
11. Execution control library [exec]
-
11.1. General [exec.general]
-
-
-
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.
-
-
-
Table N: Execution control library summary [tab:execution.summary]
[Note: A large number of execution control primitives are
-customization point objects. For an object one might define multiple types of
-customization point objects, for which different rules apply. Table 2 shows
-the types of customization point objects used in the execution control
-library:
-
-
-
Table N+1: Types of customization point objects in the execution control library [tab:execution.cpos]
-
-
-
Customization point object type
-
Purpose
-
Examples
-
-
core
-
provide core execution functionality, and connection between core components
-
connect, start, execute
-
-
completion functions
-
called by senders to announce the completion of the work (success, error, or cancellation)
-
For function types F1 and F2 denoting R1(Args1...) and R2(Args2...) respectively, MATCHING-SIG(F1,F2) is true if and only if same_as<R1(Args&&...),R2(Args2&&...)> is true.
-
-
-
11.2. Queries and queryables [exec.queryable]
-
11.2.1. General [exec.queryable.general]
-
-
-
A queryable object is a read-only collection of
-key/value pairs where each key is a customization point object known as a query object. A query is an invocation of a query object with a queryable
-object as its first argument and a (possibly empty) set of additional
-arguments. The result of a query expression is valid as long as the
-queryable object is valid. A query imposes syntactic
-and semantic requirements on its invocations.
-
-
Given a subexpression env that refers to a queryable object o, a query
-object q, and a (possibly empty) pack of subexpressions args, the expression q(env,args...) is equal to
-([concepts.equality]) the expression q(c,args...) where c is a const lvalue reference to o.
-
-
The type of a query expression can not be void.
-
-
The expression q(env,args...) is equality-preserving
-([concepts.equality]) and does not modify the function object or the
-arguments.
-
-
If tag_invoke(q,env,args...) is well-formed, then q(env,args...) is expression-equivalent to tag_invoke(q,env,args...).
-
-
Unless otherwise specified, the value returned by the expression q(env,args...) is valid as long as env is valid.
The queryable concept specifies the constraints on the types of queryable
-objects.
-
-
Let env be an object of type Env. The type Env models queryable if for each
-callable object q and a pack of subexpressions args,
-if requires{q(env,args...)} is true then q(env,args...) meets any semantic requirements imposed by q.
-
-
11.3. Asynchronous operations [async.ops]
-
-
-
An execution resource is a program entity that manages
-a (possibly dynamic) set of execution agents
-([thread.req.lockable.general]), which it uses to execute parallel work on
-behalf of callers. [Example 1: The currently active thread, a
-system-provided thread pool, and uses of an API associated with an external
-hardware accelerator are all examples of execution resources. -- end
-example] Execution resources execute asynchronous operations. An execution
-resource is either valid or invalid.
-
-
An asynchronous operation is a distinct unit of
-program execution that:
-
-
-
is explicitly created;
-
-
can be explicitly started; an
- asynchronous operation can be started once at most;
-
-
if started, eventually completes with a (possibly empty) set of result datums, and in exactly one of
- three modes: success, failure, or cancellation, known as the
- operation’s disposition; an asychronous
- operation can only complete once; a successful completion, also known
- as a value completion, can have an arbitrary
- number of result datums; a failure completion, also known as an error completion, has a single result datum; a
- cancellation completion, also known as a stopped
- completion, has no result datum; an asynchronous operation’s async result is its disposition and its
- (possibly empty) set of result datums.
-
-
can complete on a different execution resource than that on which it
- started; and
-
-
can create and start other asychronous operations called child operations. A child operation is an
- asynchronous operation that is created by the parent operation and, if
- started, completes before the parent operation completes. A parent operation is the asynchronous operation that
- created a particular child operation.
-
-
An asynchronous operation can in fact execute
-synchronously; that is, it can complete during the execution of its start
-operation on the thread of execution that started it.
-
-
An asynchronous operation has associated state known as its operation state.
-
-
An asynchronous operation has an associated environment. An environment is a queryable object ([exec.queryable])
-representing the execution-time properties of the operation’s caller. The caller of an asynchronous operation is
-its parent operation or the function that created it. An asynchronous
-operation’s operation state owns the operation’s environment.
-
-
An asynchronous operation has an associated receiver. A receiver is an aggregation of three handlers for the three
-asynchronous completion dispositions: a value completion handler for a value
-completion, an error completion handler for an error completion, and a
-stopped completion handler for a stopped completion. A receiver has an
-associated environment. An asynchronous operation’s operation state owns the
-operation’s receiver. The environment of an asynchronous operation is equal
-to its receiver’s environment.
-
-
For each completion disposition, there is a completion
-function. A completion function is a customization point object
-([customization.point.object]) that accepts an asynchronous operation’s
-receiver as the first argument and the result datums of the asynchronous
-operation as additional arguments. The value completion function invokes the
-receiver’s value completion handler with the value result datums; likewise
-for the error completion function and the stopped completion function. A
-completion function has an associated type known as its completion tag that names the unqualified type of the
-completion function. A valid invocation of a completion function is called a completion operation.
-
-
The lifetime of an
-asynchronous operation, also known as the operation’s async lifetime, begins when its start operation begins
-executing and ends when its completion operation begins executing. If the
-lifetime of an asynchronous operation’s associated operation state ends
-before the lifetime of the asynchronous operation, the behavior is
-undefined. After an asynchronous operation executes a completion operation,
-its associated operation state is invalid. Accessing any part of an invalid
-operation state is undefined behavior.
-
-
An asynchronous operation shall not execute a completion operation before its
-start operation has begun executing. After its start operation has begun
-executing, exactly one completion operation shall execute. The lifetime of an
-asynchronous operation’s operation state can end during the execution of the
-completion operation.
-
-
A sender is a factory for one or more asynchronous
-operations. Connecting a sender and a
-receiver creates an asynchronous operation. The asynchronous operation’s
-associated receiver is equal to the receiver used to create it, and its
-associated environment is equal to the environment associated with the
-receiver used to create it. The lifetime of an asynchronous operation’s
-associated operation state does not depend on the lifetimes of either the
-sender or the receiver from which it was created. A sender sends its results by way of the asynchronous operation(s)
-it produces, and a receiver receives those results. A sender is either valid or invalid; it becomes invalid
-when its parent sender (see below) becomes invalid.
-
-
A scheduler is an abstraction of an execution
-resource with a uniform, generic interface for scheduling work onto that
-resource. It is a factory for senders whose asynchronous operations execute
-value completion operations on an execution agent belonging to the
-scheduler’s associated execution resource. A schedule-expression obtains such a sender from a
-scheduler. A schedule sender is the result of a
-schedule expression. On success, an asynchronous operation produced by a
-schedule sender executes a value completion operation with an empty set of
-result datums. Multiple schedulers can refer to the same execution resource.
-A scheduler can be valid or invalid. A scheduler becomes invalid when the
-execution resource to which it refers becomes invalid, as do any schedule
-senders obtained from the scheduler, and any operation states obtained from
-those senders.
-
-
An asynchronous operation has one or more associated completion schedulers
-for each of its possible dispositions. A completion
-scheduler is a scheduler whose associated execution resource is used
-to execute a completion operation for an asynchronous operation. A value
-completion scheduler is a scheduler on which an asynchronous operation’s
-value completion operation can execute. Likewise for error completion
-schedulers and stopped completion schedulers.
-
-
A sender has an associated queryable object ([exec.queryable]) known as its attributes that describes various characteristics of
-the sender and of the asynchronous operation(s) it produces. For each
-disposition, there is a query object for reading the associated completion
-scheduler from a sender’s attributes; i.e., a value completion scheduler
-query object for reading a sender’s value completion scheduler, etc. If a
-completion scheduler query is well-formed, the returned completion scheduler
-is unique for that disposition for any asynchronous operation the sender
-creates. A schedule sender is required to have a value completion scheduler
-attribute whose value is equal to the scheduler that produced the schedule
-sender.
-
-
A completion signature is a function type that
-describes a completion operation. An asychronous operation has a finite set
-of possible completion signatures corresponding to the completion operations
-that the asynchronous operation potentially evaluates ([basic.def.odr]). For
-a completion function set, receiver rcvr, and pack of arguments args,
-let c be the completion operation set(rcvr,args...), and let F be the function type decltype(auto(set))(decltype((args))...).
-A completion signature Sig is associated with c if and only if MATCHING-SIG(Sig,F) is true ([exec.general]). Together,
-a sender type and an environment type Env determine the set of completion
-signatures of an asynchronous operation that results from connecting the
-sender with a receiver that has an environment of type Env. The type of the receiver does not affect an asychronous
-operation’s completion signatures, only the type of the receiver’s
-environment.
-
-
A sender algorithm is a function that takes and/or
-returns a sender. There are three categories of sender algorithms:
-
-
-
A sender factory is a function that takes
-non-senders as arguments and that returns a sender.
-
-
A sender adaptor is a function that constructs and
-returns a parent sender from a set of one or more child senders and a
-(possibly empty) set of additional arguments. An asynchronous operation
-created by a parent sender is a parent to the
-child operations created by the child
-senders.
-
-
A sender consumer is a function that takes one or
-more senders and a (possibly empty) set of additional arguments, and
-whose return type is not the type of a sender.
The exposition-only type variant-or-empty<Ts...> is
- defined as follows:
-
-
-
If sizeof...(Ts) is greater than zero, variant-or-empty<Ts...> names the type variant<Us...> where Us... is the pack decay_t<Ts>... with
-duplicate types removed.
-
-
Otherwise, variant-or-empty<Ts...> names the
-exposition-only class type:
forwarding_query asks a query object whether it should be forwarded
-through queryable adaptors.
-
-
The name forwarding_query denotes a query object. For some query
-object q of type Q, forwarding_query(q) is expression-equivalent
-to:
-
-
-
mandate-nothrow-call(tag_invoke,forwarding_query,q) if that expression is well-formed.
-
-
-
Mandates: The expression above has type bool and is a core
-constant expressions if q is a core constant expression.
-
-
-
Otherwise, true if derived_from<Q,forwarding_query_t> is true.
-
-
Otherwise, false.
-
-
-
11.5.2. std::get_allocator[exec.get.allocator]
-
-
-
get_allocator asks an object for its associated allocator.
-
-
The name get_allocator denotes a query object. For some subexpression env, get_allocator(env) is expression-equivalent to mandate-nothrow-call(tag_invoke,get_allocator,as_const(env)).
-
-
-
Mandates: The type of the expression above
-satisfies Allocator.
-
-
-
forwarding_query(get_allocator) is true.
-
-
get_allocator() (with no arguments) is expression-equivalent to execution::read(get_allocator) ([exec.read]).
-
-
11.5.3. std::get_stop_token[exec.get.stop.token]
-
-
-
get_stop_token asks an object for an associated stop token.
-
-
The name get_stop_token denotes a query object. For some subexpression env, get_stop_token(env) is expression-equivalent to:
-
-
-
mandate-nothrow-call(tag_invoke,get_stop_token,as_const(env)), if this expression is well-formed.
-
-
-
Mandates: The type of the expression above satisfies stoppable_token.
-
-
-
Otherwise, never_stop_token{}.
-
-
-
forwarding_query(get_stop_token) is a core constant
-expression and has value true.
-
-
get_stop_token() (with no arguments) is expression-equivalent to execution::read(get_stop_token) ([exec.read]).
-
-
11.5.4. execution::get_env[exec.get.env]
-
-
-
get_env is a customization point object. For some subexpression o of type O, get_env(o) is expression-equivalent to
-
-
-
tag_invoke(get_env,const_cast<constO&>(o)) if that expression is
-well-formed.
-
-
-
Mandates: The expression above is not potentially throwing, and
-its type satisfies queryable ([exec.queryable]).
-
-
-
Otherwise, empty_env{}.
-
-
-
The value of get_env(o) shall be valid while o is valid.
-
-
When passed a sender object, get_env returns the sender’s attributes. When
-passed a receiver, get_env returns the receiver’s environment.
-
-
11.5.5. execution::get_domain[exec.get.domain]
-
-
-
get_domain asks an object for an associated execution domain tag.
-
-
The name get_domain denotes a query object. For some subexpression env, get_domain(env) is expression-equivalent to mandate-nothrow-call(tag_invoke,get_domain,as_const(env)),
-if this expression is well-formed.
-
-
forwarding_query(execution::get_domain) is a core constant
-expression and has value true.
-
-
get_domain() (with no arguments) is expression-equivalent to execution::read(get_domain) ([exec.read]).
get_scheduler asks an object for its associated scheduler.
-
-
The name get_scheduler denotes a query object. For some
-subexpression env, get_scheduler(env) is expression-equivalent to mandate-nothrow-call(tag_invoke,get_scheduler,as_const(env)).
-
-
-
Mandates: The type of the expression above satisfies scheduler.
-
-
-
forwarding_query(execution::get_scheduler) is a core constant
-expression and has value true.
-
-
get_scheduler() (with no arguments) is expression-equivalent to execution::read(get_scheduler) ([exec.read]).
get_delegatee_scheduler asks an object for a scheduler that can be used to delegate work to for the purpose of forward progress delegation.
-
-
The name get_delegatee_scheduler denotes a query object. For some
-subexpression env, get_delegatee_scheduler(env) is expression-equivalent to mandate-nothrow-call(tag_invoke,get_delegatee_scheduler,as_const(env)).
-
-
-
Mandates: The type of the expression above is satisfies scheduler.
-
-
-
forwarding_query(execution::get_delegatee_scheduler) is a core
-constant expression and has value true.
-
-
get_delegatee_scheduler() (with no arguments) is expression-equivalent to execution::read(get_delegatee_scheduler) ([exec.read]).
get_forward_progress_guarantee asks a scheduler about the forward progress guarantees of execution agents created by that scheduler.
-
-
The name get_forward_progress_guarantee denotes a query object. For some subexpression sch, let Sch be decltype((sch)). If Sch does not satisfy scheduler, get_forward_progress_guarantee is ill-formed.
-Otherwise, get_forward_progress_guarantee(sch) is expression-equivalent to:
-
-
-
mandate-nothrow-call(tag_invoke,get_forward_progress_guarantee,as_const(sch)), if this expression is well-formed.
-
-
-
Mandates: The type of the expression above is forward_progress_guarantee.
If get_forward_progress_guarantee(sch) for some scheduler sch returns forward_progress_guarantee::concurrent, all execution agents created by that scheduler shall provide the concurrent forward progress guarantee. If it returns forward_progress_guarantee::parallel, all execution agents created by that scheduler shall provide at least the parallel forward progress guarantee.
this_thread::execute_may_block_caller asks a scheduler sch whether a call execute(sch,f) with any invocable f may block the thread where such a call occurs.
-
-
The name this_thread::execute_may_block_caller denotes a query object. For some subexpression sch, let Sch be decltype((sch)). If Sch does not satisfy scheduler, this_thread::execute_may_block_caller is ill-formed. Otherwise, this_thread::execute_may_block_caller(sch) is expression-equivalent to:
-
-
-
mandate-nothrow-call(tag_invoke,this_thread::execute_may_block_caller,as_const(sch)), if this expression is well-formed.
-
-
-
Mandates: The type of the expression above is bool.
-
-
-
Otherwise, true.
-
-
-
If this_thread::execute_may_block_caller(sch) for some scheduler sch returns false, no execute(sch,f) call with some invocable f shall block the calling thread.
get_completion_scheduler<completion-tag> obtains the
-completion scheduler associated with a completion tag from a sender’s
-attributes.
-
-
The name get_completion_scheduler denotes a query object template. For some
-subexpression q, let Q be decltype((q)). If the template argument Tag in get_completion_scheduler<Tag>(q) is not one of set_value_t, set_error_t, or set_stopped_t, get_completion_scheduler<Tag>(q) is
-ill-formed. Otherwise, get_completion_scheduler<Tag>(q) is
-expression-equivalent to mandate-nothrow-call(tag_invoke,get_completion_scheduler<Tag>,as_const(q)) if this expression is
-well-formed.
-
-
-
Mandates: The type of the expression above satisfies scheduler.
-
-
-
If, for some sender sndr and completion function C that has an associated
-completion tag Tag, get_completion_scheduler<Tag>(get_env(sndr)) is
-well-formed and results in a scheduler sch, and the sender sndr invokes C(rcvr,args...), for some receiver rcvr that has been connected to sndr, with
-additional arguments args..., on an execution agent that does not
-belong to the associated execution resource of sch, the behavior is
-undefined.
-
-
The expression forwarding_query(get_completion_scheduler<CPO>) is a core constant expression and has value true.
-
-
11.6. Schedulers [exec.sched]
-
-
-
The scheduler concept defines the requirements of a scheduler type
-([async.ops]). schedule is a customization point object that accepts a
-scheduler. A valid invocation of schedule is a schedule-expression.
Let Sch be the type of a scheduler and let Env be the type of an execution
-environment for which sender_in<schedule_result_t<Sch>,Env> is true. Then sender-of-in<schedule_result_t<Sch>,Env> shall be true.
-
-
None of a scheduler’s copy constructor, destructor, equality comparison, or swap member functions shall exit via an exception.
-
-
None of these member functions, nor a scheduler type’s schedule function,
-shall introduce data races as a result of concurrent invocations of those
-functions from different threads.
-
-
For any two (possibly const) values sch1 and sch2 of some scheduler type Sch, sch1==sch2 shall return true only if both sch1 and sch2 share the
-same associated execution resource.
-
-
For a given scheduler expression sch, the expression get_completion_scheduler<set_value_t>(get_env(schedule(sch))) shall
-compare equal to sch.
-
-
For a given scheduler expression sch, if the expression get_domain(sch) is well-formed, then the expression get_domain(get_env(schedule(sch))) is also well-formed and has the same type.
-
-
A scheduler type’s destructor shall not block pending completion of any
-receivers connected to the sender objects returned from schedule. The ability to wait for completion of submitted function
-objects can be provided by the associated execution resource of the
-scheduler.
-
-
11.7. Receivers [exec.recv]
-
11.7.1. Receiver concepts [exec.recv.concepts]
-
-
-
A receiver represents the continuation of an asynchronous operation. The receiver concept defines the requirements for a receiver type
-([async.ops]). The receiver_of concept defines the requirements for a
-receiver type that is usable as the first argument of a set of completion
-operations corresponding to a set of completion signatures. The get_env customization point is used to access a receiver’s associated environment.
Remarks: Pursuant to [namespace.std], users can specialize enable_receiver to true for cv-unqualified program-defined types that model receiver, and false for types that do not. Such specializations shall be usable in constant
-expressions ([expr.const]) and have type constbool.
-
-
Let rcvr be a receiver and let op_state be an operation state associated
-with an asynchronous operation created by connecting rcvr with a sender. Let token be a stop token equal to get_stop_token(get_env(rcvr)). token shall
-remain valid for the duration of the asynchronous operation’s lifetime
-([async.ops]). This means that, unless it knows about
-further guarantees provided by the type of receiver rcvr, the implementation
-of op_state can not use token after it executes a completion operation.
-This also implies that any stop callbacks registered on token must be
-destroyed before the invocation of the completion operation.
-
-
11.7.2. execution::set_value[exec.set.value]
-
-
-
set_value is a value completion function ([async.ops]). Its associated
-completion tag is set_value_t. The expression set_value(rcvr,vs...) for
-some subexpression rcvr and pack of subexpressions vs is ill-formed if rcvr is an lvalue or a const rvalue. Otherwise, it is expression-equivalent to mandate-nothrow-call(tag_invoke,set_value,rcvr,vs...).
-
-
11.7.3. execution::set_error[exec.set.error]
-
-
-
set_error is an error completion function. Its associated completion tag is set_error_t. The expression set_error(rcvr,err) for some subexpressions rcvr and err is ill-formed if rcvr is an lvalue or a const rvalue. Otherwise, it is
-expression-equivalent to mandate-nothrow-call(tag_invoke,set_error,rcvr,err).
-
-
11.7.4. execution::set_stopped[exec.set.stopped]
-
-
-
set_stopped is a stopped completion function. Its associated completion tag
-is set_stopped_t. The expression set_stopped(rcvr) for some subexpression rcvr is ill-formed if rcvr is an lvalue or a const rvalue. Otherwise, it is
-expression-equivalent to mandate-nothrow-call(tag_invoke,set_stopped,rcvr).
-
-
11.8. Operation states [exec.opstate]
-
-
-
The operation_state concept defines the requirements of an operation state
-type ([async.ops]).
If an operation_state object is moved during the lifetime of its
-asynchronous operation ([async.ops]), the behavior is undefined.
-
-
Library-provided operation state types are non-movable.
-
-
11.8.1. execution::start[exec.opstate.start]
-
-
-
The name start denotes a customization point object that starts
-([async.ops]) the asynchronous operation associated with the operation state
-object. The expression start(O) for some subexpression O is ill-formed
-if O is an rvalue. Otherwise, it is expression-equivalent to:
-
mandate-nothrow-call(tag_invoke,start,O)
-
-
-
If the function selected by tag_invoke does not start the asynchronous
-operation associated with the operation state O, the behavior of calling start(O) is undefined.
-
-
11.9. Senders [exec.snd]
-
11.9.1. General [exec.snd.general]
-
-
-
For the purposes of this subclause, a sender is an object that satisfies the sender concept ([async.ops]).
-
-
Subclauses [exec.factories] and [exec.adapt] define customizable algorithms
-that return senders. Each algorithm has a default implementation. Let sndr be the result of an invocation of such an algorithm or an object equal to
-such ([concepts.equality]), and let Sndr be decltype((sndr)). Let rcvr be a receiver with associated environment env of type Env such that sender_in<Sndr,Env> is true. For the default implementation of the
-algorithm that produced sndr, connecting sndr to rcvr and starting the
-resulting operation state ([async.ops]) necessarily results in the potential
-evaluation ([basic.def.odr]) of a set of completion operations whose first
-argument is a subexpression equal to rcvr. Let Sigs be a pack of
-completion signatures corresponding to this set of completion operations.
-Then the type of the expression get_completion_signatures(sndr,env) is a
-specialization of the class template completion_signatures,
-([exec.utils.cmplsigs]) the set of whose template arguments is Sigs. If a
-user-provided implementation of the algorithm that produced sndr is
-selected instead of the default, any completion signature that is in the set
-of types denoted by completion_signatures_of_t<Sndr,Env> and that is not
-part of Sigs shall correspond to error or stopped completion operations,
-unless otherwise specified.
-
-
This subclause makes use of the following exposition-only entities.
-
-
-
For a queryable object env, let FWD-ENV(env) be a
-queryable object such that for a query object q and a pack of
-subexpressions as, the expression tag_invoke(q,FWD-ENV(env),as...) is ill-formed if forwarding_query(q) is false;
-otherwise, it is expression-equivalent to tag_invoke(q,env,as...).
-
-
For a query object q and a subexpression v, let MAKE-ENV(q,v) be a queryable object env such that
-the result of tag_invoke(q,env) has a value equal to v ([concepts.equality]). Unless otherwise stated, the object to which tag_invoke(q,env) refers remains valid while env remains valid.
-
-
For two queryable objects env1 and env2, a query object q and a
-pack of subexpressions as, let JOIN-ENV(env1,env2) be a queryable object env3 such that tag_invoke(q,env3,as...) is expression-equivalent to:
-
-
-
tag_invoke(q,env1,as...) if that expression is well-formed,
-
-
otherwise, tag_invoke(q,env2,as...) if that expression is
- well-formed,
-
-
otherwise, tag_invoke(q,env3,as...) is ill-formed.
-
-
-
The expansions of FWD-ENV, MAKE-ENV, and JOIN-ENV can be context-dependent; i.e., they can expand to
-expressions with different types and value categories in different
-contexts for the same arguments.
-
-
For a scheduler sch, let SCHED-ATTRS(sch) be a
-queryable object o1 such that tag_invoke(get_completion_scheduler<Tag>,o1) is a
-prvalue with the same type and value as sch where Tag is one
-of set_value_t or set_stopped_t; and let tag_invoke(get_domain,o1) be expression-equivalent to tag_invoke(get_domain,sch). Let SCHED-ENV(sch) be a queryable object o2 such that tag_invoke(get_scheduler,o2) is a prvalue with the same
-type and value as sch, and let tag_invoke(get_domain,o2) be expression-equivalent to tag_invoke(get_domain,sch).
-
-
For two subexpressions rcvr and expr, let SET-VALUE(rcvr,expr) be (expr,set_value(rcvr)) if the type of expr is void;
-otherwise, it is set_value(rcvr,expr). Let TRY-EVAL(rcvr,expr) be:
if expr is potentially-throwing; otherwise, expr. Let TRY-SET-VALUE(rcvr,expr) be TRY-EVAL(rcvr,SET-VALUE(rcvr,expr)) except that rcvr is evaluated only once.
Effects: Let COMPL-DOMAIN(T) be the type of the expression get_domain(get_completion_scheduler<T>(get_env(sndr))). If COMPL-DOMAIN(set_value_t), COMPL-DOMAIN(set_error_t), and COMPL-DOMAIN(set_stopped_t) all share a common type
-[meta.trans.other] (ignoring those types that are ill-formed), then completion-domain<Default>(sndr) is a default-constructed
-prvalue of that type.
-Otherwise, if all of those types are ill-formed, completion-domain<Default>(sndr) is a default-constructed
-prvalue of type Default.
-Otherwise, completion-domain<Default>(sndr) is ill-formed.
Otherwise, returnDomain(); where Domain is
-the first of the following expressions that is well-formed and has class
-type:
-
-
-
get_domain(get_env(sndr))
-
-
completion-domain<void>(sndr)
-
-
get_domain(env)
-
-
get_domain(get_scheduler(env))
-
-
default_domain().
-
-
-
The transfer algorithm is unique in that it ignores the
-execution domain of its predecessor, using only the domain of its
-destination scheduler to select a customization.
Remarks: The default template argument for the Data template parameter
-denotes an unspecified empty trivial class type.
-
-
Returns: A prvalue of type basic-sender<Tag,decay_t<Data>,decay_t<Child>...> where the tag member has been default-initialized and the data and childn... members have
-been direct initialized from their respective forwarded arguments, where basic-sender is the following exposition-only class template
-except as noted below:
-
template<classT,class...Us>
-conceptone-of=(same_as<T,Us>||...);// exposition only
-
-template<classTag>
-conceptcompletion-tag=// exposition only
- one-of<Tag,set_value_t,set_error_t,set_stopped_t>;
-
-template<template<class...>classT,class...Args>
-conceptwell-formed=requires{typenameT<Args...>;};// exposition only
-
-template<constauto&Fun,class...Args>
-conceptcpo-callable=callable<decltype(Fun),Args...>;// exposition only
-
-template<constauto&Fun,class...Args>
-usingcpo-result-t=call-result-t<decltype(Fun),Args...>;// exposition only
-
-structdefault-impls{// exposition only
- staticconstexprautoget-attrs=seebelow;
- staticconstexprautoget-env=seebelow;
- staticconstexprautoget-state=seebelow;
- staticconstexprautostart=seebelow;
- staticconstexprautocomplete=seebelow;
-};
-
-template<classTag>
-structimpls-for:default-impls{};// exposition only
-
-template<classSndr,classRcvr>// exposition only
-usingstate-type=decay_t<cpo-result-t<
- impls-for<tag_of_t<Sndr>>::get-state,Sndr,Rcvr&>>;
-
-template<classIndex,classSndr,classRcvr>// exposition only
-usingenv-type=cpo-result-t<
- impls-for<tag_of_t<Sndr>>::get-env,Index,
- state-type<Sndr,Rcvr>&,constRcvr&>>;
-
-template<classSndr,classRcvr,classIndex>// arguments are not associated entities ([lib.tmpl-heads])
- requireswell-formed<env-type,Index,Sndr,Rcvr>
-structbasic-receiver{// exposition only
- usingtag_t=tag_of_t<Sndr>;// exposition only
- usingreceiver_concept=receiver_t;
-
- template<completion-tagTag,class...Args>
- requirescpo-callable<impls-for<tag_t>::complete,
- Index,state-type<Sndr,Rcvr>&,Rcvr&,Tag,Args...>
- friendvoidtag_invoke(Tag,basic-receiver&&self,Args&&...args)noexcept{
- (void)impls-for<tag_t>::complete(
- Index(),self.op_->state_,self.op_->rcvr_,Tag(),std::forward<Args>(args)...);
- }
-
- template<same_as<get_env_t>Tag>
- friendautotag_invoke(Tag,constbasic-receiver&self)noexcept
- ->env-type<Index,Sndr,Rcvr>{
- constauto&rcvr=self.op_->rcvr_;
- returnimpls-for<tag_t>::get-env(Index(),self.op_->state_,rcvr);
- }
-
- basic-operation<Sndr,Rcvr>*op_;// exposition only
-};
-
-constexprautoconnect-all=// exposition only
- []<classSndr,classRcvr,size_t...Is>(
- basic-operation<Sndr,Rcvr>*op,Sndr&&sndr,index_sequence<Is...>)
- noexcept(TODO)requires(TODO){
- auto&&[ign1,ign2,...child]=std::forward<Sndr>(sndr);
- returnproduct-type{connect(
- std::forward_like<Sndr>(child),
- basic-receiver<Sndr,Rcvr,integral_constant<size_t,Is>>{op})...};
- };
-
-template<classSndr>
-usingindices-for=make_index_sequence<tuple_size_v<Sndr>-2>;// exposition only
-
-template<classSndr,classRcvr>
-usinginner-ops-tuple=// exposition only
- cpo-result-t<connect-all,basic-operation<Sndr,Rcvr>*,Sndr,
- indices-for<Sndr>>;
-
-template<classSndr,classRcvr>// arguments are not associated entities ([lib.tmpl-heads])
- requireswell-formed<state-type,Sndr,Rcvr>&&
- well-formed<inner-ops-tuple,Sndr,Rcvr>
-structbasic-operation{// exposition only
- usingtag_t=tag_of_t<Sndr>;// exposition only
-
- Rcvrrcvr_;// exposition only
- state-type<Sndr,Rcvr>state_;// exposition only
- inner-ops-tuple<Sndr,Rcvr>inner_ops_;// exposition only
-
- basic-operation(Sndr&&sndr,Rcvrrcvr)// exposition only
- :rcvr_(std::move(rcvr))
- ,state_(impls-for<tag_t>::get-state(std::forward<Sndr>(sndr),rcvr_))
- ,inner_ops_(connect-all(this,std::forward<Sndr>(sndr),indices-for<Sndr>()))
- {}
-
- friendvoidtag_invoke(start_t,basic-operation&self)noexcept{
- auto&[...ops]=self.inner_ops_;
- impls-for<tag_t>::start(self.state_,self.rcvr_,ops...);
- }
-};
-
-template<classSndr,classEnv>
-usingcompletion-signatures-for=seebelow;// exposition only
-
-template<classTag,classData,class...Child>// arguments are not associated entities ([lib.tmpl-heads])
-structbasic-sender{// exposition only
- usingsender_concept=sender_t;
-
- template<same_as<get_env_t>GetEnvTag>
- frienddecltype(auto)tag_invoke(GetEnvTag,constbasic-sender&self)noexcept{
- returnimpls-for<Tag>::get-attrs(data,child0,...childn-1);
- }
-
- template<same_as<connect_t>ConnectTag,
- decays-to<basic-sender>Self,receiverRcvr>
- friendautotag_invoke(ConnectTag,Self&&self,Rcvrrcvr)
- ->basic-operation<Self,Rcvr>{
- return{std::forward<Self>(self),std::move(rcvr)};
- }
-
- template<same_as<get_completion_signatures_t>GetComplSigsTag,
- decays-to<basic-sender>Self,classEnv>
- friendautotag_invoke(GetComplSigsTag,Self&&self,Env&&env)noexcept
- ->completion-signatures-for<Self,Env>{
- return{};
- }
-
- Tagtag;// exposition only
- Datadata;// exposition only
- Child0child0;// exposition only
- Child1child1;// exposition only
- ...
- Childn-1childn-1;// exposition only
-};
-
-template<classSndr>
-usingdata-type=decltype((declval<Sndr>().data));// exposition only
-
-template<classSndr,size_tN=0>
-usingchild-type=decltype((declval<Sndr>().childN));// exposition only
-
-
-
It is unspecified whether instances of basic-sender can be
-aggregate initialized.
-
-
An expression of type basic-sender is usable as the
-initializer of a structured binding declaration
-[dcl.struct.bind].
-
-
The member default-impls::get-attrs is initialized
-with a callable object equivalent to the following lambda:
For a subexpression sndr let Sndr be decltype((sndr)). Let rcvr be a receiver that has an associated environment of type Env such that sender_in<Sndr,Env> is true. completion-signatures-for<Sndr,Env> denotes
-a specialization of completion_signatures, the set of whose
-template arguments correspond to the set of completion operations
-that are potentially evaluated as a result of calling start on the
-operation state that results from connecting sndr and rcvr. When sender_in<Sndr,Env> is false, the type denoted by completion-signatures-for<Sndr,Env>, if any,
-is not a specialization of completion_signatures.
-
Recommended practice: When sender_in<Sndr,Env> is false,
-implementations are encouraged to use the type denoted by completion-signatures-for<Sndr,Env> to
-communicate to users why.
-
-
-
-
11.9.2. Sender concepts [exec.snd.concepts]
-
-
-
The sender concept defines the requirements for a sender type
-([async.ops]). The sender_in concept defines the requirements for a sender
-type that can create asynchronous operations given an associated environment
-type. The sender_to concept defines the requirements for a sender type
-that can connect with a specific receiver type. The get_env customization
-point object is used to access a sender’s associated attributes. The connect customization point object is used to connect ([async.ops]) a
-sender and a receiver to produce an operation state.
Given a subexpression sndr, let Sndr be decltype((sndr)), let Env be
-the type of an environment, and let rcvr be a receiver with an associated
-environment Env. A completion operation is a permissible completion for Sndr and Env if its
-completion signature appears in the argument list of the specialization of completion_signatures denoted by completion_signatures_of_t<Sndr,Env>. Sndr and Env model sender_in<Sndr,Env> if all the completion
-operations that are potentially evaluated by connecting sndr to rcvr and
-starting the resulting operation state are permissible completions for Sndr and Env.
-
-
A type Sigs satisfies and models the exposition-only concept valid-completion-signatures if it denotes a specialization
-of the completion_signatures class template.
-
-
Remarks: Pursuant to [namespace.std], users can specialize enable_sender to true for cv-unqualified program-defined types that model sender, and false for types that do not. Such specializations shall be usable in constant
-expressions ([expr.const]) and have type constbool.
-
-
The exposition-only concepts sender-of and sender-of-in define the requirements for a sender
-type that completes with a given unique set of value result types.
Let sndr be an expression such that decltype((sndr)) is Sndr. The type tag_of_t<Sndr> is as follows:
-
-
-
If the declaration auto&&[tag,data,...children]=sndr; would be
-well-formed, tag_of_t<Sndr> is an alias for decltype(auto(tag)).
-
-
Otherwise, tag_of_t<Sndr> is ill-formed.
-
-
There is no way in standard C++ to determine whether the above declaration
-is well-formed without causing a hard error, so this presumes compiler
-magic. However, the author anticipates the adoption of [@P2141R1], which
-makes it possible to implement this purely in the library. P2141 has already
-been approved by EWG for C++26.
-
-
Let sender-for be an exposition-only concept defined as follows:
For a type T, SET-VALUE-SIG(T) denotes the type set_value_t() if T is cvvoid; otherwise, it denotes the type set_value_t(T).
-
-
Library-provided sender types:
-
-
-
Always expose an overload of a customization of connect that accepts an rvalue sender.
-
-
Only expose an overload of a customization of connect that
- accepts an lvalue sender if they model copy_constructible.
-
-
Model copy_constructible if they satisfy copy_constructible.
-
-
-
11.9.3. Awaitable helpers [exec.awaitables]
-
-
-
The sender concepts recognize awaitables as senders. For this clause
-([exec]), an awaitable is an expression that would be
-well-formed as the operand of a co_await expression within a given
-context.
-
-
For a subexpression c, let GET-AWAITER(c,p) be
-expression-equivalent to the series of transformations and conversions
-applied to c as the operand of an await-expression in a coroutine,
-resulting in lvalue e as described by [expr.await]/3.2-4, where p is an lvalue referring to the coroutine’s promise type, Promise. This includes the invocation of the promise type’s await_transform member if any, the invocation of the operatorco_await picked by overload resolution if any, and any necessary implicit
-conversions and materializations.
-
I have opened cwg#250 to give these
-transformations a term-of-art so we can more easily refer to it here.
-
-
Let is-awaitable be the following exposition-only
-concept:
await-suspend-result<T> is true if and only if one
- of the following is true:
-
-
-
T is void, or
-
-
T is bool, or
-
-
T is a specialization of coroutine_handle.
-
-
-
For a subexpression c such that decltype((c)) is type C, and
-an lvalue p of type Promise, await-result-type<C,Promise> denotes the type decltype(GET-AWAITER(c,p).await_resume()).
-
-
Let with-await-transform be the exposition-only class template:
Returns:tag_of_t<Sndr>().transform_env(std::forward<Sndr>(sndr),std::forward<Env>(env)) if that expression is well-formed; otherwise, static_cast<Env>(std::forward<Env>(env)).
-
-
Mandates: The selected expression in Returns: is not potentially throwing.
Returns:Tag().apply_sender(std::forward<Sndr>(sndr),std::forward<Args>(args)...) if that expression is well-formed; otherwise, this function shall not participate
-in overload resolution.
-
-
Remarks: The exception specification is equivalent to:
Returns: Let ENV be a parameter pack consisting of
- the single expression env for the second overload and an empty pack for
- the first. Let sndr2 be the expression dom.transform_sender(std::forward<Sndr>(sndr),ENV...) if that expression is well-formed; otherwise, default_domain().transform_sender(std::forward<Sndr>(sndr),ENV...). If sndr2 and sndr have the same type ignoring cv qualifiers, returns sndr2; otherwise, transform_sender(dom,sndr2,ENV...).
Returns:dom.transform_sender(std::forward<Sndr>(sndr),std::forward<Env>(env)) if that
- expression is well-formed; otherwise, default_domain().transform_sender(std::forward<Sndr>(sndr),std::forward<Env>(env)).
Returns:dom.apply_sender(Tag(),std::forward<Sndr>(sndr),std::forward<Args>(args)...) if that
- expression is well-formed; otherwise, default_domain().apply_sender(Tag(),std::forward<Sndr>(sndr),std::forward<Args>(args)...) if that expression is well-formed; otherwise, this function shall not participate in
- overload resolution.
-
-
Remarks: The exception specification is equivalent to:
get_completion_signatures is a customization point object. Let sndr be an
-expression such that decltype((sndr)) is Sndr, and let env be an expression
-such that decltype((env)) is Env. Then get_completion_signatures(sndr,env) is
-expression-equivalent to:
-
-
-
tag_invoke_result_t<get_completion_signatures_t,Sndr,Env>{} if that
-expression is well-formed,
-
-
Otherwise, remove_cvref_t<Sndr>::completion_signatures{} if that expression is well-formed,
-
-
Otherwise, if is-awaitable<Sndr,env-promise<Env>> is true, then:
-
completion_signatures<
- SET-VALUE-SIG(await-result-type<Sndr,env-promise<Env>>),// see [exec.snd.concepts]
- set_error_t(exception_ptr),
- set_stopped_t()>{}
-
-
-
Otherwise, get_completion_signatures(sndr,env) is ill-formed.
-
-
-
Let rcvr be an rvalue receiver of type Rcvr, and let Sndr be the type of a
-sender such that sender_in<Sndr,env_of_t<Rcvr>> is true. Let Sigs... be the
-template arguments of the completion_signatures specialization named by completion_signatures_of_t<Sndr,env_of_t<Rcvr>>. Let CSO be
-a completion function. If sender Sndr or its operation state cause the
-expression CSO(rcvr,args...) to be potentially evaluated
-([basic.def.odr]) then there shall be a signature Sig in Sigs... such
-that MATCHING-SIG(tag_t<CSO>(decltype(args)...),Sig) is true ([exec.general]).
-
-
11.9.8. execution::connect[exec.connect]
-
-
-
connect connects ([async.ops]) a sender with a receiver.
-
-
The name connect denotes a customization point object. For subexpressions sndr and rcvr, let Sndr be decltype((sndr)) and Rcvr be decltype((rcvr)), and let DS and DR be the decayed types of Sndr and Rcvr, respectively.
-
-
Let connect-awaitable-promise be the following class:
If Sndr does not satisfy sender or if Rcvr does not satisfy receiver, connect(sndr,rcvr) is ill-formed. Otherwise, the expression connect(sndr,rcvr) is
-expression-equivalent to:
-
-
-
tag_invoke(connect,sndr,rcvr) if connectable-with-tag-invoke<Sndr,Rcvr> is modeled.
-
-
-
Mandates: The type of the tag_invoke expression above
-satisfies operation_state.
-
-
-
Otherwise, connect-awaitable(sndr,rcvr) if that expression is
-well-formed.
-
-
Otherwise, connect(sndr,rcvr) is ill-formed.
-
-
-
11.9.9. Sender factories [exec.factories]
-
11.9.9.1. execution::schedule[exec.schedule]
-
-
-
schedule obtains a schedule-sender ([async.ops]) from a scheduler.
-
-
The name schedule denotes a customization point object. For some
-subexpression sch, the expression schedule(sch) is expression-equivalent to:
-
-
-
tag_invoke(schedule,sch), if that expression is valid. If the function
-selected by tag_invoke does not return a sender whose set_value completion scheduler is equivalent to sch, the behavior of calling schedule(sch) is undefined.
-
-
-
Mandates: The type of the tag_invoke expression above
-satisfies sender.
just, just_error, and just_stopped are sender factories whose
-asynchronous operations complete synchronously in their start operation
-with a value completion operation, an error completion operation, or a
-stopped completion operation respectively.
-
-
The names just, just_error, and just_stopped denote customization
-point objects. Let just-cpo be one of just, just_error, or just_stopped. For a pack of subexpressions ts, let Ts be the template parameter pack decltype((ts)). The expression just-cpo(ts...) is ill-formed if:
-
-
-
(movable-value<Ts>&&...) is false, or
-
-
just-cpo is just_error and sizeof...(ts)==1 is false, or
-
-
just-cpo is just_stopped and sizeof...(ts)==0 is false;
-
-
Otherwise, it is expression-equivalent to make-sender(just-cpo,product-type{vs...}).
-
-
For just, just_error, and just_stopped, let set-cpo be set_value, set_error, and set_stopped respectively. The
-exposition-only class template impls-for ([exec.snd.general]) is specialized for just-cpo as
-follows:
read is a sender factory for a sender whose asynchronous operation
-completes synchronously in its start operation with a value completion
-result equal to a value read from the receiver’s associated environment.
-
-
read is a customization point object. For some query object q,
-the expression read(q) is expression-equivalent to make-sender(read,q).
-
-
The exposition-only class template impls-for ([exec.snd.general])
-is specialized for read as follows:
Subclause [exec.adapt] specifies a set of sender adaptors.
-
-
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 that would observe or modify any of the arguments
-of the adaptor before the returned sender is connected with a receiver using connect, and start is called on the resulting operation state. This
-requirement applies to any function that is selected by the implementation
-of the sender adaptor.
-
-
Unless otherwise specified, a parent sender ([async.ops]) with a single child
-sender sndr has an associated attribute object equal to FWD-ENV(get_env(sndr)) ([exec.fwd.env]). Unless
-otherwise specified, a parent sender with more than one child senders has an
-associated attributes object equal to empty_env{}. These
-requirements apply to any function that is selected by the implementation of
-the sender adaptor.
-
-
Unless otherwise specified, when a parent sender is connected to a receiver rcvr, any receiver used to connect a child sender has an associated
-environment equal to FWD-ENV(get_env(rcvr)). This
-requirements applies to any sender returned from a function that is selected
-by the implementation of such sender adaptor.
-
-
For any sender type, receiver type, operation state type, queryable type, or
-coroutine promise type that is part of the implementation of any sender
-adaptor in this subclause and that is a class template, the template
-arguments do not contribute to the associated entities
-([basic.lookup.argdep]) of a function call where a specialization of the
-class template is an associated entity.
If a sender returned from a sender adaptor specified in this subclause is
-specified to include set_error_t(Err) among its set of completion signatures
-where decay_t<Err> denotes the type exception_ptr, but the implementation
-does not potentially evaluate an error completion operation with an exception_ptr argument, the implementation is allowed to omit the exception_ptr error completion signature from the set.
A pipeable sender adaptor closure object is a function object that
-accepts one or more sender arguments and returns a sender. For a sender
-adaptor closure object c and an expression sndr such that decltype((sndr)) models sender, the following expressions are equivalent
-and yield a sender:
-
c(sndr)
-sndr|c
-
-
Given an additional pipeable sender adaptor closure object d, the expression c|d produces another pipeable sender adaptor closure object e:
-
e is a perfect forwarding call wrapper ([func.require]) with the following properties:
-
-
-
Its target object is an object d2 of type decay_t<decltype((d))> direct-non-list-initialized with d.
-
-
It has one bound argument entity, an object c2 of type decay_t<decltype((c))> direct-non-list-initialized with C.
-
-
Its call pattern is d2(c2(arg)), where arg is the argument used in a function call expression of e.
-
-
The expression c|d is well-formed if and only if the initializations of the state entities of e are all well-formed.
-
-
An object t of type T is a pipeable sender adaptor closure object if T models derived_from<sender_adaptor_closure<T>>, T has no other base
-classes of type sender_adaptor_closure<U> for any other type U, and T does not model sender.
-
-
The template parameter D for sender_adaptor_closure can be an incomplete type. Before any expression of type cvD appears as
-an operand to the | operator, D shall be complete and model derived_from<sender_adaptor_closure<D>>. The behavior of an expression involving an
-object of type cvD as an operand to the | operator is undefined if overload resolution selects a program-defined operator| function.
-
-
A pipeable sender adaptor object is a customization point object that accepts a sender as its first argument and returns a 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 adaptor accepts more than one argument, then let sndr be an expression such that decltype((sndr)) models sender,
-let args... be arguments such that adaptor(sndr,args...) is a well-formed expression as specified in the rest of this subclause
-([exec.adapt.objects]), and let BoundArgs be a pack that denotes decay_t<decltype((args))>.... The expression adaptor(args...) produces a pipeable sender adaptor closure object f that is a perfect forwarding call wrapper with the following properties:
-
-
-
Its target object is a copy of adaptor.
-
-
Its bound argument entities bound_args consist of objects of types BoundArgs... direct-non-list-initialized with std::forward<decltype((args))>(args)..., respectively.
-
-
Its call pattern is adaptor(rcvr,bound_args...), where rcvr is the argument used in a function call expression of f.
-
-
The expression adaptor(args...) is well-formed if and only if the initializations of the bound argument entities of the result, as specified above,
- are all well-formed.
-
-
11.9.10.3. execution::on[exec.on]
-
-
-
on adapts an input sender into a sender that will start on an execution
-agent belonging to a particular scheduler’s associated execution resource.
-
-
The name on denotes a customization point object. For some subexpressions sch and sndr, if decltype((sch)) does not satisfy scheduler, or decltype((sndr)) does not satisfy sender, on(sch,sndr) is ill-formed.
-
-
Otherwise, the expression on(sch,sndr) is expression-equivalent to:
Let out_sndr and env be subexpressions such that OutSndr is decltype((out_sndr)). If sender-for<OutSndr,on_t> is false, then the expressions on.transform_env(out_sndr,env) and on.transform_sender(out_sndr,env) are ill-formed;
-otherwise:
Let out_sndr be a subexpression denoting a sender returned from on(sch,sndr) or one equal to such, and let OutSndr be the type decltype((out_sndr)). Let out_rcvr be a subexpression denoting a receiver that has an environment of
-type Env such that sender_in<OutSndr,Env> is true. Let op be an lvalue
-referring to the operation state that results from connecting out_sndr with out_rcvr. Calling start(op) shall start sndr on an execution agent of the
-associated execution resource of sch, or failing that, shall execute an
-error completion on out_rcvr.
-
-
11.9.10.4. execution::transfer[exec.transfer]
-
-
-
transfer adapts a sender into one with a different associated set_value completion scheduler. It results in a transition
-between different execution resources when executed.
-
-
The name transfer denotes a customization point object. For some
-subexpressions sch and sndr, if decltype((sch)) does not satisfy scheduler, or decltype((sndr)) does not satisfy sender, transfer(sndr,sch) is ill-formed.
-
-
Otherwise, the expression transfer(sndr,sch) is expression-equivalent to:
Let sndr and env be subexpressions such that Sndr is decltype((sndr)). If sender-for<Sndr,transfer_t> is false, then the expression transfer.transform_sender(sndr,env) is ill-formed; otherwise, it
-is equal to:
This causes the transfer(sndr,sch) sender to become schedule_from(sch,sndr) when it is connected with a receiver with an
-execution domain that does not customize transfer.
-
-
Let out_sndr be a subexpression denoting a sender returned from transfer(sndr,sch) or one equal to such, and let OutSndr be the type decltype((out_sndr)). Let out_rcvr be a subexpression denoting a
-receiver that has an environment of type Env such that sender_in<OutSndr,Env> is true. Let op be an lvalue referring to the operation state that
-results from connecting out_sndr with out_rcvr. Calling start(op) shall start sndr on the current execution agent and execute completion
-operations on out_rcvr on an execution agent of the execution resource
-associated with sch. If scheduling onto sch fails, execute an error
-completion on out_rcvr on an unspecified execution agent.
schedule_from schedules work dependent on the completion of a sender onto a
-scheduler’s associated execution resource. schedule_from is not meant to be used in user code; it is
-used in the implementation of transfer.
-
-
The name schedule_from denotes a customization point object. For some
-subexpressions sch and sndr, let Sch be decltype((sch)) and Sndr be decltype((sndr)). If Sch does not satisfy scheduler, or Sndr does not
-satisfy sender, schedule_from is ill-formed.
-
-
Otherwise, the expression schedule_from(sch,sndr) is expression-equivalent
-to:
Let Sigs be a pack of the arguments to the completion_signatures specialization named by completion_signatures_of_t<Child,env_of_t<Rcvr>>. Let as-tuple be an alias template that transforms a
- completion signature Tag(Args...) into the tuple specialization decayed-tuple<Tag,Args...>.
- Then variant-type denotes the type variant<monostate,as-tuple<Sigs>...>,
- except with duplicate types removed.
Let the subexpression out_sndr denote the result of the invocation schedule_from(sch,sndr) or an object copied or moved from such, and let
-the subexpression rcvr denote a receiver such that the expression connect(out_sndr,rcvr) is well-formed. The expression connect(out_sndr,rcvr) has undefined behavior unless it creates an
-asynchronous operation ([async.ops]) that, when started:
-
-
-
eventually completes on an execution agent belonging to the associated
-execution resource of sch, and
then attaches an invocable as a continuation for an input sender’s value
-completion operation. upon_error and upon_stopped do the same for the
-error and stopped completion operations respectively, sending the result
-of the invocable as a value completion.
-
-
The names then, upon_error, and upon_stopped denote customization point
-objects. Let the expression then-cpo be one of then, upon_error, or upon_stopped. For subexpressions sndr and f, let Sndr be decltype((sndr)) and let F be the decayed type of f. If Sndr does not
-satisfy sender, or F does not satisfy movable-value, then-cpo(sndr,f) is ill-formed.
-
-
Otherwise, the expression then-cpo(sndr,f) is
-expression-equivalent to:
For then, upon_error, and upon_stopped, let set-cpo be set_value, set_error, and set_stopped respectively. The
-exposition-only class template impls-for ([exec.snd.general]) is specialized for then-cpo as follows:
The expression then-cpo(sndr,f) has undefined behavior
-unless it returns a sender out_sndr that:
-
-
-
Invokes f or a copy of such with the value, error, or stopped result
-datums of sndr (for then, upon_error, and upon_stopped respectively), using the result value of f as out_sndr's value
-completion, and
-
-
Forwards all other completion operations unchanged.
let_value, let_error, and let_stopped transform a sender’s value,
-error, and stopped completions respectively into a new child asynchronous
-operation by passing the sender’s result datums to a user-specified
-callable, which returns a new sender that is connected and started.
-
-
Let the expression let-cpo be one of let_value, let_error, or let_stopped and let set-cpo be the
-completion function that corresponds to let-cpo (set_value for let_value, etc.). For a subexpression sndr, let let-env(sndr) be expression-equivalent to the first
-well-formed expression below:
The names let_value, let_error, and let_stopped denote customization
-point objects. For subexpressions sndr and f, let Sndr be decltype((sndr)),
-let F be the decayed type of f. If Sndr does not satisfy sender or if F does not satisfy movable-value, the expression let-cpo(sndr,f) is ill-formed. If F does not satisfy invocable, the expression let_stopped(sndr,f) is ill-formed.
-
-
Otherwise, the expression let-cpo(sndr,f) is
-expression-equivalent to:
Let Sigs be a pack of the arguments to the completion_signatures specialization named by completion_signatures_of_t<child-type<Sndr>,env_of_t<Rcvr>>. Let LetSigs be a pack of those types in Sigs with a return type of tag_t<set-cpo>. Let as-tuple be an alias template such that as-tuple<Tag(Args...)> denotes the type decayed-tuple<Args...>. Then args-variant-type denotes the type variant<monostate,as-tuple<LetSigs>...>.
-
-
Let as-sndr2 be an alias template such that as-sndr2<Tag(Args...)> denotes the type call-result-t<Fn,decay_t<Args>&...>.
-Then ops2-variant-type denotes the type variant<monostate,connect_result_t<as-sndr2<LetSigs>,receiver2<Rcvr,Env>>...>.
-
-
The requires-clause constraining the above lambda is
-satisfied if and only if the types args-variant-type and ops2-variant-type are well-formed.
-
-
-
The exposition-only function template let-bind is equal to:
Let sndr and env be subexpressions, and let Sndr be decltype((sndr)).
-If sender-for<Sndr,tag_t<let-cpo>> is false, then the expression let-cpo.transform_env(sndr,env) is ill-formed. Otherwise, it is equal to JOIN-ENV(let-env(sndr),FWD-ENV(env)).
-
-
Let the subexpression out_sndr denote the result of the invocation let-cpo(sndr,f) or an object copied or moved from such,
-and let the subexpression rcvr denote a receiver such that the expression connect(out_sndr,rcvr) is well-formed. The expression connect(out_sndr,rcvr) has undefined behavior unless it creates an asynchronous operation
-([async.ops]) that, when started:
-
-
-
invokes f when set-cpo is called with sndr's
- result datums,
-
-
makes its completion dependent on the completion of a sender returned
- by f, and
-
-
propagates the other completion operations sent by sndr.
-
-
-
11.9.10.8. execution::bulk[exec.bulk]
-
-
-
bulk runs a task repeatedly for every index in an index space.
-
-
The name bulk denotes a customization point object. For subexpressions sndr, shape, and f, let Sndr be decltype((sndr)), let Shape be
-the decayed type of shape, and let F be the decayed type of f. If Sndr does not satisfy sender, or if Shape does not satisfy integral,
-or if F does not satisfy movable-value, bulk(sndr,shape,f) is ill-formed.
-
-
Otherwise, the expression bulk(sndr,shape,f) is
-expression-equivalent to:
The expression in the requires-clause of the lambda above is true if and only if Tag denotes a type other than set_value_t or if the expression f(auto(shape),args...) is well-formed.
-
-
-
-
Let the subexpression out_sndr denote the result of the invocation bulk(sndr,shape,f) or an object copied or moved from such,
-and let the subexpression rcvr denote a receiver such that the expression connect(out_sndr,rcvr) is well-formed. The expression connect(out_sndr,rcvr) has undefined behavior unless it creates an asynchronous operation
-([async.ops]) that, when started:
-
-
-
on a value completion operation, invokes f(i,args...) for every i of type Shape from 0 to shape, where args is a pack of lvalue
- subexpressions referring to the value completion result datums of the
- input sender, and
-
-
propagates all completion operations sent by sndr.
-
-
-
11.9.10.9. execution::split and execution::ensure_started[exec.split]
-
-
-
split adapts an arbitrary sender into a sender that can be connected
-multiple times. ensure_started eagerly starts the execution of a sender,
-returning a sender that is usable as input to additional sender algorithms.
-
-
Let shared-env be the type of an environment such that,
-given an instance env, the expression get_stop_token(env) is well-formed
-and has type in_place_stop_token.
-
-
- The names split and ensure_started denote customization point objects.
-Let the expression shared-cpo be one of split or ensure_started. For a subexpression sndr, let Sndr be decltype((sndr)). If sender_in<Sndr,shared-env>
- or constructible_from<decay_t<env_of_t<Sndr>>,env_of_t<Sndr>>
- is false, shared-cpo(sndr) is ill-formed.
-
-
Although it has not yet been approved by LEWG, there
-is a bug in the current wording that makes it impossible to safely copy the
-attributes of a sender; it may have reference semantics, leading to a
-dangling reference. I am striking this part for now and will bring a fix to
-LEWG.
-
-
Otherwise, the expression shared-cpo(sndr) is
-expression-equivalent to:
The default implementation of transform_sender will have the effect of connecting the sender to a receiver and, in the
-case of ensure_started, calling start on the resulting operation
-state. It will return a sender with a different tag type.
-
-
-
Let local-state denote the following exposition-only class:
Let Sigs be a pack of the arguments to the completion_signatures specialization named by completion_signatures_of_t<Sndr>. Let as-tuple be an
- alias template such that as-tuple<Tag(Args...)> denotes the type decayed-tuple<Tag,Args...>. Then variant-type denotes the type variant<tuple<set_stopped_t>,tuple<set_error_t,exception_ptr>,as-tuple<Sigs>...>, but with
- duplicate types removed.
-
-
Let state-list-type be a type that stores a list of pointers
- to local-state-base objects and that permits atomic insertion.
- Let state-flag-type be a type that can be atomically toggled
- between true and false.
-
-
explicitshared-state(Sndr&&sndr);
-
-
-
Effects: Initializes op_state with the result of connect(std::forward<Sndr>(sndr),shared-receiver{this}).
-
-
Postcondition:waiting_states is empty, and completed is false.
-
-
-
voidstart-op()noexcept;
-
-
-
Effects:inc-ref(). If stop_src.stop_requested() is true, calls notify(); otherwise, calls start(op_state).
-
-
-
voidnotify()noexcept;
-
-
-
Effects: Atomically does the following:
-
-
-
Sets completed to true, and
-
-
Exchanges waiting_states with an empty list, storing the old
-value in a local prior_states. For each pointer p in prior_states, calls p->notify().
-Finally, calls dec-ref().
-
-
-
-
voiddetach()noexcept;
-
-
-
Effects: If completed is false and waiting_states is empty,
- calls stop_src.request_stop(). This has
- the effect of requesting early termination of any asynchronous
- operation that was started as a result of a call to ensure_started,
- but only if the resulting sender was never connected and started.
-
-
-
voidinc-ref()noexcept;
-
-
-
Effects: Increments ref_count.
-
-
-
voiddec-ref()noexcept;
-
-
-
Effects: Decrements ref_count. If the new value of ref_count is 0, calls deletethis.
-
-
Synchronization: If dec_ref() does not decrement the ref_count to 0 then synchronizes with
- the call to dec_ref() that decrements ref_count to 0.
-
-
-
-
For each type split_t and ensure_started_t, there is a different,
-associated exposition-only implementation tag type, split-impl-tag and ensure-started-impl-tag, respectively. Let shared-impl-tag be the associated implementation tag type of shared-cpo. Given an expression sndr, the expression shared-cpo.transform_sender(sndr) is equivalent to:
where shared-wrapper is an exposition-only class that manages the
-reference count of the shared-state object pointed to by sh_state. shared-wrapper models movable with move operations nulling out the
-moved-from object. If tag is split_t, shared-wrapper models copyable with copy operations incrementing the reference count by calling sh_state->inc-ref(). The constructor calls sh_state->start-op() if tag is ensure_started_t. The
-destructor has no effect if sh_state is null; otherwise, it calls sh_state->detach() if tag is ensure_started_t;
-and finally, it decrements the reference count by calling sh_state->dec-ref().
-
-
The exposition-only class template impls-for ([exec.snd.general]) is specialized for shared-impl-tag as follows:
If shared-impl-tag is ensure-started-impl-tag, and if state.sh_state->stop_src.stop_requested() is true, calls set_stopped(std::move(rcvr)) and returns.
-
-
Then atomically does the following:
-
-
-
Inserts &state into state.sh_state->waiting_states, and
-
-
Reads the value of state.sh_state->completed.
-
-
If the value is true, calls state.notify() and returns.
-
-
If shared-impl-tag is split-impl-tag, and if &state is the first item added to state.sh_state->waiting_states, calls state.sh_state->start-op().
-
-
-
-
-
- Under the following conditions, the results of the
-child operation are discarded:
-
-
-
When a sender returned from ensure_started is destroyed without being
-connected to a receiver, or
-
-
If the sender is connected to a receiver but the operation state
-is destroyed without having been started, or
-
-
If polling the receiver’s stop token indicates that stop has been
-requested when start is called, and the operation has not yet
-completed.
-
-
-
-
-
11.9.10.10. execution::when_all[exec.when.all]
-
-
-
when_all and when_all_with_variant both adapt multiple input senders into
-a sender that completes when all input senders have completed. when_all only accepts senders with a single value completion signature and on success
-concatenates all the input senders' value result datums into its own value
-completion operation. when_all_with_variant(sndrs...) is semantically
-equivalent to when_all(into_variant(sndrs)...), where sndrs is a pack of
-subexpressions of sender types.
-
-
The names when_all and when_all_with_variant denote customization point
-objects. For some subexpressions sndri..., let Sndri... be decltype((sndri)).... The expressions when_all(sndri...) and when_all_with_variant(sndri...) are ill-formed if
-any of the following is true:
-
-
-
If the number of subexpressions in sndri... is 0, or
-
-
If any type Sndri does not satisfy sender.
-
-
If the types of the expressions get-domain-early(sndri) do not share
-a common type ([meta.trans.other]) for all values of i.
-
-
Otherwise, let CD be the common type of the input senders' domains.
-
-
The expression when_all(sndri...) is
-expression-equivalent to:
Let copy-fail be exception_ptr if decay-copying any of the
- input senders' result datums can potentially throw; otherwise, none-such, where none-such is an unspecified
- empty class type.
-
-
The alias values_tuple denotes the type tuple<value_types_of_t<Sndrs,env_of_t<Rcvr>,decayed-tuple,optional>...> if that type is well-formed;
- otherwise, tuple<>.
-
-
The alias errors_variant denotes the type variant<none-such,copy-fail,Es...> with duplicate types removed, where Es is the pack of the decayed types of all the
- input senders' possible error result datums.
-
-
The member voidstate::complete(Rcvr&rcvr)noexcept behaves as follows:
-
-
-
If disp is equal to disposition::started,
- evaluates:
if the expression decltype(auto(e))(e) is potentially throwing; otherwise, v.templateemplace<decltype(auto(e))>(e); and where TRY-EMPLACE-VALUE(c,o,as...), for subexpressions c, o, and pack of subexpressions as, is equivalent to:
Given subexpressions sndr and env, if sender-for<decltype((sndr)),when_all_with_variant_t> is false,
-then the expression when_all_with_variant.transform_sender(sndr,env) is
-ill-formed; otherwise, the body of the transform_sender member-function is equivalent to:
This causes the when_all_with_variant(sndr...) sender
-to become when_all(into_variant(sndr)...) when it is connected with a
-receiver with an execution domain that does not customize when_all_with_variant.
The name into_variant denotes a customization point object. For a
-subexpression sndr, let Sndr be decltype((sndr)). If Sndr does not
-satisfy sender, into_variant(sndr) is ill-formed.
-
-
Otherwise, the expression into_variant(sndr) is expression-equivalent to:
stopped_as_optional maps an input sender’s stopped completion operation into the value completion operation as an empty optional. The input sender’s value completion operation is also converted into an optional. The result is a sender that never completes with stopped, reporting cancellation by completing with an empty optional.
-
-
The name stopped_as_optional denotes a customization point object. For some subexpression sndr, let Sndr be decltype((sndr)).
-The expression stopped_as_optional(sndr) is expression-equivalent to:
Let sndr and env be subexpressions such that Sndr is decltype((sndr)) and Env is decltype((env)).
-If either sender-for<Sndr,stopped_as_optional_t> or single-sender<Sndr,Env> is false then the expression stopped_as_optional.transform_sender(sndr,env) is ill-formed; otherwise, it is equal to:
stopped_as_error maps an input sender’s stopped completion operation into
-an error completion operation as a custom error type. The result is a sender
-that never completes with stopped, reporting cancellation by completing with
-an error.
-
-
The name stopped_as_error denotes a customization point object. For some subexpressions sndr and err, let Sndr be decltype((sndr)) and let Err be decltype((err)). If the type Sndr does not satisfy sender or if the type Err doesn’t satisfy movable-value, stopped_as_error(sndr,err) is ill-formed. Otherwise, the expression stopped_as_error(sndr,err) is expression-equivalent to:
Let sndr and env be subexpressions such that Sndr is decltype((sndr)) and Env is decltype((env)).
-If sender-for<Sndr,stopped_as_error_t> is false, then the expression stopped_as_error.transform_sender(sndr,env) is ill-formed; otherwise, it is equal to:
start_detached eagerly starts a sender without the caller needing to manage
-the lifetimes of any objects.
-
-
The name start_detached denotes a customization point object. For some
-subexpression sndr, let Sndr be decltype((sndr)). If sender_in<Sndr,empty_env> is false, start_detached is ill-formed.
-Otherwise, the expression start_detached(sndr) is expression-equivalent to:
Mandates: The type of the expression above is void.
-
-
If the expression above does not eagerly start the sender sndr after
-connecting it with a receiver that ignores value and stopped completion
-operations and calls terminate() on error completions, the behavior of
-calling start_detached(sndr) is undefined.
-
-
Let sndr be a subexpression such that Sndr is decltype((sndr)), and let detached-receiver and detached-operation be the following exposition-only
-class types:
If sender_to<Sndr,detached-receiver> is false, the
-expression start_detached.apply_sender(sndr) is ill-formed; otherwise, it is
-expression-equivalent to start(*newdetached-operation(sndr)).
-
-
11.9.11.2. this_thread::sync_wait[exec.sync.wait]
-
-
-
this_thread::sync_wait and this_thread::sync_wait_with_variant 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. sync_wait requires that the input sender has exactly one value completion signature.
-
-
For any receiver rcvr created by an implementation of sync_wait and sync_wait_with_variant, the expressions get_scheduler(get_env(rcvr)) and get_delegatee_scheduler(get_env(rcvr)) shall be well-formed. For a receiver
-created by the default implementation of this_thread::sync_wait, these
-expressions shall return a scheduler to the same thread-safe,
-first-in-first-out queue of work such that tasks scheduled to the queue
-execute on the thread of the caller of sync_wait. [Note: The
-scheduler for an instance of run_loop that is a local variable
-within sync_wait is one valid implementation. -- end note]
-
-
The templates sync-wait-type and sync-wait-with-variant-type are used to determine the
-return types of this_thread::sync_wait and this_thread::sync_wait_with_variant. Let sync-wait-env be the type of the expression get_env(rcvr) where rcvr is an instance of the
-receiver created by the default implementation of sync_wait.
The name this_thread::sync_wait denotes a customization point object. For
-some subexpression sndr, let Sndr be decltype((sndr)). If sender_in<Sndr,sync-wait-env> is false,
-or if the type completion_signatures_of_t<Sndr,sync-wait-env,type-list,type_identity_t> is ill-formed, this_thread::sync_wait(sndr) is ill-formed.
-Otherwise, this_thread::sync_wait(sndr) is expression-equivalent to:
Mandates: The type of the expression above is sync-wait-type<Sndr,sync-wait-env>.
-
-
-
Let sync-wait-receiver be a class type that satisfies receiver, let rcvr be an xvalue of that type,
-and let crcvr be a const lvalue referring to rcvr such that get_env(crcvr) has type sync-wait-env.
-If sender_in<Sndr,sync-wait-env> is false, or if the type completion_signatures_of_t<Sndr,sync-wait-env,type-list,type_identity_t> is ill-formed,
-the expression sync_wait_t().apply_sender(sndr) is ill-formed; otherwise it has the following effects:
-
-
-
Calls connect(sndr,rcvr), resulting in an operation state op_state, then calls start(op_state).
-
-
Blocks the current thread until a completion operation of rcvr is executed. When it is:
-
-
-
If set_value(rcvr,ts...) has been called, returns sync-wait-type<Sndr,sync-wait-env>{decayed-tuple<decltype(ts)...>{ts...}}. If that expression exits exceptionally, the exception is propagated to the caller of sync_wait.
-
-
If set_error(rcvr,err) has been called, let Err be the decayed type of err. If Err is exception_ptr, calls rethrow_exception(err). Otherwise, if the Err is error_code, throws system_error(err). Otherwise, throws err.
-
-
If set_stopped(rcvr) has been called, returns sync-wait-type<Sndr,sync-wait-env>{}.
-
-
-
-
The name this_thread::sync_wait_with_variant denotes a customization point
-object. For some subexpression sndr, let Sndr be the type of into_variant(sndr). If sender_in<Sndr,sync-wait-env> is false, this_thread::sync_wait_with_variant(sndr) is ill-formed. Otherwise, this_thread::sync_wait_with_variant(sndr) is expression-equivalent to:
execute creates fire-and-forget tasks on a specified scheduler.
-
-
The name execute denotes a customization point object. For some subexpressions sch and f, let Sch be decltype((sch)) and F be decltype((f)). If Sch does not satisfy scheduler or F does not satisfy invocable, execute(sch,f) is ill-formed. Otherwise, execute(sch,f) is expression-equivalent to:
Mandates: The type of the expression above is void.
-
-
-
For some subexpressions sndr and f where F is decltype((f)),
-if F does not satisfy invocable, the expression execute_t().apply_sender(sndr,f) is ill-formed; otherwise it is
-expression-equivalent to start_detached(then(sndr,f)).
-
-
11.11. Sender/receiver utilities [exec.utils]
-
-
-
This subclause makes use of the following exposition-only entities:
-
// [Editorial note: copy_cvref_t as in [[P1450R3]] -- end note]
-// Mandates: is_base_of_v<T, remove_reference_t<U>> is true
-template<classT,classU>
- copy_cvref_t<U&&,T>c-style-cast(U&&u)noexceptrequiresdecays-to<T,T>{
- return(copy_cvref_t<U&&,T>)std::forward<U>(u);
- }
-
-
-
- [Note: The C-style cast in
- c-style-cast
- is to disable accessibility checks. -- end note]
-
template<
- class-typeDerived,
- receiverBase=unspecified>// arguments are not associated entities ([lib.tmpl-heads])
- classreceiver_adaptor;
-
-
-
-
receiver_adaptor simplifies the implementation of one receiver type in terms of another. It defines tag_invoke overloads that forward to named members if they exist, and to the adapted receiver otherwise.
-
-
If Base is an alias for the unspecified default template argument, then:
-
-
-
Let HAS-BASE be false, and
-
-
Let GET-BASE(d) be d.base().
-
-
otherwise, let:
-
-
-
Let HAS-BASE be true, and
-
-
Let GET-BASE(d) be c-style-cast<receiver_adaptor<Derived,Base>>(d).base().
-
-
Let BASE-TYPE(D) be the type of GET-BASE(declval<D>()).
-
-
receiver_adaptor<Derived,Base> is equivalent to the following:
[Note:receiver_adaptor provides tag_invoke overloads on behalf of
-the derived class Derived, which is incomplete when receiver_adaptor is
-instantiated.]
Let SET-VALUE-MBR be the expression std::move(self).set_value(std::forward<As>(as)...).
-
-
Constraints: Either SET-VALUE-MBR is a valid expression or typenameDerived::set_value denotes a type and callable<set_value_t,BASE-TYPE(Derived),As...> is true.
-
-
Mandates:SET-VALUE-MBR, if that expression is valid, is not potentially-throwing.
-
-
Effects: Equivalent to:
-
-
-
If SET-VALUE-MBR is a valid expression, SET-VALUE-MBR;
Let SET-ERROR-MBR be the expression std::move(self).set_error(std::forward<Err>(err)).
-
-
Constraints: Either SET-ERROR-MBR is a valid expression or typenameDerived::set_error denotes a type and callable<set_error_t,BASE-TYPE(Derived),Err> is true.
-
-
Mandates:SET-ERROR-MBR, if that expression is valid, is not potentially-throwing.
-
-
Effects: Equivalent to:
-
-
-
If SET-ERROR-MBR is a valid expression, SET-ERROR-MBR;
Let SET-STOPPED-MBR be the expression std::move(self).set_stopped().
-
-
Constraints: Either SET-STOPPED-MBR is a valid expression or typenameDerived::set_stopped denotes a type and callable<set_stopped_t,BASE-TYPE(Derived)> is true.
-
-
Mandates:SET-STOPPED-MBR, if that expression is valid, is not potentially-throwing.
-
-
Effects: Equivalent to:
-
-
-
If SET-STOPPED-MBR is a valid expression, SET-STOPPED-MBR;
Constraints: Either self.get_env() is a valid expression or typenameDerived::get_env denotes a type and callable<get_env_t,BASE-TYPE(constDerived&)> is true.
-
-
Effects: Equivalent to:
-
-
-
If self.get_env() is a valid expression, self.get_env();
-
-
Otherwise, get_env(GET-BASE(self)).
-
-
-
Remarks: The expression in the noexcept clause is:
-
-
-
If self.get_env() is a valid expression, noexcept(self.get_env());
completion_signatures is a type that encodes a set of completion signatures
-([async.ops]).
-
-
[Example:
-
classmy_sender{
- usingcompletion_signatures=
- completion_signatures<
- set_value_t(),
- set_value_t(int,float),
- set_error_t(exception_ptr),
- set_error_t(error_code),
- set_stopped_t()>;
-};
-
-// Declares my_sender to be a sender that can complete by calling
-// one of the following for a receiver expression rcvr:
-// set_value(rcvr)
-// set_value(rcvr, int{...}, float{...})
-// set_error(rcvr, exception_ptr{...})
-// set_error(rcvr, error_code{...})
-// set_stopped(rcvr)
-
-
-- end example]
-
-
This subclause makes use of the following exposition-only entities:
Let Fns... be a template parameter pack of the arguments of the completion_signatures specialization named by Completions, let TagFns be a
-template parameter pack of the function types in Fns whose return types
-are Tag, and let Tsn be a template parameter
-pack of the function argument types in the n-th type
-in TagFns. Then, given two variadic templates Tuple and Variant, the type gather-signatures<Tag,Completions,Tuple,Variant> names the type META-APPLY(Variant,META-APPLY(Tuple,Ts0...),META-APPLY(Tuple,Ts1...),...META-APPLY(Tuple,Tsm-1...)), where m is the size of the parameter pack TagFns and META-APPLY(T,As...) is
-equivalent to:
transform_completion_signatures is an alias template used to transform one
-set of completion signatures into another. It takes a set of completion
-signatures and several other template arguments that apply modifications to
-each completion signature in the set to generate a new specialization of completion_signatures.
-
-
[Example:
-
// Given a sender Sndr and an environment Env, adapt the completion
-// signatures of Sndr by lvalue-ref qualifying the values, adding an additional
-// exception_ptr error completion if its not already there, and leaving the
-// other completion signatures alone.
-template<class...Args>
- usingmy_set_value_t=
- completion_signatures<
- set_value_t(add_lvalue_reference_t<Args>...)>;
-
-usingmy_completion_signatures=
- transform_completion_signatures<
- completion_signatures_of_t<Sndr,Env>,
- completion_signatures<set_error_t(exception_ptr)>,
- my_set_value_t>;
-
-
-- end example]
-
-
This subclause makes use of the following exposition-only entities:
SetValue shall name an alias template such that for any template
-parameter pack As..., the type SetValue<As...> is either ill-formed
-or else valid-completion-signatures<SetValue<As...>> is satisfied.
-
-
SetError shall name an alias template such that for any type Err, SetError<Err> is either ill-formed or else valid-completion-signatures<SetError<Err>> is satisfied.
-
-
Then:
-
-
-
Let Vs... be a pack of the types in the type-list named
-by gether-signatures<set_value_t,InputSignatures,SetValue,type-list>.
-
-
Let Es... be a pack of the types in the type-list named by gather-signatures<set_error_t,InputSignatures,type_identity_t,error-list>, where error-list is an
-alias template such that error-list<Ts...> names type-list<SetError<Ts>...>.
-
-
Let Ss name the type completion_signatures<> if gather-signatures<set_stopped_t,InputSignatures,type-list,type-list> is an alias for the type type-list<>; otherwise, SetStopped.
-
-
Then:
-
-
-
If any of the above types are ill-formed, then transform_completion_signatures<InputSignatures,AdditionalSignatures,SetValue,SetError,SetStopped> is ill-formed,
-
-
Otherwise, transform_completion_signatures<InputSignatures,AdditionalSignatures,SetValue,SetError,SetStopped> names the type completion_signatures<Sigs...> where Sigs... is the unique set of types in all the template arguments
-of all the completion_signatures specializations in [AdditionalSignatures,Vs...,Es...,Ss].
-
-
-
11.12. Execution contexts [exec.ctx]
-
-
-
This subclause specifies some execution resources on which work can be scheduled.
-
-
11.12.1. run_loop[exec.run.loop]
-
-
-
A run_loop is an execution resource on which work can be scheduled. It maintains a simple, thread-safe first-in-first-out queue of work. Its run() member function removes elements from the queue and executes them in a loop on whatever thread of execution calls run().
-
-
A run_loop instance has an associated count that corresponds to the number of work items that are in its queue. Additionally, a run_loop has an associated state that can be one of starting, running, or finishing.
-
-
Concurrent invocations of the member functions of run_loop, other than run and its destructor, do not introduce data races. The member functions pop_front, push_back, and finish execute atomically.
-
-
[Note: Implementations are encouraged to use an intrusive queue of operation states to hold the work units to make scheduling allocation-free. — end note]
-
classrun_loop{
- // [exec.run.loop.types] Associated types
- classrun-loop-scheduler;// exposition only
- classrun-loop-sender;// exposition only
- structrun-loop-opstate-base{// exposition only
- virtualvoidexecute()=0;
- run_loop*loop_;
- run-loop-opstate-base*next_;
- };
- template<receiver_of<completion_signatures<set_value_t()>>Rcvr>
- usingrun-loop-opstate=unspecified;// exposition only
-
- // [exec.run.loop.members] Member functions:
- run-loop-opstate-base*pop_front();// exposition only
- voidpush_back(run-loop-opstate-base*);// exposition only
-
- public:
- // [exec.run.loop.ctor] construct/copy/destroy
- run_loop()noexcept;
- run_loop(run_loop&&)=delete;
- ~run_loop();
-
- // [exec.run.loop.members] Member functions:
- run-loop-schedulerget_scheduler();
- voidrun();
- voidfinish();
-};
-
-
-
11.12.1.1. Associated types [exec.run.loop.types]
-
classrun-loop-scheduler;
-
-
-
-
run-loop-scheduler is an unspecified type that models the scheduler concept.
-
-
Instances of run-loop-scheduler remain valid until the end of the lifetime of the run_loop instance from which they were obtained.
-
-
Two instances of run-loop-scheduler compare equal if and only if they were obtained from the same run_loop instance.
-
-
Let sch be an expression of type run-loop-scheduler. The expression schedule(sch) is not potentially-throwing and has type run-loop-sender.
-
-
classrun-loop-sender;
-
-
-
-
run-loop-sender is an unspecified type such that sender-of<run-loop-sender> is true.
- Additionally, the types reported by its error_types associated type is exception_ptr, and the value of its sends_stopped trait is true.
-
-
An instance of run-loop-sender remains valid until the
- end of the lifetime of its associated run_loop instance.
-
-
Let sndr be an expression of type run-loop-sender, let rcvr be an
- expression such that decltype(rcvr) models the receiver_of concept, and let C be either set_value_t or set_stopped_t. Then:
-
-
-
The expression connect(sndr,rcvr) has type run-loop-opstate<decay_t<decltype(rcvr)>> and is potentially-throwing if and only if the initialiation of decay_t<decltype(rcvr)> from rcvr is potentially-throwing.
-
-
The expression get_completion_scheduler<C>(get_env(sndr)) is not potentially-throwing, has type run-loop-scheduler, and compares equal to the run-loop-scheduler instance from which sndr was obtained.
-
-
-
template<receiver_of<completion_signatures<set_value_t()>>Rcvr>// arguments are not associated entities ([lib.tmpl-heads])
- structrun-loop-opstate;
-
-
-
-
run-loop-opstate<Rcvr> inherits unambiguously from run-loop-opstate-base.
-
-
Let o be a non-const lvalue of type run-loop-opstate<Rcvr>, and let REC(o) be a non-const lvalue reference to an instance of type Rcvr that was initialized with the expression rcvr passed to the invocation of connect that returned o. Then:
-
-
-
The object to which REC(o) refers remains valid for the lifetime of the object to which o refers.
-
-
The type run-loop-opstate<Rcvr> overrides run-loop-opstate-base::execute() such that o.execute() is equivalent to the following:
as_awaitable transforms an object into one that is awaitable within a particular coroutine. This subclause makes use of the following exposition-only entities:
Alias template single-sender-value-type is defined as follows:
-
-
-
If value_types_of_t<Sndr,Env,Tuple,Variant> would have the form Variant<Tuple<T>>, then single-sender-value-type<Sndr,Env> is an alias for type decay_t<T>.
-
-
Otherwise, if value_types_of_t<Sndr,Env,Tuple,Variant> would have the form Variant<Tuple<>> or Variant<>, then single-sender-value-type<Sndr,Env> is an alias for type void.
-
-
Otherwise, if value_types_of_t<Sndr,Env,Tuple,Variant> would have the form Variant<Tuple<Ts...>> where Ts is a parameter pack, then single-sender-value-type<Sndr,Env> is an alias for type std::tuple<decay_t<Ts>...>.
-
-
Otherwise, single-sender-value-type<Sndr,Env> is ill-formed.
-
-
-
The type sender-awaitable<Sndr,Promise> is equivalent to the following:
Let rcvr be an rvalue expression of type awaitable-receiver, let crcvr be a const lvalue that refers to rcvr, let vs be a parameter pack of types Vs..., and let err be an arbitrary expression of type Err. Then:
-
-
-
If constructible_from<result_t,Vs...> is satisfied, the expression set_value(rcvr,vs...) is equivalent to:
err if decay_t<Err> names the same type as exception_ptr,
-
-
Otherwise, make_exception_ptr(system_error(err)) if decay_t<Err> names the same type as error_code,
-
-
Otherwise, make_exception_ptr(err).
-
-
-
The expression set_stopped(rcvr) is equivalent to static_cast<coroutine_handle<>>(rcvr.continuation_.promise().unhandled_stopped()).resume().
-
-
For any expression tag whose type satisfies forwarding-query and for any pack of subexpressions as, tag_invoke(tag,get_env(crcvr),as...) is expression-equivalent to tag(get_env(as_const(crcvr.continuation_.promise())),as...) when that expression is well-formed.
as_awaitable is a customization point object. For some subexpressions expr and p where p is an lvalue, Expr names the type decltype((expr)) and Promise names the type decltype((p)), as_awaitable(expr,p) is expression-equivalent to the following:
-
-
-
tag_invoke(as_awaitable,expr,p) if that expression is well-formed.
-
-
-
Mandates:is-awaitable<A,Promise> is true, where A is the type of the tag_invoke expression above.
-
-
-
Otherwise, expr if is-awaitable<Expr,U> is true, where U is an unspecified class type that
-lacks a member named await_transform. The
-condition is not is-awaitable<Expr,Promise> as that
-creates the potential for constraint recursion.
-
-
-
Preconditions:is-awaitable<Expr,Promise> is true and the expression co_awaitexpr in a coroutine with promise
-type U is expression-equivalent to the same
-expression in a coroutine with promise type Promise.
-
-
-
Otherwise, sender-awaitable{expr,p} if awaitable-sender<Expr,Promise> is true.
with_awaitable_senders, when used as the base class of a coroutine promise type, makes senders awaitable in that coroutine type.
-
In addition, it provides a default implementation of unhandled_stopped() such that if a sender completes by calling set_stopped, it is treated as if an uncatchable "stopped" exception were thrown from the await-expression. In practice, the coroutine is never resumed, and the unhandled_stopped of the coroutine caller’s promise type is called.
ISO/IEC JTC1/SC22/WG21 14882: Programming Language — C++
-
Audience:
-
LEWG
-
-
-
-
-
-
-
-
1. Introduction
-
This paper proposes a refactorization of the receiver concepts of [P2300R4] to
-address concerns raised by LEWG during its design review related to the
-requirement of an error channel that accepts exception_ptr. The change to receiver_of proposed herein enables a corresponding change to the sender_to concept that strengthens type checking and removes some need to constrain
-customizations of the connect customization point.
-
1.1. Motivation
-
In [P2300R4], the receiver concepts are currently expressed as follows:
During the design review of P2300, LEWG raised the following concerns about the
-form of these concepts:
-
-
-
Since set_value is permitted to be potentially throwing, and since the
-receiver type is not known when a sender is asked to compute its completion
-signatures, most senders will need to pessimistically report that they can
-complete exceptionally, when that may in fact not be true. This may cause the
-instantiation of expensive error handling code that is effectively dead.
-
-
No receiver R can satisfy the receiver<R> or receiver_of<R,As...> concepts without providing an error channel for exception_ptr. This has the
-following problems:
-
-
-
exception_ptr is a relatively heavy-weight error type, not unlike a shared_ptr. Requiring the presence of this channel is likely to cause
-needless code generation.
-
-
It makes it questionable whether any of P2300 can be reasonably expected
-to work in freestanding environments, which often lack exception
-handling support.
-
-
-
Although the design of P2300 is sound, LEWG nevertheless wanted an investigation
-into these issues and a recommendation to be made.
-
This paper makes a recommendation to change the receiver concepts to address
-these concerns.
-
1.2. Design Summary
-
This paper proposes to make the following changes, summarized here without
-commentary. Commentary is provided below.
-
-
-
Remove the default implementation of the get_env receiver query.
-
-
The receiver_of concept takes a receiver and an instance of the completion_signatures<> class template.
-
-
A receiver’s customization of set_value is required to be noexcept.
-
-
The sender_to<Sndr,Rcvr> concept requires Rcvr to accept all of Sndr's
-completions.
-
-
connect(sndr,rcvr) also requires rcvr to accept all of sndr's completions.
-
-
get_completion_signatures is required to return an instantiation of the completion_signatures class template; the value_types_of_t and error_types_of_t template aliases remain unchanged.
-
-
The make_completion_signatures design is slightly tweaked to be more general.
-
-
1.3. Design Rationale
-
The author believes these are all reasonable adjustments to the design of P2300,
-but one may wonder why they were not considered before now.
-
The fourth revision of P2300 brought with it some notable changes, the two most
-significant of which are:
-
-
-
Support for dependently-typed senders, where a sender’s completions can
-depend on information that isn’t known independently of the execution
-environment within which the sender will be initiated. For instance,
-a get_scheduler() sender which queries the receiver for the current
-scheduler and then sends it through the value channel, cannot possibly
-know the type of the scheduler it will send until it has been connected
-to a receiver.
-
-
Dropping of support for "untyped" senders, which do not declare their
-completion signatures. Untyped senders were supported because of the lack
-of dependently-typed senders, which ceased to be an issue with R4. At the
-direction of LEWG, "untyped" senders were dropped, greatly simplifying
-the design.
-
-
Taken together, these two changes open up a huge piece of the design space. The
-implication is that a sender is always able to provide its completion
-signatures. This is new, and P2300R4 is not taking advantage of this extra
-type information.
-
The author realized that the extra type information can be leveraged to
-accommodate LEWGs requests regarding the receiver interface, while at the same
-time simplifying uses of std::execution by permitting the library to take on
-more of the type checking burden.
-
The sender_to concept, which checks whether a sender and a receiver can be
-connected, now has perfect information: it can ask the receiver for the execution
-environment; it can ask the sender how it will complete when initiated in that
-environment; and it can ask the receiver if it is capable of receiving all of
-the sender’s possible completions. This was not possible before R4.
-
Below we look at each of the changes suggested in the summary and explain its
-rationale in light of the extra information now available to the type system.
-
2. Design Details
-
2.1. Remove the default implementation of the get_env receiver query.
-
The presence of a customization of get_env becomes the distinguishing feature
-of receivers. A "receiver" no longer needs to provide any completion channels at
-all to be considered a receiver, only get_env.
-
2.2. The receiver_of concept takes a receiver and an instance of the completion_signatures<> class template.
-
The receiver_of concept, rather than accepting a receiver and some value
-types, is changed to take a receiver and an instance of the completion_signatures<> class template. A sender uses completion_signatures<> to describe the signals with which it completes. The receiver_of concept ensures that a particular receiver is capable of receiving
-those signals.
-
Notably, if a sender only sends a value (i.e., can never send an error or a
-stopped signal), then a receiver need only provide a value channel to be
-compatible with it.
-
2.3. A receiver’s customization of set_value is required to be noexcept.
-
This makes it possible for many senders to become "no-fail"; that is, they
-cannot complete with an error. just(1), for instance, will only ever
-successfully send an integer through the value channel. An adaptor such as then(sndr,fun) can check whether fun can ever exit exceptionally when
-called with all the sets of values that sndr may complete with. If so, the then sender must add set_error_t(exception_ptr) to its list of completions.
-Otherwise, it need not.
-
2.4. The sender_to<Sndr,Rcvr> concept requires Rcvr to accept all of Sndr's completions.
-
The sender_to concept, which checks whether a sender and a receiver can be
-connected, now enforces that the sender’s completion signatures can in fact be
-handled by the receiver. Previously, it only checked that connect(sndr,rcvr) was well-formed, relying on sender authors to properly constrain their connect customizations.
-
2.5. connect(sndr,rcvr) also requires rcvr to accept all of sndr's completions.
-
For good measure, the connect customization point also checks whether a
-receiver can receive all of the sender’s possible completions before trying to
-dispatch via tag_invoke to a connect customization. This often entirely
-frees sender authors from having to constrain their connect customizations at
-all. It is enough to customize get_completion_signatures, and the type
-checking is done automatically.
-
Strictly speaking, with this change, the change to sender_to is unnecessary.
-The change to sender_to results in better diagnostics, in the author’s
-experience.
-
2.6. get_completion_signatures is required to return an instantiation of the completion_signatures class template.
-
get_completion_signatures was added in R4 in response to feedback that
-authoring sender traits was too difficult/arcane. Rather than defining a struct
-with templatetemplate aliases, a user can simply declare a sender’s
-completions as:
In R4, completion_signatures generated the templatetemplate aliases for
-you. The proposed change is to take it further and requireget_completion_signatures to return an instance of the completion_signatures class template. With this change, the last vestige of the old sender traits
-design with its unloved temlatetemplate alias interface is swept away. completion_signatures entirely replaces sender traits, further simplifying the
-design.
-
The sender concept enforces the new requirement.
-
2.7. The value_types_of_t and error_types_of_t template aliases remain.
-
It can still be helpful sometimes to consume the old templatetemplate, say,
-for generating a variant of the tuples of all the sets of a sender’s value
-types. For that reason, the alias templates value_types_of_t and error_types_of_t retain the same interface and semantic as before. For
-instance, generating the variant of tuples of value types, you would use the
-following:
Additionally, these two alias joined by a sends_stopped<Sndr,Env> Boolean
-variable template to complete the set.
-
2.8. The make_completion_signatures design is slightly tweaked to be more general.
-
In the proposed design, completion_signatures plays a much larger role.
-Accordingly, the job of specifying the completion signatures of custom sender
-adaptors also becomes more important, necessitating better tools. The make_completion_signatures, new to R4, narrowly misses being that better tool.
-
In R4, make_completion_signatures has the following interface:
In the R4 design, SetValue and SetError are alias templates, instantiations
-of which are required to name function types whose return types are excecution::set_value_t and execution::set_error_t, respectively. This is
-overly-restrictive. The problems with it are:
-
-
-
It is not possible to map one kind of completion into a different kind. For
-instance, the upon_error(sndr,fun) maps error completions into value
-completions.
-
-
It is not possible to map a single completion signature into multiple
-different completions. For instance, the let_value(sndr,fun) sender
-adaptor needs to map a set of sndr's value types into the set of
-completions of whatever sender that is returned from fun(values...), which
-is likely more than one.
-
-
In addition, the final Boolean SendsStopped parameter merely controls whether
-or not the completion execution::set_stopped_t() should be added to the
-resulting list of completion signatures. This doesn’t help a sender adaptor
-such as let_stopped(sndr,fun), which needs to transform a stopped signal
-into the set of completions of the sender that fun() returns.
-
This design proposes to change the three final template arguments as follows:
-
-
-
template<class...>classSetValue: Instantiations of this alias
-template must name an instantiation of the completion_signatures class
-template.
-
-
template<class>classSetError: Instantiations of this alias
-template must name an instantiation of the completion_signatures class
-template.
-
-
classSetStopped: Must name an instantiation of the completion_signatures class template. If the sender Sndr can complete
-with set_stopped, then these signatures are included in the resulting list
-of completions. Otherwise, this template parameter is ignored.
-
-
The semantics of make_completion_signatures is likewise simplified: The three
-template arguments, SetValue, SetError, and SetStopped, are used to map
-each of a sender’s completions into a list of completions which are all
-concatenated together, along with any additional signatures specified by the OtherSigs list, and made unique.
-
3. Considerations
-
3.1. Implications of noexceptset_value
-
The role of execution::set_value is to execute a continuation on the success
-of the predecessor. A continuation is arbitrary code, and surely arbitrary code
-can exit exceptionally, so how can we require execution::set_value to be noexcept?
-
The answer has two parts:
-
-
-
execution::set_value always has the option of accepting arguments by
-forwarding reference and executing any potentially throwing operations
-within a try/catch block, routing any exceptions to set_error(std::exception_ptr).
-
-
A sender knows what types it will send and with what value category. The sender_to concept checks that none of the set_value expression(s) it
-will execute are potentially throwing. This doesn’t necessitate that all
-receivers accept all arguments by forwarding reference, however. For
-instance, if a sender knows it will pass an rvalue std::string to the
-receiver’s set_value, and if the sender is connected to a receiver whose set_value takes a std::string by value, that will type-check. The sender_to concept will essentially be enforcing this constraint:
Since std::string's move constructor is noexcept, this constraint
- is satisfied regardless of whether rcvr's set_value customization
- accepts the string by value or by reference.
-
-
3.2. Diagnostics
-
On the whole, the authors of P2300 feel that this design change is the right one
-to make to meet LEWG’s requirements. It comes with one drawback, however: The
-satisfaction checking of the receiver_of concept, which must now check against
-a set of signatures specified in a type-list, now requires metaprogramming in
-addition to requires clauses. As a result, diagnostics can suffer.
-
During the implementation experience, the author was able to surface a
-relatively suscinct and accurate error for, say, the lack of a particular
-completion channel on a receiver, by employing several tricks. While regrettable
-that such tricks are required, we do not feel that the issue of mediocre
-diagnostics is dire enough to offset the many advantages of the design presented
-here.
-
In addition, the author has discovered a way that an implementation may choose
-to extend the connect customization point in a way that permits users to
-bypass the constraint checking entirely, thus generating a deep instantiation
-backtrace that often greatly assists the debugging of custom
-sender/receiver-based algorithms. This mechanism can be enshrined in the standard
-as "recommended practice."
-
4. Open questions
-
4.1. Weasel wording for -fno-exceptions
-
We may need to add some weasel wording to the effect that:
-
-
... if an implementation is able to deduce that all of its operations are not
-potentially throwing, a conforming implementation of the algorithms in
-<section> may omit set_error_t(exception_ptr) from any sender’s list of
-completion signatures.
-
-
If an implementation doesn’t support exceptions, e.g., if the user is compiling
-with -fno-exceptions, it can safely assume that an expression expr is not
-going to exit exceptionally regardless of the value of noexcept(expr). An
-implementation shouldn’t be required to report that it can complete with an
-exception in that case.
-
4.2. Error channel of allocating algorithms
-
An interesting question is what to do on freestanding implementations for those
-algorithms that necessarily must allocate. Those algorithms, as P2300 stands
-today, will always have a set_error_t(exception_ptr) completion signature. The
-possibilities I see are:
-
-
-
Permit implementations to omit the exceptional completion signature when it
-knows allocations can’t fail with an exception (see above),
-
-
Replace the exceptional completion signature with set_error_t(std::error_code), and call the receiver with std::make_error_code(std::errc::not_enough_memory) on allocation failure.
-
-
Replace the exceptional completion signature with set_error_t(std::bad_alloc); that is, pass an instance of the std::bad_alloc exception type through the error channel by value. (From
-what the author can infer, freestanding implementations are required to
-provide the std::bad_alloc type even when actually throwing exceptions is
-not supported.)
-
-
5. Implementation experience
-
The design described above has been implemented in a branch of the reference
-implementation which can be found in the following GitHub pull request:
-https://github.com/brycelelbach/wg21_p2300_std_execution/pull/410.
-
The change, while somewhat disruptive to the reference implementation itself,
-had the benefits described above; namely:
-
-
-
Stricter type-checking "for free". Sender authors need only report the
-completion signatures, and the concepts and customization points of the
-library do all the heavy lifting to make sure the capabilities of receivers
-match the requirements of the senders.
-
-
More "no-fail" senders. Many fewer of the senders need an error channel at
-all, and the ones that do generally need it only conditionally, when working
-with potentially-thrwoing callables or types whose special operations can
-throw. Only those few senders that must dynamically allocate state necessarily
-need a set_error_t(exception_ptr) channel, and we may even choose to change
-those to use something like set_error_t(bad_alloc) instead.
-
-
No required set_error_t(exception_ptr) or set_stopped_t() channels at all.
-
-
In addition, in the author’s opinion, the reference implementation got
-significantly simpler for the change, and the pull request removes more lines
-than it adds, while adding functionality at the same time.
In [exec.recv], replace paragraphs 1 and 2 with the following:
-
-
-
-
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, set_error, and set_stopped. These are
-collectively known as a receiver’s completion-signal operations.
-
-
The receiver concept defines the requirements for a receiver type with an
-unknown set of completion signatures. The receiver_of concept defines the
-requirements for a receiver type with a known set of completion signatures.
execution::set_value is used to send a value completion signal to a receiver.
-
-
The name execution::set_value denotes a customization point object. The
-expression execution::set_value(R,Vs...) for some subexpressions R and Vs... is expression-equivalent to:
-
-
-
tag_invoke(execution::set_value,R,Vs...), if that expression is
-valid. If the function selected by tag_invoke does not send the
-value(s) Vs... to the receiver R’s value channel, the behavior of
-calling execution::set_value(R,Vs...) is undefined.
-
-
-
-
Mandates: The tag_invoke expression above is not potentially
-throwing.
-
-
-
-
Otherwise, execution::set_value(R,Vs...) is ill-formed.
-
-
-
6.5. Senders
-
Change [exec.snd] as follows:
-
-
-
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 sender concept defines the requirements for a sender type. The sender_to concept defines the requirements for a sender type capable of being connected with a specific receiver type.
-
template<template<template<class...>class,template<class...>class>class>
- structhas-value-types;// exposition only
-
-template<template<template<class...>class>class>
- structhas-error-types;// exposition only
-
-template<classS>
- concepthas-sender-types=// exposition only
- requires{
- typenamehas-value-types<S::templatevalue_types>;
- typenamehas-error-types<S::templateerror_types>;
- typenamebool_constant<S::sends_stopped>;
- };
execution::get_completion_signatures is a customization point object. Let s be an expression such that decltype((s)) is S, and let e be an
-expression such that decltype((e)) is E. Then get_completion_signatures(s) is expression-equivalent to get_completion_signatures(s,no_env{}) and get_completion_signatures(s,e) is expression-equivalent to:
-
-
-
tag_invoke_result_t<get_completion_signatures_t,S,E>{} if that expression is well-formed,
-
-
-
-
Mandates:is-instance-of<Sigs,completion_signatures> or is-instance-of<Sigs,dependent_completion_signatures>, where Sigs names the type tag_invoke_result_t<get_completion_signatures_t,S,E>.
-
-
-
-
- Otherwise, if remove_cvref_t<S>::completion_signatures is well-formed
-and names a type, then a
- value-initialized
- prvalue of
- type
- remove_cvref_t<S>::completion_signatures,
-
-
-
-
-
Mandates:is-instance-of<Sigs,completion_signatures> or is-instance-of<Sigs,dependent_completion_signatures>, where Sigs names the type remove_cvref_t<S>::completion_signatures.
-
-
-
-
Otherwise, [...]
-
-
-
6.7. dependent_completion_signatures
-
Change [exec.depsndtraits] as follows:
-
template<classE>// arguments are not associated entities ([lib.tmpl-heads])
- structdependent_completion_signatures{};
-
-
-
-
- dependent_completion_signatures is a placeholder completion signatures
-descriptor that can be
- used
- returned from get_completion_signatures
- to report
-that a type might be a sender within a particular execution environment, but
-it isn’t a sender in an arbitrary execution environment.
-
-
-
-
-
-
If decay_t<E> is no_env, dependent_completion_signatures<E> is equivalent to:
Otherwise, dependent_completion_signatures<E> is an empty struct.
-
-
-
-
-
-
When used as the return type of a customization of get_completion_signatures, the template argument E shall be the
-unqualified type of the second argument.
-
-
-
-
6.8. execution::connect
-
Change [exec.connect]/p2 as follows:
-
-
-
The name execution::connect denotes a customization point object. For some subexpressions s and r, let S be decltype((s)) and R be decltype((r)), and let S' and R' be the decayed types of S and R, respectively. If R does not satisfy execution::receiver, execution::connect(s,r) is ill-formed. Otherwise, the expression execution::connect(s,r) is expression-equivalent to:
-
-
-
- tag_invoke(execution::connect,s,r), if
- that expression is valid and S satisfies execution::sender
- the constraints below are satisfied
- . If the function selected by tag_invoke does not return an operation state for which execution::start starts work described by s, the behavior of calling execution::connect(s,r) is undefined.
-
Mandates: The type of the tag_invoke expression above satisfies operation_state.
-
-
-
Otherwise, connect-awaitable(s,r) if [...]
-
[...]
-
The operand of the requires-clause of connect-awaitable is equivalent to receiver_of<R> if await-result-type<S,connect-awaitable-promise> is cvvoid; otherwise, it is receiver_of<R,await-result-type<S,connect-awaitable-promise>>.
-
- Let Res be await-result-type<S,connect-awaitable-promise>, and let Vs... be an empty parameter pack if Res is cvvoid, or a pack containing the single type Res otherwise. The operand of the requires-clause of connect-awaitable is equivalent to receiver_of<R,Sigs> where Sigs names the type:
-
Replace [exec.schedule_from]/3.3, which begins with "Given an expression e, let E be decltype((e))," with the following:
-
-
-
-
Given subexpressions s2 and e, where s2 is a sender returned from schedule_from or a copy of such, let S2 be decltype((s2)) and let E be decltype((e)). Then the type of tag_invoke(get_completion_signatures,s2,e) shall be:
-
make_completion_signatures<
- copy_cvref_t<S2,S>,
- E,
- make_completion_signatures<
- schedule_result_t<Sch>,
- E,
- completion_signatures<set_error_t(exception_ptr)>,
- no-value-completions>>;
-
-
where no-value-completions<As...> names the type completion_signatures<> for any set of types As....
-
-
-
6.14. execution::then
-
Replace [exec.then]/p2.3.3, which begins with "Given an expression e, let E be decltype((e))," with the following:
-
-
-
-
Let compl-sig-t<Tag,Args...> name the type Tag() if Args... is a template paramter pack containing the
-single type void; otherwise, Tag(Args...). Given
-subexpressions s2 and e where s2 is a sender returned from then or a copy of such, let S2 be decltype((s2)) and let E be decltype((e)). The type of tag_invoke(get_completion_signatures,s2,e) shall be equivalent
-to:
and set-error-signature is an alias for completion_signatures<set_error_t(exception_ptr)> if any of the types
-in the type-list named by value_types_of_t<copy_cvref_t<S2,S>,E,potentially-throwing,type-list> are true_type; otherwise, completion_signatures<>, where potentially-throwing is the template alias:
Replace [exec.upon_error]/p2.3.3, which begins with "Given an expression e, let E be decltype((e))," with the following:
-
-
-
-
Let compl-sig-t<Tag,Args...> name the type Tag() if Args... is a template paramter pack containing the
-single type void; otherwise, Tag(Args...). Given
-subexpressions s2 and e where s2 is a sender returned from upon_error or a copy of such, let S2 be decltype((s2)) and let E be decltype((e)). The type of tag_invoke(get_completion_signatures,s2,e) shall be equivalent
-to:
and set-error-signature is an alias for completion_signatures<set_error_t(exception_ptr)> if any of the types
-in the type-list named by error_types_of_t<copy_cvref_t<S2,S>,E,potentially-throwing> are true_type; otherwise, completion_signatures<>, where potentially-throwing is the template alias:
Replace [exec.upon_stopped]/p2.3.3, which begins "Given some expression e, let E be decltype((e))," with the following:
-
-
-
-
Let compl-sig-t<Tag,Args...> name the type Tag() if Args... is a template paramter pack containing the
-single type void; otherwise, Tag(Args...). Given
-subexpressions s2 and e where s2 is a sender returned from upon_stopped or a copy of such, let S2 be decltype((s2)) and let E be decltype((e)). The type of tag_invoke(get_completion_signatures,s2,e) shall be equivalent
-to:
where set-stopped-completions names the type completion_signatures<compl-sig-t<set_value_t,invoke_result_t<F>>, and set-error-signature names the type completion_signatures<set_error_t(exception_ptr)> if is_nothrow_invocable_v<F> is true, or completion_signatures<> otherwise.
-
-
-
6.17. execution::bulk
-
Replace [exec.bulk]/p2.4, which begins, "Given an expression e, let E be decltype((e))," with the following:
-
-
-
-
Given subexpressions s2 and e where s2 is a sender returned
-from bulk or a copy of such, let S2 be decltype((s2)) and let E be decltype((e)). The type of tag_invoke(get_completion_signatures,s2,e) shall be equivalent
-to:
Replace [exec.split]/p3.4, which begins, "Given an expression e, let E be decltype((e))," with the following:
-
-
-
-
Given subexpressions s2 and e where s2 is a sender returned
-from split or a copy of such, let S2 be decltype((s2)) and let E be decltype((e)). The type of tag_invoke(get_completion_signatures,s2,e) shall be equivalent
-to:
Replace [exec.when_all]/p2.2.5, which begins, "Given some expression e, let E be decltype((e))," with the following:
-
-
-
-
Given subexpressions s2 and e where s2 is a sender returned
-from when_all or a copy of such, let S2 be decltype((s2)), let E be decltype((e)), and let Ss... be the decayed types of the
-arguments to the when_all expression that created s2. If the
-decayed type of e is no_env, let WE be no_env; otherwise,
-let WE be a type such that stop_token_of_t<WE> is in_place_stop_token and tag_invoke_result_t<Tag,WE,As...> names the type, if any, of call-result-t<Tag,E,As...> for all types As... and all types Tag besides get_stop_token_t. The type of tag_invoke(get_completion_signatures,s2,e) shall be as follows:
-
-
-
For each type Si in Ss..., let S'i name the type copy_cvref_t<S2,Si>. If for
-any type S'i, the type completion_signatures_of_t<S'i,WE> names a type other than an instantiation of completion_signatures, the type of tag_invoke(get_completion_signatures,s2,e) shall be dependent_completion_signatures<E>.
-
-
Otherwise, for each type S'i, let Sigsi... be the set of template
-arguments in the instantiation of completion_signatures named
-by completion_signatures_of_t<S'i,WE>, and let Ci be the
-count of function types in Sigsi... for which the return
-type is set_value_t. If any Ci is two or greater, then the
-type of tag_invoke(get_completion_signatures,s2,e) shall be dependent_completion_signatures<E>.
-
-
Otherwise, let Sigs2i... be the set of
-function types in Sigsi... whose
-return types are notset_value_t, and let Ws... be
-the unique set of types in [Sigs20...,Sigs21...,...Sigs2n-1...,set_error_t(exception_ptr),set_stopped_t()], where n is sizeof...(Ss). If any Ci is 0, then the type of tag_invoke(get_completion_signatures,s2,e) shall be completion_signatures<Ws...>.
-
-
Otherwise, let Vi... be the function
-argument types of the single type in Sigsi... for which the return type is set_value_t. Then the type of tag_invoke(get_completion_signatures,s2,e) shall be completion_signatures<Ws...,set_value_t(V0...,V1...,...Vn-1...)>.
-
-
-
-
6.20. execution::ensure_started
-
Replace [exec.ensure_started]/p2.4 which begins, "Given an expression e, let E be decltype((e))," with the following:
-
-
-
-
Given subexpressions s2 and e where s2 is a sender returned
-from ensure_started or a copy of such, let S2 be decltype((s2)) and let E be decltype((e)). The type of tag_invoke(get_completion_signatures,s2,e) shall be equivalent
-to:
- Constructs a receiver r
- Let R be the type of a receiver, let r be an rvalue of type R, and let cr be a
-lvalue reference to constR such that
- :
-
-
- When set_value(r,ts...) is called, it does nothing.
- The expression set_value(r) is not potentially throwing and has no effect,
-
- When set_error(r,e) is called, it calls std::terminate.
- For any subexpression e, the expression set_error(r,e) is expression-equivalent
-to terminate(),
-
- When set_stopped(r) is called, it does nothing.
- The expression set_stopped(r) is not potentially throwing and has no effect, and
-
-
-
-
-
The expression get_env(cr) is expression-equivalent to empty-env{}.
-
-
-
-
Calls execution::connect(s,r), resulting in an operation state op_state, then calls execution::start(op_state). The lifetime of op_state lasts until one of the receiver completion-signals of r is called.
-
-
-
6.22. this_thread::sync_wait
-
Change [exec.sync_wait]/p4.3.3.1 as follows:
-
-
-
- If execution::set_value(r,ts...) has been called, returns sync-wait-type<S,sync-wait-env>{decayed-tuple<decltype(ts)...>{ts...}}.
- If that expression exits exceptionally, the exception is propagated to the caller of sync_wait.
-
-
-
6.23. execution::receiver_adaptor
-
Remove [exec.utils.rcvr_adptr]/p2, which begins, "This section makes use of the following exposition-only entities," and renumber all subsequent paragraphs.
-
Change [exec.utils.rcvr_adptr]/p4-6 (now p3-5) as follows:
-
-
-
receiver_adaptor<Derived,Base> is equivalent to the following:
[Note:receiver_adaptor provides tag_invoke overloads on behalf of
-the derived class Derived, which is incomplete when receiver_adaptor is
-instantiated.]
Let SET-VALUE be the expression std::move(self).set_value(std::forward<As>(as)...).
-
-
Constraints: Either SET-VALUE is a valid expression or typenameDerived::set_value denotes a type and callable<set_value_t,BASE-TYPE(Derived),As...> is true.
-
-
Mandates:SET-VALUE, if that expression is valid, is not potentially throwing.
Let SET-ERROR be the expression std::move(self).set_error(std::forward<E>(e)).
-
-
Constraints: Either SET-ERROR is a valid expression or typenameDerived::set_error denotes a type and callable<set_error_t,BASE-TYPE(Derived),E> is true.
-
-
Mandates:SET-ERROR, if that expression is valid, is not potentially throwing.
Let SET-STOPPED be the expression std::move(self).set_stopped().
-
-
Constraints: Either SET-STOPPED is a valid expression or typenameDerived::set_stopped denotes a type and callable<set_stopped_t,BASE-TYPE(Derived)> is true.
-
-
Mandates:SET-STOPPED, if that expression is valid, is not potentially throwing.
-
-
Effects: Equivalent to:
-
-
-
If SET-STOPPED is a valid expression, SET-STOPPED;
Constraints: Either self.get_env() is a valid expression or typenameDerived::get_env denotes a type and callable<get_env_t,BASE-TYPE(constDerived&)> is true.
-
-
Effects: Equivalent to:
-
-
-
If self.get_env() is a valid expression, self.get_env();
-
-
Otherwise, execution::get_env(GET-BASE(self)).
-
-
-
Remarks: The expression in the noexcept clause is:
-
-
-
If self.get_env() is a valid expression, noexcept(self.get_env());
- completion_signatures is used to define a type that implements the nested value_types, error_types, and sends_stopped members that describe the
-ways a sender completes. Its arguments are a flat list of function types
-that describe the signatures of the receiver’s completion-signal operations
-that the sender invokes.
-
- completion_signatures is used to describe the completion signals of a receiver that
-a sender may invoke. Its template argument list is a list of function types corresponding
-to the signatures of the receiver’s completion signals.
-
-
[Example:
-
classmy_sender{
- usingcompletion_signatures=
- execution::completion_signatures<
- execution::set_value_t(),
- execution::set_value_t(int,float),
- execution::set_error_t(exception_ptr),
- execution::set_error_t(error_code),
- execution::set_stopped_t()>;
-};
-
-// completion_signatures_of_t<my_sender>
-// ::value_types<tuple, variant> names the type:
-// variant<tuple<>, tuple<int, float>>
-//
-// completion_signatures_of_t<my_sender>
-// ::error_types<variant> names the type:
-// variant<exception_ptr, error_code>
-//
-// completion_signatures_of_t<my_sender>::sends_stopped is true
-// Declares my_sender to be a sender that can complete by calling
-// one of the following for a receiver expression R:
-// execution::set_value(R)
-// execution::set_value(R, int{...}, float{...})
-// execution::set_error(R, exception_ptr{...})
-// execution::set_error(R, error_code{...})
-// execution::set_stopped(R)
-
-
-- end example]
-
-
This section makes use of the following exposition-only concept:
Let ValueFns be a template parameter pack of the function types in Fns whose return types are execution::set_value_t, and let Valuesn be a template parameter pack of the function argument types in the n-th type in ValueFns. Then, given two variadic templates Tuple and Variant, the type completion_signatures<Fns...>::value_types<Tuple,Variant> names the type Variant<Tuple<Values0...>,Tuple<Values1...>,...Tuple<Valuesm-1...>>, where m is the size of the parameter pack ValueFns.
-
-
Let ErrorFns be a template parameter pack of the function types in Fns whose return types are execution::set_error_t, and let Errorn be the function argument type in the n-th type in ErrorFns. Then, given a variadic template Variant, the type completion_signatures<Fns...>::error_types<Variant> names the type Variant<Error0,Error1,...Errorm-1>, where m is the size of the parameter pack ErrorFns.
-
-
completion_signatures<Fns...>::sends_stopped is true if at least one of the types in Fns is execution::set_stopped_t(); otherwise, false.
Let Fns... be a template parameter pack of the arguments of the completion_signatures instantiation named by completion_signatures_of_t<S,E>, let ValueFns be a
-template parameter pack of the function types in Fns whose return types
-are execution::set_value_t, and let Valuesn be a template parameter
-pack of the function argument types in the n-th type
-in ValueFns. Then, given two variadic templates Tuple and Variant, the type value_types_of_t<S,E,Tuple,Variant> names the type Variant<Tuple<Values0...>,Tuple<Values1...>,...Tuple<Valuesm-1...>>, where m is the size of the parameter pack ValueFns.
Let Fns... be a template parameter pack of the arguments of the completion_signatures instantiation named by completion_signatures_of_t<S,E>, let ErrorFns be a
-template parameter pack of the function types in Fns whose return types
-are execution::set_error_t, and let Errorn be the function argument
-type in the n-th type in ErrorFns. Then, given a variadic template Variant, the type error_types_of_t<S,E,Variant> names the type Variant<Error0,Error1,...Errorm-1>, where m is
-the size of the parameter pack ErrorFns.
Let Fns... be a template parameter pack of the arguments of the completion_signatures instantiation named by completion_signatures_of_t<S,E>. sends_stopped<S,E> is true if at
-least one of the types in Fns is execution::set_stopped_t();
-otherwise, false.
-
-
-
-
6.25. execution::make_completion_signatures
-
Change [exec.utils.mkcmplsigs] as follows:
-
-
-
make_completion_signatures is an alias template used to adapt the
-completion signatures of a sender. It takes a sender, and environment, and
-several other template arguments that apply modifications to the sender’s
-completion signatures to generate a new instantiation of execution::completion_signatures.
-
-
[Example:
-
// Given a sender S and an environment Env, adapt a S’s completion
-// signatures by lvalue-ref qualifying the values, adding an additional
-// exception_ptr error completion if its not already there, and leaving the
-// other signals alone.
-template<class...Args>
- usingmy_set_value_t=
- execution::completion_signatures<
- execution::set_value_t(add_lvalue_reference_t<Args>...)>;
-
-usingmy_completion_signals=
- execution::make_completion_signatures<
- S,Env,
- execution::completion_signatures<execution::set_error_t(exception_ptr)>,
- my_set_value_t>;
-
-
-- end example]
-
-
This section makes use of the following exposition-only entities:
- AddlSigs shall name an instantiation of the execution::completion_signatures class template.
-
-
- SetValue shall name an alias template such that for any template
- parameter pack As..., the type SetValue<As...> is either ill-formed
- , void or an
-alias for a function type whose return type is execution::set_value_t
- or else valid-completion-signatures<SetValue<As...>,E> is satisfied
- .
-
-
-
- SetError shall name an alias template such that for any type Err, SetError<Err> is either ill-formed
- , void or an alias for a function
-type whose return type is execution::set_error_t
- or else valid-completion-signatures<SetError<Err>,E> is satisfied
- .
-
-
- Then:
-
-
-
- Let Vs... be a pack of the
- non-void
- types in the type-list named
-by value_types_of_t<Sndr,Env,SetValue,type-list>.
-
-
-
- Let Es... be a pack of the
- non-void
- types in the type-list named by error_types_of_t<Sndr,Env,error-list>, where error-list is an
-alias template such that error-list<Ts...> names type-list<SetError<Ts>...>.
-
-
-
- Let Ss
- be an empty pack if SendsStopped is false; otherwise, a
-pack containing the single type execution::set_stopped_t()
- name the type completion_signatures<> if sends_stopped<Sndr,Env> is false; otherwise, SetStopped
- .
-
-
- Then:
-
-
-
-
Let MoreSigs... be a pack of the template arguments of the execution::completion_signatures instantiation named by AddlSigs.
-
-
If any of the above types are ill-formed, then make_completion_signatures<Sndr,Env,AddlSigs,SetValue,SetDone,SendsStopped> is an alias for dependent_completion_signatures<Env>.
-
-
Otherwise, make_completion_signatures<Sndr,Env,AddlSigs,SetValue,SetDone,SendsStopped> names the type completion_signatures<Sigs...> where Sigs... is the unique set of types in [Vs...,Es...,Ss...,MoreSigs...].
-
-
-
-
-
-
If any of the above types are ill-formed, then make_completion_signatures<Sndr,Env,AddlSigs,SetValue,SetError,SetStopped> is ill-formed,
-
-
Otherwise, if any type in [AddlSigs,Vs...,Es...,Ss] is not an
-instantiation of completion_signatures, then make_completion_signatures<Sndr,Env,AddlSigs,SetValue,SetError,SetStopped> is an alias for dependent_completion_signatures<no_env>,
-
-
Otherwise, make_completion_signatures<Sndr,Env,AddlSigs,SetValue,SetError,SetStopped> names the type completion_signatures<Sigs...> where Sigs... is the unique set of types in all the template arguments
-of all the completion_signatures instantiations in [AddlSigs,Vs...,Es...,Ss].
-
-
-
-
6.26. execution::as_awaitable
-
Change [exec.as_awaitable]/p1.2.1 as follows:
-
-
-
awaitable-receiver is equivalent to the following:
- Let r be an rvalue expression of type awaitable-receiver,
-let cr be a const lvalue that refers to r, let
- v
- vs...
- be an
- expression of type result_t
- arbitrary function parameter
-pack of types Vs...
- , and let err be an arbitrary expression of type Err. Then:
-
-
-
- If value_t is void, then execution::set_value(r) is expression-equivalent to (r.result_ptr_->emplace<1>(),r.continuation_.resume()); otherwise, execution::set_value(r,v) is expression-equivalent to (r.result_ptr_->emplace<1>(v),r.continuation_.resume()).
-
- If constructible_from<result_t,Vs...> is satisfied, the expression execution::set_value(r,vs...) is not potentially throwing and is equivalent to:
-
- Otherwise, execution::set_value(r,vs...) is ill-formed.
-
- The expression
- execution::set_error(r,err) is
- not potentially throwing and is
- expression-
- equivalent to
- (r.result_ptr_->emplace<2>(AS_EXCEPT_PTR(err)),r.continuation_.resume()),
- :
-
err if decay_t<Err> names the same type as exception_ptr,
-
-
Otherwise, make_exception_ptr(system_error(err)) if decay_t<Err> names the same type as error_code,
-
-
Otherwise, make_exception_ptr(err).
-
-
- The expression
- execution::set_stopped(r) is
- not potentially throwing and is
- expression-
- equivalent to static_cast<coroutine_handle<>>(r.continuation_.promise().unhandled_stopped()).resume().
-
-
tag_invoke(tag,cr,as...) is expression-equivalent to tag(as_const(cr.continuation_.promise()),as...) for any expression tag whose type satisfies forwarding-receiver-query and for any set of arguments as....
-
-
-
-
\ No newline at end of file
diff --git a/README.md b/README.md
deleted file mode 100644
index e674353..0000000
--- a/README.md
+++ /dev/null
@@ -1,11 +0,0 @@
-# std::execution
-
-`std::execution`, the proposed C++ framework for asynchronous and parallel programming.
-
-You can see a rendered copy of the current draft [here](https://cplusplus.github.io/sender-receiver/execution.html).
-
-[](https://github.com/cplusplus/sender-receiver/actions/workflows/gh-pages.yml)
-
-## Reference implementation
-
-You can find the reference implementation at [NVIDIA/stdexec](https://github.com/NVIDIA/stdexec) on GitHub.
diff --git a/execution.bs b/execution.bs
deleted file mode 100644
index 8ce69d2..0000000
--- a/execution.bs
+++ /dev/null
@@ -1,10001 +0,0 @@
-
-Title: `std::execution`
-H1: std::execution
-Shortname: D2300
-Revision: 11
-Status: D
-Group: WG21
-Date: 2024-06-28
-Audience: SG1, LEWG
-Editor: Michał Dominiak, griwes@griwes.info
-Editor: Georgy Evtushenko, evtushenko.georgy@gmail.com
-Editor: Lewis Baker, lewissbaker@gmail.com
-Editor: Lucian Radu Teodorescu, lucteo@lucteo.ro
-Editor: Lee Howes, xrikcus@gmail.com
-Editor: Kirk Shoop, kirk.shoop@gmail.com
-Editor: Michael Garland, mgarland@nvidia.com
-Editor: Eric Niebler, eric.niebler@gmail.com
-Editor: Bryce Adelstein Lelbach, brycelelbach@gmail.com
-URL: https://wg21.link/P2300
-!Source: GitHub
-Issue Tracking: GitHub https://github.com/cplusplus/sender-receiver/issues
-Metadata Order: Editor, This Version, Source, Issue Tracking, Project, Audience
-Markup Shorthands: markdown yes
-Toggle Diffs: no
-No Abstract: yes
-Default Biblio Display: inline
-Default Highlight: c++
-
-
-
-
-# Introduction # {#intro}
-
-This paper proposes a self-contained design for a Standard C++ framework for
-managing asynchronous execution on generic execution resources. It is based on
-the ideas in [[P0443R14]] and its companion papers.
-
-## Motivation ## {#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 needs 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 of concurrency primitives
-(`std::atomic`, `std::mutex`, `std::counting_semaphore`, etc) and lower level
-building blocks (`std::thread`, etc), we lack a Standard vocabulary and
-framework for asynchrony and parallelism that C++ programmers desperately need.
-`std::async`/`std::future`/`std::promise`, 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.
-
-## Priorities ## {#priorities}
-
-* Be composable and generic, allowing users to write code that can be used with
- many different types of execution resources.
-* Encapsulate common asynchronous patterns in customizable and reusable
- algorithms, so users don't have to invent things themselves.
-* Make it easy to be correct by construction.
-* Support the diversity of execution resources and execution agents, because not
- all execution agents are created equal; some are less capable than others,
- but not less important.
-* Allow everything to be customized by an execution resource, including transfer
- to other execution resources, but don't require that execution resources
- customize everything.
-* Care about all reasonable use cases, domains and platforms.
-* Errors must be propagated, but error handling must not present a burden.
-* Support cancellation, which is not an error.
-* Have clear and concise answers for where things execute.
-* Be able to manage and terminate the lifetimes of objects asynchronously.
-
-## Examples: End User ## {#example-end-user}
-
-In this section we demonstrate the end-user experience of asynchronous
-programming directly with the sender algorithms presented in this paper. See
-[[#design-sender-factories]], [[#design-sender-adaptors]], and
-[[#design-sender-consumers]] for short explanations of the algorithms used in
-these code examples.
-
-### Hello world ### {#example-hello-world}
-
-```c++
-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:
-
-1. First we need to get a scheduler from somewhere, such as a thread pool. A
- scheduler is a lightweight handle to an execution resource.
-
-2. To start a chain of work on a scheduler, we call
- [[#design-sender-factory-schedule]], which returns a sender that completes on
- the scheduler. A sender describes asynchronous work and sends a signal
- (value, error, or stopped) to some recipient(s) when that work completes.
-
-3. We use sender algorithms to produce senders and compose asynchronous work.
- [[#design-sender-adaptor-then]] is a sender adaptor that takes an input
- sender and a `std::invocable`, and calls the `std::invocable` on the signal
- sent by the input sender. The sender returned by `then` sends the result of
- that invocation. In this case, the input sender came from `schedule`, so its
- `void`, meaning it won't send us a value, so our `std::invocable` takes no
- parameters. But we return an `int`, which will be sent to the next recipient.
-
-4. Now, we add another operation to the chain, again using
- [[#design-sender-adaptor-then]]. This time, we get sent a value - the `int`
- from the previous step. We add `42` to it, and then return the result.
-
-5. 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
- [[#design-sender-consumer-sync_wait]], which will either return a
- `std::optional>` with the value sent by the last sender, or
- an empty `std::optional` if the last sender sent a stopped signal, or it
- throws an exception if the last sender sent an error.
-
-### Asynchronous inclusive scan ### {#example-async-inclusive-scan}
-
-```c++
-using namespace std::execution;
-
-sender auto async_inclusive_scan(scheduler auto sch, // 2
- std::span input, // 1
- std::span 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 partials(tile_count + 1); // 4
- partials[0] = init; // 4
-
- return just(std::move(partials)) // 5
- | continues_on(sch)
- | bulk(tile_count, // 6
- [ = ](std::size_t i, std::vector& 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&& 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& 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&& partials) { // 15
- return output; // 15
- }); // 15
-}
-```
-
-This example builds an asynchronous computation of an inclusive scan:
-
-1. It scans a sequence of `double`s (represented as the `std::span` `input`) and stores the result in another sequence of `double`s
- (represented as `std::span` `output`).
-2. It takes a scheduler, which specifies what execution resource the scan should
- be launched on.
-3. It also takes a `tile_count` parameter that controls the number of execution
- agents that will be spawned.
-4. First we need to allocate temporary storage needed for the algorithm, which
- we'll do with a `std::vector`, `partials`. We need one `double` of temporary
- storage for each execution agent we create.
-5. Next we'll create our initial sender with [[#design-sender-factory-just]] and
- [[#design-sender-adaptor-continues_on]]. These senders will send the temporary
- storage, which we've moved into the sender. The sender has a completion
- scheduler of `sch`, which means the next item in the chain will use `sch`.
-6. Senders and sender adaptors support composition via `operator|`, similar to
- C++ ranges. We'll use `operator|` to attach the next piece of work, which
- will spawn `tile_count` execution agents using
- [[#design-sender-adaptor-bulk]] (see [[#design-pipeable]] for details).
-7. Each agent will call a `std::invocable`, passing it two arguments. The first
- is the agent's index (`i`) in the [[#design-sender-adaptor-bulk]] operation,
- in this case a unique integer in `[0, tile_count)`. The second argument is
- what the input sender sent - the temporary storage.
-8. 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.
-9. Then we do a sequential `std::inclusive_scan` over our elements. We store the
- scan result for our last element, which is the sum of all of our elements,
- in our temporary storage `partials`.
-10. After all computation in that initial [[#design-sender-adaptor-bulk]] pass
- has completed, every one of the spawned execution agents will have written
- the sum of its elements into its slot in `partials`.
-11. Now we need to scan all of the values in `partials`. We'll do that with a
- single execution agent which will execute after the
- [[#design-sender-adaptor-bulk]] completes. We create that execution agent
- with [[#design-sender-adaptor-then]].
-12. [[#design-sender-adaptor-then]] takes an input sender and an
- `std::invocable` and calls the `std::invocable` with the value sent by the
- input sender. Inside our `std::invocable`, we call `std::inclusive_scan`
- on `partials`, which the input senders will send to us.
-13. Then we return `partials`, which the next phase will need.
-14. Finally we do another [[#design-sender-adaptor-bulk]] of the same shape as
- before. In this [[#design-sender-adaptor-bulk]], we will use the scanned
- values in `partials` to integrate the sums from other tiles into our
- elements, completing the inclusive scan.
-15. `async_inclusive_scan` returns a sender that sends the output
- `std::span`. A consumer of the algorithm can chain additional work
- that uses the scan result. At the point at which `async_inclusive_scan`
- returns, the computation may not have completed. In fact, it may not have
- even started.
-
-### Asynchronous dynamically-sized read ### {#example-async-dynamically-sized-read}
-
-```c++
-using namespace std::execution;
-
-sender_of auto async_read( // 1
- sender_of> auto buffer, // 1
- auto handle); // 1
-
-struct dynamic_buffer { // 3
- std::unique_ptr data; // 3
- std::size_t size; // 3
-}; // 3
-
-sender_of auto async_read_array(auto handle) { // 2
- return just(dynamic_buffer{}) // 4
- | let_value([handle] (dynamic_buffer& buf) { // 5
- return just(std::as_writable_bytes(std::span(&buf.size, 1))) // 6
- | async_read(handle) // 7
- | then( // 8
- [&buf] (std::size_t bytes_read) { // 9
- assert(bytes_read == sizeof(buf.size)); // 10
- buf.data = std::make_unique(buf.size); // 11
- return std::span(buf.data.get(), buf.size); // 12
- })
- | async_read(handle) // 13
- | then(
- [&buf] (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:
-
-1. `async_read` 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 a `std::span`, and
- a handle to an I/O context. It will asynchronously read into the input
- buffer, up to the size of the `std::span`. It returns a sender which will
- send the number of bytes read once the read completes.
-2. `async_read_array` 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 a `dynamic_buffer`
- object that owns the data that was sent.
-3. `dynamic_buffer` is an aggregate struct that contains a
- `std::unique_ptr` and a size.
-4. The first thing we do inside of `async_read_array` is create a sender that
- will send a new, empty `dynamic_array` object using
- [[#design-sender-factory-just]]. We can attach more work to the pipeline
- using `operator|` composition (see [[#design-pipeable]] for details).
-5. We need the lifetime of this `dynamic_array` object to last for the entire
- pipeline. So, we use `let_value`, which takes an input sender and a
- `std::invocable` that must return a sender itself (see
- [[#design-sender-adaptor-let]] for details). `let_value` sends the value
- from the input sender to the `std::invocable`. Critically, the lifetime of
- the sent object will last until the sender returned by the `std::invocable`
- completes.
-6. Inside of the `let_value` `std::invocable`, we have the rest of our logic.
- First, we want to initiate an `async_read` of the buffer size. To do that,
- we need to send a `std::span` pointing to `buf.size`. We can do that with
- [[#design-sender-factory-just]].
-7. We chain the `async_read` onto the [[#design-sender-factory-just]] sender
- with `operator|`.
-8. Next, we pipe a `std::invocable` that will be invoked after the `async_read`
- completes using [[#design-sender-adaptor-then]].
-9. That `std::invocable` gets sent the number of bytes read.
-10. We need to check that the number of bytes read is what we expected.
-11. Now that we have read the size of the data, we can allocate storage for it.
-12. We return a `std::span` to the storage for the data from the
- `std::invocable`. This will be sent to the next recipient in the pipeline.
-13. And that recipient will be another `async_read`, which will read the data.
-14. Once the data has been read, in another [[#design-sender-adaptor-then]], we
- confirm that we read the right number of bytes.
-15. Finally, we move out of and return our `dynamic_buffer` object. It will get
- sent by the sender returned by `async_read_array`. We can attach more
- things to that sender to use the data in the buffer.
-
-## Asynchronous Windows socket `recv` ## {#example-async-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 `async_recv()`
-operation for a Windows Socket.
-
-```c++
-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
-struct recv_op : operation_base {
- using operation_state_concept = std::execution::operation_state_t;
-
- 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(data);
- }
-
- void start() & noexcept {
- // Avoid even calling WSARecv() if operation already cancelled
- auto st = std::execution::get_stop_token(
- std::execution::get_env(receiver));
- if (st.stop_requested()) {
- std::execution::set_stopped(std::move(receiver));
- return;
- }
-
- // Store and cache result here in case it changes during execution
- const bool stopPossible = st.stop_possible();
- if (!stopPossible) {
- ready.store(true, std::memory_order_relaxed);
- }
-
- // Launch the operation
- DWORD bytesTransferred = 0;
- DWORD flags = 0;
- int result = WSARecv(sock, &buffer, 1, &bytesTransferred, &flags,
- static_cast(this), NULL);
- if (result == SOCKET_ERROR) {
- int errorCode = WSAGetLastError();
- if (errorCode != WSA_IO_PENDING) {
- if (errorCode == WSA_OPERATION_ABORTED) {
- std::execution::set_stopped(std::move(receiver));
- } else {
- std::execution::set_error(std::move(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(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
- stopCallback.emplace(std::move(st), cancel_cb{*this});
-
- // Mark as 'completed'
- if (ready.load(std::memory_order_acquire) ||
- ready.exchange(true, std::memory_order_acq_rel)) {
- // Already completed on another thread
- stopCallback.reset();
-
- BOOL ok = WSAGetOverlappedResult(sock, (WSAOVERLAPPED*)this, &bytesTransferred, FALSE, &flags);
- if (ok) {
- std::execution::set_value(std::move(receiver), bytesTransferred);
- } else {
- int errorCode = WSAGetLastError();
- std::execution::set_error(std::move(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(op);
-
- if (self.ready.load(std::memory_order_acquire) ||
- self.ready.exchange(true, std::memory_order_acq_rel)) {
- // Unsubscribe any stop callback so we know that CancelIoEx() is not accessing 'op'
- // any more
- self.stopCallback.reset();
-
- if (errorCode == 0) {
- std::execution::set_value(std::move(self.receiver), bytesTransferred);
- } else {
- std::execution::set_error(std::move(self.receiver),
- std::error_code(errorCode, std::system_category()));
- }
- }
- }
-
- using stop_callback_t = stop_callback_of_t>, cancel_cb>;
-
- Receiver receiver;
- SOCKET sock;
- WSABUF buffer;
- std::optional stopCallback;
- std::atomic ready{false};
-};
-
-struct recv_sender {
- using sender_concept = std::execution::sender_t;
- SOCKET sock;
- void* data;
- size_t len;
-
- template
- recv_op connect(Receiver r) const {
- return recv_op{sock, data, len, std::move(r)};
- }
-};
-
-recv_sender async_recv(SOCKET s, void* data, size_t len) {
- return recv_sender{s, data, len};
-}
-```
-
-### More end-user examples ### {#example-moar}
-
-#### Sudoku solver #### {#example-sudoku}
-
-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](https://github.com/kirkshoop/libunifex/blob/sudoku/examples/sudoku.cpp).
-Some things that are worth noting about Kirk's solution:
-
-1. Although it schedules asynchronous 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`.
-
-2. 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](https://github.com/oneapi-src/oneTBB/issues/568).
-
-For comparison, the TBB-based Sudoku solver can be found
-[here](https://github.com/oneapi-src/oneTBB/blob/40a9a1060069d37d5f66912c6ee4cf165144774b/examples/task_group/sudoku/sudoku.cpp).
-
-#### File copy #### {#example-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](https://github.com/kirkshoop/libunifex/blob/filecopy/examples/file_copy.cpp).
-
-#### Echo server #### {#example-echo-server}
-
-Dietmar Kuehl has proposed networking APIs that use the sender/receiver
-abstraction (see \[P2762](https://wg21.link/P2762)). He has implemented an echo
-server as a demo. His echo server code can be found
-[here](https://github.com/dietmarkuehl/kuhllib/blob/main/src/examples/echo_server.cpp).
-
-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.
-
-
-
-In this code, `NN::async_read_some` and `NN::async_write_some` are asynchronous
-socket-based networking APIs that return senders. `EX::repeat_effect_until`,
-`EX::let_value`, and `EX::then` 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 `repeat_effect_until` expression and all its
-child operations -- is stored altogether in a single object.
-
-## Examples: Algorithms ## {#example-algorithm}
-
-In this section we show a few simple sender/receiver-based algorithm
-implementations.
-
-### `then` ### {#example-then}
-
-```c++
-namespace stdexec = std::execution;
-
-template
-class _then_receiver : public R {
- F f_;
-
- public:
- _then_receiver(R r, F f) : R(std::move(r)), f_(std::move(f)) {}
-
- // Customize set_value by invoking the callable and passing the result to
- // the inner receiver
- template
- requires std::invocable
- void set_value(As&&... as) && noexcept {
- try {
- stdexec::set_value(std::move(*this).base(), std::invoke((F&&) f_, (As&&) as...));
- } catch(...) {
- stdexec::set_error(std::move(*this).base(), std::current_exception());
- }
- }
-};
-
-template
-struct _then_sender {
- using sender_concept = stdexec::sender_t;
- S s_;
- F f_;
-
- template
- using _set_value_t = stdexec::completion_signatures<
- stdexec::set_value_t(std::invoke_result_t)>;
-
- using _except_ptr_sig =
- stdexec::completion_signatures;
-
- // Compute the completion signatures
- template
- auto get_completion_signatures(Env&& env) && noexcept
- -> stdexec::transform_completion_signatures_of<
- S, Env, _except_ptr_sig, _set_value_t> {
- return {};
- }
-
- // Connect:
- template
- auto connect(R r) && -> stdexec::connect_result_t> {
- return stdexec::connect(
- (S&&) s_, _then_receiver{(R&&) r, (F&&) f_});
- }
-
- decltype(auto) get_env() const noexcept {
- return get_env(s_);
- }
-};
-
-template
-stdexec::sender auto then(S s, F f) {
- return _then_sender{(S&&) s, (F&&) f};
-}
-```
-
-This code builds a `then` 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 (`set_error` and `set_stopped`), as
-well as all receiver queries, are passed through unchanged.
-
-In detail, it does the following:
-
-1. Defines a receiver in terms of receiver and an invocable that:
- * Defines a constrained `set_value` member function for transforming the
- value channel.
- * Delegates `set_error` and `set_stopped` to the inner receiver.
-
-2. Defines a sender that aggregates another sender and the invocable, which
- defines a `connect` member function that wraps the incoming receiver in the
- receiver from (1) and passes it and the incoming sender to
- `std::execution::connect`, returning the result. It also defines a
- `get_completion_signatures` member function that declares the sender's
- completion signatures when executed within a particular environment.
-
-### `retry` ### {#example-retry}
-
-```c++
-using namespace std;
-namespace stdexec = execution;
-
-template
-concept _decays_to = same_as, To>;
-
-// _conv needed so we can emplace construct non-movable types into
-// a std::optional.
-template
-struct _conv {
- F f_;
-
- static_assert(is_nothrow_move_constructible_v);
- explicit _conv(F f) noexcept : f_((F&&) f) {}
-
- operator invoke_result_t() && {
- return ((F&&) f_)();
- }
-};
-
-template
-struct _retry_op;
-
-// pass through all customizations except set_error, which retries
-// the operation.
-template
-struct _retry_receiver {
- _retry_op* o_;
-
- void set_value(auto&&... as) && noexcept {
- stdexec::set_value(std::move(o_->r_), (decltype(as)&&) as...);
- }
-
- void set_error(auto&&) && noexcept {
- o_->_retry(); // This causes the op to be retried
- }
-
- void set_stopped() && noexcept {
- stdexec::set_stopped(std::move(o_->r_));
- }
-
- decltype(auto) get_env() const noexcept {
- return get_env(o_->r_);
- }
-};
-
-// Hold the nested operation state in an optional so we can
-// re-construct and re-start it if the operation fails.
-template
-struct _retry_op {
- using operation_state_concept = stdexec::operation_state_t;
- using _child_op_t =
- stdexec::connect_result_t>;
-
- S s_;
- R r_;
- optional<_child_op_t> o_;
-
- _op(_op&&) = delete;
- _op(S s, R r)
- : s_(std::move(s)), r_(std::move(r)), o_{_connect()} {}
-
- auto _connect() noexcept {
- return _conv{[this] {
- return stdexec::connect(s_, _retry_receiver{this});
- }};
- }
-
- void _retry() noexcept {
- try {
- o_.emplace(_connect()); // potentially-throwing
- stdexec::start(*o_);
- } catch(...) {
- stdexec::set_error(std::move(r_), std::current_exception());
- }
- }
-
- void start() & noexcept {
- stdexec::start(*o_);
- }
-};
-
-// Helpers for computing the `then` sender's completion signatures:
-template
- using _value_t =
- stdexec::completion_signatures;
-
-template
- using _error_t = stdexec::completion_signatures<>;
-
-using _except_sig =
- stdexec::completion_signatures;
-
-template
-struct _retry_sender {
- using sender_concept = stdexec::sender_t;
- S s_;
- explicit _retry_sender(S s) : s_(std::move(s)) {}
-
- // Declare the signatures with which this sender can complete
- template
- using _compl_sigs =
- stdexec::transform_completion_signatures_of<
- S&, Env, _except_sig, _value_t, _error_t>;
-
- template
- auto get_completion_signatures(Env&&) const noexcept -> _compl_sigs {
- return {};
- }
-
- template
- requires stdexec::sender_to>
- _retry_op connect(R r) && {
- return {std::move(s_), std::move(r)};
- }
-
- decltype(auto) get_env() const noexcept {
- return get_env(s_);
- }
-};
-
-template
-stdexec::sender auto retry(S s) {
- return _retry_sender{std::move(s)};
-}
-```
-
-The `retry` algorithm takes a multi-shot sender and causes it to repeat on
-error, passing through values and stopped signals. Each time the input sender is
-restarted, a new receiver is connected and the resulting operation state is
-stored in an `optional`, which allows us to reinitialize it multiple times.
-
-This example does the following:
-
-1. Defines a `_conv` utility that takes advantage of C++17's guaranteed copy
- elision to emplace a non-movable type in a `std::optional`.
-
-2. Defines a `_retry_receiver` 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 `set_error`, which causes a `_retry()`
- function to be called instead.
-
-3. Defines an operation state that aggregates the input sender and receiver, and
- declares storage for the nested operation state in an `optional`.
- Constructing the operation state constructs a `_retry_receiver` with a
- pointer to the (under construction) operation state and uses it to connect
- to the input sender.
-
-4. Starting the operation state dispatches to `start` on the inner operation
- state.
-
-5. The `_retry()` 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.
-
-6. After reinitializing the inner operation state, `_retry()` calls `start` on
- it, causing the failed operation to be rescheduled.
-
-7. Defines a `_retry_sender` that implements a `connect` member function to
- return an operation state constructed from the passed-in sender and
- receiver.
-
-8. `_retry_sender` also implements a `get_completion_signatures` member function
- to describe the ways this sender may complete when executed in a particular
- execution resource.
-
-## Examples: Schedulers ## {#example-schedulers}
-
-In this section we look at some schedulers of varying complexity.
-
-### Inline scheduler ### {#example-schedulers-inline}
-
-```c++
-namespace stdexec = std::execution;
-
-class inline_scheduler {
- template
- struct _op {
- using operation_state_concept = operation_state_t;
- R rec_;
-
- void start() & noexcept {
- stdexec::set_value(std::move(rec_));
- }
- };
-
- struct _env {
- template
- inline_scheduler query(stdexec::get_completion_scheduler_t) const noexcept {
- return {};
- }
- };
-
- struct _sender {
- using sender_concept = stdexec::sender_t;
- using _compl_sigs = stdexec::completion_signatures;
- using completion_signatures = _compl_sigs;
-
- template R>
- _op connect(R rec) noexcept(std::is_nothrow_move_constructible_v) {
- return {std::move(rec)};
- }
-
- _env get_env() const noexcept {
- return {};
- }
- };
-
- public:
- inline_scheduler() = default;
-
- _sender schedule() const 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 `std::execution::start` on the operation
-state produced by its sender. In other words,
-`start(connect(schedule(inline_scheduler()), receiver))` is just a fancy way of
-saying `set_value(receiver)`, with the exception of the fact that `start` wants
-to be passed an lvalue.
-
-Although not a particularly useful scheduler, it serves to illustrate the basics
-of implementing one. The `inline_scheduler`:
-
-1. Customizes `execution::schedule` to return an instance of the sender type
- `_sender`.
-2. The `_sender` type models the `sender` concept and provides the metadata
- needed to describe it as a sender of no values
- and that never calls `set_error` or `set_stopped`. This
- metadata is provided with the help of the `execution::completion_signatures`
- utility.
-3. The `_sender` type customizes `execution::connect` to accept a receiver of no
- values. It returns an instance of type `_op` that holds the receiver by
- value.
-4. The operation state customizes `std::execution::start` to call
- `std::execution::set_value` on the receiver.
-
-### Single thread scheduler ### {#example-single-thread}
-
-This example shows how to create a scheduler for an execution resource that
-consists of a single thread. It is implemented in terms of a lower-level
-execution resource called `std::execution::run_loop`.
-
-```c++
-class single_thread_context {
- std::execution::run_loop loop_;
- std::thread thread_;
-
-public:
- single_thread_context()
- : loop_()
- , thread_([this] { loop_.run(); })
- {}
- single_thread_context(single_thread_context&&) = delete;
-
- ~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 `single_thread_context` 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 `execution::run_loop` context implementation. It
-is slightly too long to include here, so we only provide [a reference to
-it](https://github.com/NVIDIA/stdexec/blob/596707991a321ecf8219c03b79819ff4e8ecd278/include/stdexec/execution.hpp#L4201-L4339),
-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 `this_thread::sync_wait`, 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.
-
-## Examples: Server theme ## {#example-server}
-
-In this section we look at some examples of how one would use senders to
-implement an HTTP server. The examples ignore the low-level details of the HTTP
-server and looks at how senders can be combined to achieve the goals of the
-project.
-
-General application context:
-* server application that processes images
-* execution resources:
- - 1 dedicated thread for network I/O
- - N worker threads used for CPU-intensive work
- - M threads for auxiliary I/O
- - optional GPU context that may be used on some types of servers
-* all parts of the applications can be asynchronous
-* no locks shall be used in user code
-
-### Composability with `execution::let_*` ### {#example-server-let}
-
-Example context:
-- we are looking at the flow of processing an HTTP request and sending back the
- response.
-- show how one can break the (slightly complex) flow into steps with
- `execution::let_*` functions.
-- different phases of processing HTTP requests are broken down into separate
- concerns.
-- each part of the processing might use different execution resources (details
- not shown in this example).
-- error handling is generic, regardless which component fails; we always send
- the right response to the clients.
-
-Goals:
-- show how one can break more complex flows into steps with let_* functions.
-- exemplify the use of `let_value`, `let_error`, `let_stopped`, and `just`
- algorithms.
-
-```c++
-namespace stdexec = std::execution;
-
-// Returns a sender that yields an http_request object for an incoming request
-stdexec::sender auto schedule_request_start(read_requests_ctx ctx) {...}
-
-// Sends a response back to the client; yields a void signal on success
-stdexec::sender auto send_response(const http_response& resp) {...}
-
-// Validate that the HTTP request is well-formed; forwards the request on success
-stdexec::sender auto validate_request(const http_request& req) {...}
-
-// Handle the request; main application logic
-stdexec::sender auto handle_request(const http_request& req) {
- //...
- return stdexec::just(http_response{200, result_body});
-}
-
-// Transforms server errors into responses to be sent to the client
-stdexec::sender auto error_to_response(std::exception_ptr err) {
- try {
- std::rethrow_exception(err);
- } catch (const std::invalid_argument& e) {
- return stdexec::just(http_response{404, e.what()});
- } catch (const std::exception& e) {
- return stdexec::just(http_response{500, e.what()});
- } catch (...) {
- return stdexec::just(http_response{500, "Unknown server error"});
- }
-}
-
-// Transforms cancellation of the server into responses to be sent to the client
-stdexec::sender auto stopped_to_response() {
- return stdexec::just(http_response{503, "Service temporarily unavailable"});
-}
-
-//...
-
-// The whole flow for transforming incoming requests into responses
-stdexec::sender auto snd =
- // get a sender when a new request comes
- schedule_request_start(the_read_requests_ctx)
- // make sure the request is valid; throw if not
- | stdexec::let_value(validate_request)
- // process the request in a function that may be using a different execution resource
- | stdexec::let_value(handle_request)
- // If there are errors transform them into proper responses
- | stdexec::let_error(error_to_response)
- // If the flow is cancelled, send back a proper response
- | stdexec::let_stopped(stopped_to_response)
- // write the result back to the client
- | stdexec::let_value(send_response)
- // done
- ;
-
-// execute the whole flow asynchronously
-stdexec::start_detached(std::move(snd));
-```
-
-The example shows how one can separate out the concerns for interpreting
-requests, validating requests, running the main logic for handling the request,
-generating error responses, handling cancellation and sending the response back
-to the client. They are all different phases in the application, and can be
-joined together with the `let_*` functions.
-
-All our functions return `execution::sender` objects, so that they can all
-generate success, failure and cancellation paths. For example, regardless where
-an error is generated (reading request, validating request or handling the
-response), we would have one common block to handle the error, and following
-error flows is easy.
-
-Also, because of using `execution::sender` objects at any step, we might expect
-any of these steps to be completely asynchronous; the overall flow doesn't care.
-Regardless of the execution resource in which the steps, or part of the steps
-are executed in, the flow is still the same.
-
-### Moving between execution resources with `execution::starts_on` and `execution::continues_on` ### {#example-server-on}
-
-Example context:
-- reading data from the socket before processing the request
-- reading of the data is done on the I/O context
-- no processing of the data needs to be done on the I/O context
-
-Goals:
-- show how one can change the execution resource
-- exemplify the use of `starts_on` and `continues_on` algorithms
-
-```c++
-namespace stdexec = std::execution;
-
-size_t legacy_read_from_socket(int sock, char* buffer, size_t buffer_len);
-void process_read_data(const char* read_data, size_t read_len);
-//...
-
-// A sender that just calls the legacy read function
-auto snd_read = stdexec::just(sock, buf, buf_len)
- | stdexec::then(legacy_read_from_socket);
-
-// The entire flow
-auto snd =
- // start by reading data on the I/O thread
- stdexec::starts_on(io_sched, std::move(snd_read))
- // do the processing on the worker threads pool
- | stdexec::continues_on(work_sched)
- // process the incoming data (on worker threads)
- | stdexec::then([buf](int read_len) { process_read_data(buf, read_len); })
- // done
- ;
-
-// execute the whole flow asynchronously
-stdexec::start_detached(std::move(snd));
-```
-
-The example assume that we need to wrap some legacy code of reading sockets, and
-handle execution resource switching. (This style of reading from socket may not
-be the most efficient one, but it's working for our purposes.) For performance
-reasons, the reading from the socket needs to be done on the I/O thread, and all
-the processing needs to happen on a work-specific execution resource (i.e.,
-thread pool).
-
-Calling `execution::starts_on` will ensure that the given sender will be started on the
-given scheduler. In our example, `snd_read` is going to be started on the I/O
-scheduler. This sender will just call the legacy code.
-
-The completion-signal will be issued in the I/O execution resource, so we have
-to move it to the work thread pool. This is achieved with the help of the
-`execution::continues_on` algorithm. The rest of the processing (in our case, the
-last call to `then`) will happen in the work thread pool.
-
-The reader should notice the difference between `execution::starts_on` and
-`execution::continues_on`. The `execution::starts_on` algorithm will ensure that the given
-sender will start in the specified context, and doesn't care where the
-completion-signal for that sender is sent. The `execution::continues_on` algorithm
-will not care where the given sender is going to be started, but will ensure
-that the completion-signal of will be transferred to the given context.
-
-## Design changes from P0443 ## {#intro-compare}
-
-1. The `executor` concept has been removed and all of its proposed functionality
- is now based on schedulers and senders, as per SG1 direction.
-2. Properties are not included in this paper. We see them as a possible future
- extension, if the committee gets more comfortable with them.
-3. Senders now advertise what scheduler, if any, their evaluation will complete
- on.
-4. The places of execution of user code in P0443 weren't precisely defined,
- whereas they are in this paper. See [[#design-propagation]].
-5. P0443 did not propose a suite of sender algorithms necessary for writing
- sender code; this paper does. See [[#design-sender-factories]],
- [[#design-sender-adaptors]], and [[#design-sender-consumers]].
-6. P0443 did not specify the semantics of variously qualified `connect`
- overloads; this paper does. See [[#design-shot]].
-7. This paper extends the sender traits/typed sender design to support typed
- senders whose value/error types depend on type information provided late via
- the receiver.
-8. Support for untyped senders is dropped; the `typed_sender` concept is renamed
- `sender`; `sender_traits` is replaced with `completion_signatures_of_t`.
-8. Specific type erasure facilities are omitted, as per LEWG direction. Type
- erasure facilities can be built on top of this proposal, as discussed in
- [[#design-dispatch]].
-9. A specific thread pool implementation is omitted, as per LEWG direction.
-10. Some additional utilities are added:
- * `run_loop`: An execution resource that provides a multi-producer,
- single-consumer, first-in-first-out work queue.
- * `completion_signatures` and `transform_completion_signatures`:
- Utilities for describing the ways in which a sender can complete in a
- declarative syntax.
-
-## Prior art ## {#intro-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.
-
-### Futures ### {#intro-prior-art-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.
-
-### Coroutines ### {#intro-prior-art-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 choice for a basis of all
-standard async.
-
-### Callbacks ### {#intro-prior-art-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.
-
-## Field experience ## {#intro-field-experience}
-
-### libunifex ### {#intro-field-experience-libunifex}
-
-This proposal draws heavily from our field experience with
-[libunifex](https://github.com/facebookexperimental/libunifex). Libunifex
-implements all of the concepts and customization points defined in this paper
-(with slight variations -- the design of P2300 has evolved due to LEWG
-feedback), many of this paper's algorithms (some under different names), and
-much more besides.
-
-Libunifex has several concrete schedulers in addition to the `run_loop`
-suggested here (where it is called `manual_event_loop`). 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 resource 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 Meta. An employee summarizes it
-as follows:
-
-> As of June, 2023, Unifex is still used in production at Meta. It's used to
-> express the asynchrony in
-> [rsys](https://engineering.fb.com/2020/12/21/video-engineering/rsys/), and is
-> therefore serving video calling to billions of people every month on Meta's
-> social networking apps on iOS, Android, Windows, and macOS. It's also serving
-> the Virtual Desktop experience on Oculus Quest devices, and some internal uses
-> that run on Linux.
->
-> One team at Meta has migrated from `folly::Future` to `unifex::task` and seen
-> significant developer efficiency improvements. Coroutines are easier to
-> understand than chained futures so the team was able to meet requirements for
-> certain constrained environments that would have been too complicated to
-> maintain with futures.
->
-> In all the cases mentioned above, developers mix-and-match between the sender
-> algorithms in Unifex and Unifex's coroutine type, `unifex::task`. We also rely
-> on `unifex::task`'s scheduler affinity to minimize surprise when programming
-> with coroutines.
-
-### stdexec ### {#intro-field-experience-stdexec}
-
-[stdexec](https://github.com/NVIDIA/stdexec) is the reference implementation of
-this proposal. It is a complete implementation, written from the specification
-in this paper, and is current with [\R8](https://wg21.link/P2300R8).
-
-The original purpose of stdexec was to help find specification bugs and to
-harden the wording of the proposal, but it has since become one of NVIDIA's core
-C++ libraries for high-performance computing. In addition to the facilities
-proposed in this paper, stdexec has schedulers for CUDA, Intel TBB, and MacOS.
-Like libunifex, its scope has also expanded to include a streaming abstraction
-and stream algorithms, and time-based schedulers and algorithms.
-
-The stdexec project has seen lots of community interest and contributions. At the
-time of writing (March, 2024), the GitHub repository has 1.2k stars, 130 forks,
-and 50 contributors.
-
-stdexec is fit for broad use and for ultimate contribution to libc++.
-
-### Other implementations ### {#intro-field-experience-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]]
-
- 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 resource. In HPX, algorithms can migrate
- execution between execution resources, 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 Simberg summarizes the experience as follows:
-
- > Summarizing, for us the major benefits of sender/receiver compared to the
- > old model are:
- >
- > 1. Proper hooks for transitioning between execution resources.
- > 2. The adaptors. Things like `let_value` are really nice additions.
- > 3. 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 `f2` in
- > the continuation will always be ready here `f1.then([](future f2)
- > {...})` is enough of a reason to separate the channels. All the other
- > obvious reasons apply as well of course.
- > 4. For futures we have a thing called `hpx::dataflow` which is an
- > optimized version of `when_all(...).then(...)` which avoids
- > intermediate allocations. With the sender/receiver `when_all(...) |
- > then(...)` we get that "for free".
-
-* [kuhllib](https://github.com/dietmarkuehl/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.
-
-* [C++ Bare Metal Senders and Receivers](https://github.com/intel/cpp-baremetal-senders-and-receivers) from Intel
-
- This is a prototype implementation of sender/receiver by Intel that has been
- under development since August, 2023. It is significant mostly for its
- support for bare metal (no operating system) and embedded systems, a domain
- for which senders are particularly well-suited due to their very low dynamic
- memory requirements.
-
-### Inspirations ### {#intro-field-experience-inspirations}
-
-This proposal also draws heavily from our experience with
-[Thrust](https://github.com/NVIDIA/thrust) and
-[Agency](https://github.com/agency-library/agency). It is also inspired by the
-needs of countless other C++ frameworks for asynchrony, parallelism, and
-concurrency, including:
-
-* \[HPX](https://github.com/STEllAR-GROUP/hpx)
-* [Folly](https://github.com/facebook/folly/blob/master/folly/docs/Futures.md)
-* [stlab](https://stlab.cc/libraries/concurrency/)
-
-# Revision history # {#revisions}
-
-## R10 ## {#r10}
-
-The changes since R9 are as follows:
-
-Fixes:
-
- * Fixed `connect` and `get_completion_signatures` to use `transform_sender`,
- as "[[P2999R3]]" proposed (but failed) to do. See "[[P3303R1]]" for details.
-
- * `ensure_started`, `start_detached`, `execute`, and `execute_may_block_caller`
- are removed from the proposal. They are to be replaced with safer and more
- structured APIs by "[[P3149R3]]". See "[[P3187R1]]" for details.
-
- * Fixed a logic error in the specification of `split` that could have caused a
- receiver to be completed twice in some cases.
-
- * Fixed `stopped_as_optional` to handle the case where the child sender
- completes with more than one value, in which case the `stopped_as_optional`
- sender completes with an `optional` of a `tuple` of the values.
-
- * The `queryable`, `stoppable_source`, and `stoppable_callback_for` concepts
- have been made exposition-only.
-
-Enhancements:
-
- * The `operation_state` concept no longer requires that operation states
- model `queryable`.
-
- * The `get_delegatee_scheduler` query has been renamed to
- `get_delegation_scheduler`.
-
- * The `read` environment has been renamed to `read_env`.
-
- * The nullary forms of the queries which returned instances of the `read_env`
- sender have been removed. That is, `get_scheduler()` is no longer another way
- to spell `read_env(get_scheduler)`. Same for the other queries.
-
- * A feature test macro has been added: `__cpp_lib_senders`.
-
- * `transfer` has been renamed to `continues_on`. `on` has been renamed to
- `starts_on`. A new `on` algorithm has been added that is a combination of
- `starts_on` and `continues_on` for performing work on a different context
- and automatically transitioning back to the starting one. See "[[P3175R3]]"
- for details.
-
- * An exposition-only `simple-allocator` concept is added to the
- Library introduction ([library]), and the specification of the
- `get_allocator` query is expressed in terms of it.
-
- * An exposition-only `write-env` sender adaptor has been added for
- use in the implementation of the new `on` algorithm.
-
-## R9 ## {#r9}
-
-The changes since R8 are as follows:
-
-Fixes:
-
- * The `tag_invoke` mechanism has been replaced with member functions
- for customizations as per "[[P2855R1]]".
-
- * Per guidance from LWG and LEWG, `receiver_adaptor` has been removed.
-
- * The `receiver` concept is tweaked to require that receiver types are not
- `final`. Without `receiver_adaptor` and `tag_invoke`, receiver adaptors
- are easily written using implementation inheritance.
-
- * `std::tag_t` is made exposition-only.
-
- * The types `in_place_stop_token`, `in_place_stop_source`, and
- `in_place_stop_callback` are renamed to `inplace_stop_token`,
- `inplace_stop_source`, and `inplace_stop_callback`, respectively.
-
-Enhancements:
-
- * The specification of the `sync_wait` algorithm has been updated
- for clarity.
-
- * The specification of all the stop token, source, and callback types have
- been re-expressed in terms of shared concepts.
-
- * Declarations are shown in their proper namespaces.
-
- * Editorial changes have been made to clarify what text is added,
- what is removed, and what is an editorial note.
-
- * The section numbers of the proposed wording now match the section
- numbers in the working draft of the C++ standard.
-
-## R8 ## {#r8}
-
-The changes since R7 are as follows:
-
-Fixes:
-
- * `get_env(obj)` is required to be nothrow.
-
- * `get_env` and the associated environment utilities are moved back into
- `std::execution` from `std::`.
-
- * `make_completion_signatures` is renamed `transform_completion_signatures_of`
- and is expressed in terms of the new `transform_completion_signatures`,
- which takes an input set of completion signatures instead of a sender and an
- environment.
-
- * Add a requirement on queryable objects that if `tag_invoke(query, env,
- args...)` is well-formed, then `query(env, args...)` is
- expression-equivalent to it. This is necessary to properly specify how to
- join two environments in the presence of queries that have defaults.
-
- * The `sender_in` concept requires that `E` satisfies `queryable`.
-
- * Senders of more than one value are now `co_await`-able in coroutines, the
- result of which is a `std::tuple` of the values (which is suitable as the
- initializer of a structured binding).
-
-Enhancements:
-
- * The exposition-only class template `basic-sender` is greatly
- enhanced, and the sender algorithms are respecified in term of it.
-
- * `enable_sender` and `enable_receiver` traits now have default
- implementations that look for nested `sender_concept` and `receiver_concept`
- types, respectively.
-
-## R7 ## {#r7}
-
-The changes since R6 are as follows:
-
-Fixes:
-
- * Make it valid to pass non-variadic templates to the exposition-only alias
- template `gather-signatures`, fixing the definitions of
- `value_types_of_t`, `error_types_of_t`, and the exposition-only alias
- template `sync-wait-result-type`.
- * Removed the query forwarding from `receiver_adaptor` that was
- inadvertantly left over from a previous edit.
- * When adapting a sender to an awaitable with `as_awaitable`, the sender's
- value result datum is decayed before being stored in the exposition-only
- `variant`.
- * Correctly specify the completion signatures of the `schedule_from`
- algorithm.
- * The `sender_of` concept no longer distinguishes between a sender of a
- type `T` and a sender of a type `T&&`.
- * The `just` and `just_error` sender factories now reject C-style arrays
- instead of silently decaying them to pointers.
-
-Enhancements:
-
- * The `sender` and `receiver` concepts get explicit opt-in traits called
- `enable_sender` and `enable_receiver`, respectively. The traits have
- default implementations that look for nested `is_sender` and `is_receiver`
- types, respectively.
- * `get_attrs` is removed and `get_env` is used in its place.
- * The exposition-only type `empty-env` is made normative
- and is renamed `empty_env`.
- * `get_env` gets a fall-back implementation that simply returns `empty_env{}`
- if a `tag_invoke` overload is not found.
- * `get_env` is required to be insensitive to the cvref-qualification of its
- argument.
- * `get_env`, `empty_env`, and `env_of_t` are moved into the `std::` namespace.
- * Add a new subclause describing the async programming model of senders in
- abstract terms. See [[#spec-execution-async.ops]].
-
-## R6 ## {#r6}
-
-The changes since R5 are as follows:
-
-Fixes:
-
- * Fix typo in the specification of `in_place_stop_source` about the relative
- lifetimes of the tokens and the source that produced them.
- * `get_completion_signatures` tests for awaitability with a promise type
- similar to the one used by `connect` for the sake of consistency.
- * A coroutine promise type is an environment provider (that is, it implements
- `get_env()`) rather than being directly queryable. The previous draft was
- inconsistent about that.
-
-Enhancements:
-
- * Sender queries are moved into a separate queryable "attributes" object
- that is accessed by passing the sender to `get_attrs()` (see below). The
- `sender` concept is reexpressed to require `get_attrs()` and separated
- from a new `sender_in` concept for checking whether a type is
- a sender within a particular execution environment.
- * The placeholder types `no_env` and `dependent_completion_signatures<>`
- are no longer needed and are dropped.
- * `ensure_started` and `split` are changed to persist the result of
- calling `get_attrs()` on the input sender.
- * Reorder constraints of the `scheduler` and `receiver` concepts to avoid
- constraint recursion when used in tandem with poorly-constrained, implicitly
- convertible types.
- * Re-express the `sender_of` concept to be more ergonomic and general.
- * Make the specification of the alias templates `value_types_of_t` and
- `error_types_of_t`, and the variable template `sends_done` more concise by
- expressing them in terms of a new exposition-only alias template
- `gather-signatures`.
-
-### Environments and attributes ### {#environments-and-attributes}
-
-In earlier revisions, receivers, senders, and schedulers all were directly
-queryable. In R4, receiver queries were moved into a separate "environment"
-object, obtainable from a receiver with a `get_env` accessor. In R6, the
-sender queries are given similar treatment, relocating to a "attributes"
-object obtainable from a sender with a `get_attrs` accessor. This was done
-to solve a number of design problems with the `split` and `ensure_started`
-algorithms; e.g., see
-[NVIDIA/stdexec#466](https://github.com/NVIDIA/stdexec/issues/466).
-
-Schedulers, however, remain directly queryable. As lightweight handles
-that are required to be movable and copyable, there is little reason to
-want to dispose of a scheduler and yet persist the scheduler's queries.
-
-This revision also makes operation states directly queryable, even though
-there isn't yet a use for such. Some early prototypes of cooperative bulk
-parallel sender algorithms done at NVIDIA suggest the utility of
-forwardable operation state queries. The authors chose to make opstates
-directly queryable since the opstate object is itself required to be kept
-alive for the duration of asynchronous operation.
-
-## R5 ## {#r5}
-
-The changes since R4 are as follows:
-
-Fixes:
-
- * `start_detached` requires its argument to be a `void` sender (sends no values
- to `set_value`).
-
-Enhancements:
-
- * Receiver concepts refactored to no longer require an error channel for
- `exception_ptr` or a stopped channel.
- * `sender_of` concept and `connect` customization point additionally require
- that the receiver is capable of receiving all of the sender's possible
- completions.
- * `get_completion_signatures` is now required to return an instance of either
- `completion_signatures` or `dependent_completion_signatures`.
- * `make_completion_signatures` made more general.
- * `receiver_adaptor` handles `get_env` as it does the `set_*` members; that is,
- `receiver_adaptor` will look for a member named `get_env()` in the derived
- class, and if found dispatch the `get_env_t` tag invoke customization to it.
- * `just`, `just_error`, `just_stopped`, and `into_variant` have been respecified
- as customization point objects instead of functions, following LEWG guidance.
-
-## R4 ## {#r4}
-
-The changes since R3 are as follows:
-
-Fixes:
-
- * Fix specification of `get_completion_scheduler` on the `transfer`, `schedule_from`
- and `transfer_when_all` algorithms; the completion scheduler cannot be guaranteed
- for `set_error`.
- * The value of `sends_stopped` for the default sender traits of types that are
- generally awaitable was changed from `false` to `true` to acknowledge the
- fact that some coroutine types are generally awaitable and may implement the
- `unhandled_stopped()` protocol in their promise types.
- * Fix the incorrect use of inline namespaces in the `` header.
- * Shorten the stable names for the sections.
- * `sync_wait` now handles `std::error_code` specially by throwing a
- `std::system_error` on failure.
- * Fix how ADL isolation from class template arguments is specified so it
- doesn't constrain implmentations.
- * Properly expose the tag types in the header `` synopsis.
-
-Enhancements:
-
- * Support for "dependently-typed" senders, where the completion signatures -- and
- thus the sender metadata -- depend on the type of the receiver connected
- to it. See the section [dependently-typed
- senders](#dependently-typed-senders) below for more information.
- * Add a read(query) sender factory for issuing a query
- against a receiver and sending the result through the value channel. (This is
- a useful instance of a dependently-typed sender.)
- * Add `completion_signatures` utility for declaratively defining a typed
- sender's metadata.
- * Add `make_completion_signatures` utility for specifying a sender's completion
- signatures by adapting those of another sender.
- * Drop support for untyped senders and rename `typed_sender` to `sender`.
- * `set_done` is renamed to `set_stopped`. All occurances of "`done`" in
- indentifiers replaced with "`stopped`"
- * Add customization points for controlling the forwarding of scheduler,
- sender, receiver, and environment queries through layers of adaptors;
- specify the behavior of the standard adaptors in terms of the new
- customization points.
- * Add `get_delegatee_scheduler` query to forward a scheduler that can be used
- by algorithms or by the scheduler to delegate work and forward progress.
- * Add `schedule_result_t` alias template.
- * More precisely specify the sender algorithms, including precisely what their
- completion signatures are.
- * `stopped_as_error` respecified as a customization point object.
- * `tag_invoke` respecified to improve diagnostics.
-
-### Dependently-typed senders ### {#dependently-typed-senders}
-
-**Background:**
-
-In the sender/receiver model, as with coroutines, contextual information about
-the current execution is most naturally propagated from the consumer to the
-producer. In coroutines, that means information like stop tokens, allocators and
-schedulers are propagated from the calling coroutine to the callee. In
-sender/receiver, that means that that contextual information is associated with
-the receiver and is queried by the sender and/or operation state after the
-sender and the receiver are `connect`-ed.
-
-**Problem:**
-
-The implication of the above is that the sender alone does not have all the
-information about the async computation it will ultimately initiate; some of
-that information is provided late via the receiver. However, the `sender_traits`
-mechanism, by which an algorithm can introspect the value and error types the
-sender will propagate, *only* accepts a sender parameter. It does not take into
-consideration the type information that will come in late via the receiver. The
-effect of this is that some senders cannot be typed senders when they
-otherwise could be.
-
-**Example:**
-
-To get concrete, consider the case of the "`get_scheduler()`" sender: when
-`connect`-ed and `start`-ed, it queries the receiver for its associated
-scheduler and passes it back to the receiver through the value channel. That
-sender's "value type" is the type of the *receiver's* scheduler. What then
-should `sender_traits::value_types` report for the
-`get_scheduler()`'s value type? It can't answer because it doesn't know.
-
-This causes knock-on problems since some important algorithms require a typed
-sender, such as `sync_wait`. To illustrate the problem, consider the following
-code:
-
-
-namespace ex = std::execution;
-
-ex::sender auto task =
- ex::let_value(
- ex::get_scheduler(), // Fetches scheduler from receiver.
- [](auto current_sched) {
- // Lauch some nested work on the current scheduler:
- return ex::starts_on(current_sched, nested work...);
- });
-
-std::this_thread::sync_wait(std::move(task));
-
-
-The code above is attempting to schedule some work onto the `sync_wait`'s
-`run_loop` execution resource. But `let_value` only returns a typed sender when
-the input sender is typed. As we explained above, `get_scheduler()` is not
-typed, so `task` is likewise not typed. Since `task` isn't typed, it cannot be
-passed to `sync_wait` which is expecting a typed sender. The above code would
-fail to compile.
-
-**Solution:**
-
-The solution is conceptually quite simple: extend the `sender_traits` mechanism
-to optionally accept a receiver in addition to the sender. The algorithms can
-use sender_traits<Sender, Receiver> to inspect the
-async operation's completion-signals. The `typed_sender` concept would also need
-to take an optional receiver parameter. This is the simplest change, and it
-would solve the immediate problem.
-
-**Design:**
-
-Using the receiver type to compute the sender traits turns out to have pitfalls
-in practice. Many receivers make use of that type information in their
-implementation. It is very easy to create cycles in the type system, leading to
-inscrutible errors. The design pursued in R4 is to give receivers an associated
-*environment* object -- a bag of key/value pairs -- and to move the contextual
-information (schedulers, etc) out of the receiver and into the environment. The
-`sender_traits` template and the `typed_sender` concept, rather than taking a
-receiver, take an environment. This is a much more robust design.
-
-A further refinement of this design would be to separate the receiver and the
-environment entirely, passing then as separate arguments along with the sender to
-`connect`. This paper does not propose that change.
-
-**Impact:**
-
-This change, apart from increasing the expressive power of the sender/receiver
-abstraction, has the following impact:
-
- * Typed senders become moderately more challenging to write. (The new
- `completion_signatures` and `transform_completion_signatures` utilities are
- added to ease this extra burden.)
-
- * Sender adaptor algorithms that previously constrained their sender arguments
- to satisfy the `typed_sender` concept can no longer do so as the receiver is
- not available yet. This can result in type-checking that is done later, when
- `connect` is ultimately called on the resulting sender adaptor.
-
- * Operation states that own receivers that add to or change the environment
- are typically larger by one pointer. It comes with the benefit of far fewer
- indirections to evaluate queries.
-
-**"Has it been implemented?"**
-
-Yes, the reference implementation, which can be found at
-[https://github.com/NVIDIA/stdexec](https://github.com/NVIDIA/stdexec), has
-implemented this design as well as some dependently-typed senders to confirm
-that it works.
-
-**Implementation experience**
-
-Although this change has not yet been made in libunifex, the most widely adopted
-sender/receiver implementation, a similar design can be found in Folly's
-coroutine support library. In Folly.Coro, it is possible to await a special
-awaitable to obtain the current coroutine's associated scheduler (called an
-executor in Folly).
-
-For instance, the following Folly code grabs the current executor, schedules a
-task for execution on that executor, and starts the resulting (scheduled) task
-by enqueueing it for execution.
-
-```c++
-// From Facebook's Folly open source library:
-template
-folly::coro::Task CancellableAsyncScope::co_schedule(folly::coro::Task&& task) {
- this->add(std::move(task).scheduleOn(co_await co_current_executor));
- co_return;
-}
-```
-
-Facebook relies heavily on this pattern in its coroutine code. But as described
-above, this pattern doesn't work with R3 of `std::execution` because of the lack
-of dependently-typed schedulers. The change to `sender_traits` in R4 rectifies that.
-
-**Why now?**
-
-The authors are loathe to make any changes to the design, however small, at this
-stage of the C++23 release cycle. But we feel that, for a relatively minor
-design change -- adding an extra template parameter to `sender_traits` and
-`typed_sender` -- the returns are large enough to justify the change. And there
-is no better time to make this change than as early as possible.
-
-One might wonder why this missing feature not been added to sender/receiver
-before now. The designers of sender/receiver have long been aware of the need.
-What was missing was a clean, robust, and simple design for the change, which we
-now have.
-
-**Drive-by:**
-
-We took the opportunity to make an additional drive-by change: Rather than
-providing the sender traits via a class template for users to specialize, we
-changed it into a sender *query*: get_completion_signatures(sender,
-env). That function's return type is used as the sender's traits.
-The authors feel this leads to a more uniform design and gives sender authors a
-straightforward way to make the value/error types dependent on the cv- and
-ref-qualification of the sender if need be.
-
-**Details:**
-
-Below are the salient parts of the new support for dependently-typed senders in
-R4:
-
-* Receiver queries have been moved from the receiver into a separate environment
- object.
-* Receivers have an associated environment. The new `get_env` CPO retrieves a
- receiver's environment. If a receiver doesn't implement `get_env`, it
- returns an unspecified "empty" environment -- an empty struct.
-* `sender_traits` now takes an optional `Env` parameter that is used to
- determine the error/value types.
-* The primary `sender_traits` template is replaced with a
- `completion_signatures_of_t` alias implemented in terms of a new
- `get_completion_signatures` CPO that dispatches with `tag_invoke`.
- `get_completion_signatures` takes a sender and an optional environment. A
- sender can customize this to specify its value/error types.
-* Support for untyped senders is dropped. The `typed_sender` concept has been
- renamed to `sender` and now takes an optional environment.
-* The environment argument to the `sender` concept and the
- `get_completion_signatures` CPO defaults to `no_env`. All environment
- queries fail (are ill-formed) when passed an instance of `no_env`.
-* A type `S` is required to satisfy sender<S> to be
- considered a sender. If it doesn't know what types it will complete with
- independent of an environment, it returns an instance of the placeholder
- traits `dependent_completion_signatures`.
-* If a sender satisfies both sender<S> and
- sender<S, Env>, then the completion signatures
- for the two cannot be different in any way. It is possible for an
- implementation to enforce this statically, but not required.
-* All of the algorithms and examples have been updated to work with
- dependently-typed senders.
-
-## R3 ## {#r3}
-
-The changes since R2 are as follows:
-
-Fixes:
-
-* Fix specification of the `starts_on` algorithm to clarify lifetimes of intermediate
- operation states and properly scope the `get_scheduler` query.
-* Fix a memory safety bug in the implementation of
- `connect-awaitable`.
-* Fix recursive definition of the `scheduler` concept.
-
-Enhancements:
-
-* Add `run_loop` execution resource.
-* Add `receiver_adaptor` utility to simplify writing receivers.
-* Require a scheduler's sender to model `sender_of` and provide a completion
- scheduler.
-* Specify the cancellation scope of the `when_all` algorithm.
-* Make `as_awaitable` a customization point.
-* Change `connect`'s handling of awaitables to consider those types that are
- awaitable owing to customization of `as_awaitable`.
-* Add `value_types_of_t` and `error_types_of_t` alias templates; rename
- `stop_token_type_t` to `stop_token_of_t`.
-* Add a design rationale for the removal of the possibly eager algorithms.
-* Expand the section on field experience.
-
-## R2 ## {#r2}
-
-The changes since R1 are as follows:
-
-* Remove the eagerly executing sender algorithms.
-* Extend the `execution::connect` customization point and the `sender_traits<>`
- template to recognize awaitables as `typed_sender`s.
-* Add utilities `as_awaitable()` and `with_awaitable_senders<>` so a coroutine
- type can trivially make senders awaitable with a coroutine.
-* 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 `bulk` algorithm.
-* 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 `execution::just_error` and `execution::just_stopped`.
-* Add sender adaptors `execution::stopped_as_optional` and
- `execution::stopped_as_error`.
-* Document more production uses of sender/receiver at scale.
-* Various fixes of typos and bugs.
-
-## R1 ## {#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 `unschedule` adaptor.
-* Various fixes of typos and bugs.
-
-## R0 ## {#r0}
-
-Initial revision.
-
-# Design - introduction # {#design-intro}
-
-The following three sections describe the entirety of the proposed design.
-
-* [[#design-intro]] 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.
-* [[#design-user]] 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.
-* [[#design-implementer]] 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.
-
-## Conventions ## {#design-conventions}
-
-The following conventions are used throughout the design section:
-
-1. The namespace proposed in this paper is the same as in [[P0443R14]]:
- `std::execution`; however, for brevity, the `std::` part of this name is
- omitted. When you see `execution::foo`, treat that as `std::execution::foo`.
-2. Universal references and explicit calls to `std::move`/`std::forward` are
- omitted in code samples and signatures for simplicity; assume universal
- references and perfect forwarding unless stated otherwise.
-3. 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.
-
-## Queries and algorithms ## {#design-queries-and-algorithms}
-
-A **query** is a callable 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 callable 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.
-
-# Design - user side # {#design-user}
-
-## Execution resources describe the place of execution ## {#design-contexts}
-
-An [=execution resource=] 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.
-
-## Schedulers represent execution resources ## {#design-schedulers}
-
-A [=scheduler=] is a lightweight handle that represents a strategy for
-scheduling work onto an execution resource. Since execution resources 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, `schedule`, which returns a sender that
-will complete on an execution resource 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 resource associated with sch
-
-
-Note that a particular scheduler type may provide other kinds of scheduling
-operations which are supported by its associated execution resource. It is not
-limited to scheduling purely using the `execution::schedule` API.
-
-Future papers will propose additional scheduler concepts that extend `scheduler`
-to add other capabilities. For example:
-
-* A `time_scheduler` concept that extends `scheduler` to support time-based
- scheduling. Such a concept might provide access to `schedule_after(sched,
- duration)`, `schedule_at(sched, time_point)` and `now(sched)` APIs.
-* Concepts that extend `scheduler` to support opening, reading and writing files
- asynchronously.
-* Concepts that extend `scheduler` to support connecting, sending data and
- receiving data over the network asynchronously.
-
-## Senders describe work ## {#design-senders}
-
-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 [[#design-connect]]) to that sender will eventually *receive* said values.
-
-The primary defining sender algorithm is [[#design-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 [[#design-composable]], [[#design-sender-factories]],
-[[#design-sender-adaptors]], and [[#design-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
-
-
-## Senders are composable through sender algorithms ## {#design-composable}
-
-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
- `execution::connect`) senders and return a sender.
-* [=sender consumers=], algorithms that take (and potentially
- `execution::connect`) senders and do not return a sender.
-
-## Senders can propagate completion schedulers ## {#design-propagation}
-
-One of the goals of executors is to support a diverse set of execution
-resources, including traditional thread pools, task and fiber frameworks (like
-\[HPX](https://github.com/STEllAR-GROUP/hpx)
-[Legion](https://github.com/StanfordLegion/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 resource
-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:
-
- 1. trying to submit work to one execution resource (such as a CPU thread pool)
- from another execution resource (such as a GPU or a task framework), which
- assumes that all execution agents are as capable as a `std::thread` (which
- they aren't).
- 2. forcibly interleaving two adjacent execution graph nodes that are both
- executing on one execution resource (such as a GPU) with glue code that
- runs on another execution resource (such as a CPU), which is prohibitively
- expensive for some execution resources (such as CUDA or SYCL).
- 3. having to customise most or all sender algorithms to support an execution
- resource, 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](https://github.com/agency-library/agency).
-
-None of these implementation strategies are acceptable for many classes of
-parallel runtimes, such as task frameworks (like
-\[HPX](https://github.com/STEllAR-GROUP/hpx)) or accelerator runtimes (like CUDA
-or SYCL).
-
-Therefore, in addition to the `starts_on` sender algorithm from [[P1897R3]], we are
-proposing a way for senders to advertise what scheduler (and by extension what
-execution resource) they will complete on. Any given sender may have
-[=completion schedulers=] for some or all of the signals (value, error, or
-stopped) it completes with (for more detail on the completion-signals, see
-[[#design-receivers]]). When further work is attached to that sender by invoking
-sender algorithms, that work will also complete on an appropriate completion
-scheduler.
-
-### `execution::get_completion_scheduler` ### {#design-sender-query-get_completion_scheduler}
-
-`get_completion_scheduler` is a query that retrieves the completion scheduler
-for a specific completion-signal from a sender's environment. For a sender that
-lacks a completion scheduler query for a given signal, calling
-`get_completion_scheduler` is ill-formed. If a sender advertises a completion
-scheduler for a signal in this way, that sender must ensure that it
-[=send|sends=] that signal on an execution agent belonging to an execution
-resource represented by a scheduler returned from this function. See
-[[#design-propagation]] 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>(get_env(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>(get_env(snd1));
-// completion_sch1 is equivalent to cpu_sched
-
-execution::sender auto snd2 = execution::continues_on(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>(get_env(snd3));
-// completion_sch3 is equivalent to gpu_sched
-
-
-## Execution resource transitions are explicit ## {#design-transitions}
-
-[[P0443R14]] does not contain any mechanisms for performing an execution
-resource transition. The only sender algorithm that can create a sender that
-will move execution to a *specific* execution resource is `execution::schedule`,
-which does not take an input sender. That means that there's no way to construct
-sender chains that traverse different execution resources. 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 resource transitions must be explicit; running user code
-anywhere but where they defined it to run must be considered a bug.
-
-The `execution::continues_on` sender adaptor performs a transition from one
-execution resource 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::continues_on(then1, sch2);
-execution::sender auto then2 = execution::then(snd2, []{
- std::cout << "I am running on sch2!\n";
-});
-
-this_thread::sync_wait(then2);
-
-
-## Senders can be either multi-shot or single-shot ## {#design-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 `std::unique_ptr` that it will be transferring ownership of to the
-operation-state returned by a call to `execution::connect` so that the operation has access to
-this resource. In such a sender, calling `execution::connect` 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 `execution::connect` only has overloads for
-an rvalue-qualified sender. Callers must pass the sender as an rvalue to the
-call to `execution::connect`, 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
-`execution::connect` to accept an lvalue reference to the sender. Callers can
-indicate that they want the sender to remain valid after the call to
-`execution::connect` by passing an lvalue reference to the sender to call these
-overloads. Multi-shot senders should also define overloads of
-`execution::connect` 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 `execution::connect`. 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 `execution::connect`. 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 `execution::connect` on the input
-sender, such as in `this_thread::sync_wait`.
-
-Some multi-use sender algorithms may require that an input sender be
-copy-constructible but will only call `execution::connect` 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 `execution::connect` 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.
-
-## Senders are forkable ## {#design-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 `split` 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"; })
- );
-}
-
-
-## Senders support cancellation ## {#design-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
-`execution::connect()` before calling `execution::start()`, 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 `when_all(ops...)` algorithm should cancel other operations as soon as one
- operation fails
-* a `first_successful(ops...)` algorithm should cancel the other operations as
- soon as one operation completes successfuly
-* a generic `timeout(src, duration)` algorithm needs to be able to cancel the
- `src` operation after the timeout duration has elapsed.
-* a `stop_when(src, trigger)` algorithm should cancel `src` if `trigger`
- completes first and cancel `trigger` if `src` completes first
-
-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:
-
-
-
-In this example, if we take the operation returned by `query_server_b(query)`,
-this operation will receive a stop-request when any of the following happens:
-
-* `first_successful` algorithm will send a stop-request if
- `query_server_a(query)` completes successfully
-* `when_all` algorithm will send a stop-request if the
- `load_file("some_file.jpg")` operation completes with an error or stopped
- result.
-* `timeout` algorithm will send a stop-request if the operation does not
- complete within 5 seconds.
-* `stop_when` algorithm will send a stop-request if the user clicks on the
- "Cancel" button in the user-interface.
-* The parent operation consuming the `composed_cancellation_example()` sends a
- stop-request
-
-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.
-
-### Cancellation design summary ### {#design-cancellation-summary}
-
-The design of cancellation described in this paper is built on top of and
-extends the `std::stop_token`-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 a `std::stoppable_token` concept that generalises the interface of the
- `std::stop_token` type to allow other stop token types with different
- implementation strategies.
-* Add `std::unstoppable_token` concept for detecting whether a `stoppable_token`
- can never receive a stop-request.
-* Add `std::inplace_stop_token`, `std::inplace_stop_source` and
- `std::inplace_stop_callback` types that provide a more efficient
- implementation of a stop-token for use in structured concurrency situations.
-* Add `std::never_stop_token` for use in places where you never want to issue a
- stop-request.
-* Add `std::execution::get_stop_token()` CPO for querying the stop-token to use
- for an operation from its receiver's execution environment.
-* Add `std::execution::stop_token_of_t` for querying the type of a stop-token
- returned from `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 `execution::get_stop_token()` CPO. This CPO takes a single
-parameter, which is the execution environment of the receiver passed to
-`execution::connect`, and returns a `std::stoppable_token` that the operation
-can use to check for stop-requests for that operation.
-
-As the caller of `execution::connect` typically has control over the receiver
-type it passes, it is able to customise the `std::execution::get_env()` CPO for
-that receiver to return an execution environment that hooks the
-`execution::get_stop_token()` CPO to return a stop-token that the receiver has
-control over and that it can use to communicate a stop-request to the operation
-once it has started.
-
-### Support for cancellation is optional ### {#design-cancellation-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's execution environment does not customise the
-`execution::get_stop_token()` CPO then invoking the CPO on that receiver's
-environment will invoke the default implementation which returns
-`std::never_stop_token`. This is a special `stoppable_token` type that is
-statically known to always return `false` from the `stop_possible()` 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 `execution::get_stop_token()`, 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.
-
-### Cancellation is inherently racy ### {#design-cancellation-racy}
-
-Usually, an operation will attach a stop callback at some point inside the call
-to `execution::start()` 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 `execution::start()` 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
-`execution::start()` implementation.
-
-An implementation of `execution::start()` 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
-`execution::start` 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 `execution::start()` call (which is still executing).
-
-For an example of an implementation strategy for solving these data-races see
-[[#example-async-windows-socket-recv]].
-
-### Cancellation design status ### {#design-cancellation-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.
-
-## Sender factories and adaptors are lazy ## {#design-lazy-algorithms}
-
-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 weighing 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 write an algorithm 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.
-
-### Eager execution leads to detached work or worse ### {#design-lazy-algorithms-detached}
-
-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 `execution::connect`?" or similarly, "What happens if a call
-to `execution::connect` is made, but the returned operation state is destroyed
-before `execution::start` 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:
-
-1. 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
- `when_all(eager_op_1(), eager_op_2())`. Imagine `eager_op_1()` starts an
- asynchronous operation successfully, but then `eager_op_2()` throws. For
- lazy senders, that failure happens in the context of the `when_all`
- 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 before `when_all` has been called.
-
- It then becomes the responsibility, not of the algorithm, but of the end
- user to handle the exception and ensure that `eager_op_1()` is joined before
- allowing the exception to propagate. If they fail to do that, they incur
- undefined behavior.
-
-2. Detach from the computation - let the operation continue in the background -
- like an implicit call to `std::thread::detach()`. 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::shared_ptr`, to ensure resources are kept alive until the operations
- complete, and can make clean shutdown nigh impossible.
-
-3. 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 threads 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:
-
-4. A variation of (1): Call `std::terminate` if an eager sender is destructed
- without joining it. This is the approach that `std::thread` destructor
- takes.
-
-5. 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.
-
-6. A variation of (3): Request cancellation of the operation before blocking on
- its completion. This is the strategy that `std::jthread` uses for its
- destructor. It reduces the risk of deadlock but does not eliminate it.
-
-### Eager senders complicate algorithm implementations ### {#design-lazy-algorithms-complexity}
-
-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 `execution::start` 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
-`sequence` that puts async operations in sequence by starting an operation only
-after the preceding one has completed. In an expression like `sequence(a(),
-then(src, [] { b(); }), c())`, one may reasonably assume that `a()`, `b()` and
-`c()` 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:
-
-1. 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.
-
-2. 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.
-
-### Eager senders incur cancellation-related overhead ### {#design-lazy-algorithms-runtime}
-
-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 `execution::get_stop_token` is an `std::unstoppable_token` 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).
-
-### Eager senders cannot access execution resource from the receiver ### {#design-lazy-algorithms-context}
-
-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.
-
-## Schedulers advertise their forward progress guarantees ## {#design-fpg}
-
-To decide whether a scheduler (and its associated execution resource) 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,
-`get_forward_progress_guarantee`, which returns one of the enumerators of a new
-`enum` type, `forward_progress_guarantee`. Each enumerator of
-`forward_progress_guarantee` corresponds to one of the aforementioned
-guarantees.
-
-## Most sender adaptors are pipeable ## {#design-pipeable}
-
-To facilitate an intuitive syntax for composition, most sender adaptors are pipeable; they can be composed (piped)
-together with `operator|`. This mechanism is similar to the `operator|`
-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.
-
-`a | b` will pass the sender `a` as the first argument to the pipeable sender
-adaptor `b`. 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:
-
-
-
-Certain sender adaptors are not 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.
-
-* `execution::when_all` and `execution::when_all_with_variant`: 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::starts_on`: 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 to `continues_on`.
-
-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.
-
-## A range of senders represents an async sequence of data ## {#design-range-of-senders}
-
-Senders represent a single unit of asynchronous work. In many cases though, what
-is being modeled 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 `R` that is a range of senders, consider
-the following in a coroutine that returns an async generator type:
-
-
-
-This transforms each element of the asynchronous sequence `R`
-with the function `fn` on demand, as the data arrives. The result is a new
-asynchronous sequence of the transformed values.
-
-Now imagine that `R` is the simple expression `views::iota(0)
-| views::transform(execution::just)`. 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 [`fn(0)`, `fn(1)`, `fn(2)`, ...].
-
-Far more interesting would be if `R` 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.
-
-## Senders can represent partial success ## {#design-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::stopped_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).
-
-## All awaitables are senders ## {#design-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 `sender` 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 `await_transform`
-trickery from a promise type to make them awaitable.)
-
-For an example, imagine a coroutine type called `task` that knows nothing
-about senders. It doesn't implement any of the sender customization points.
-Despite that fact, and despite the fact that the `this_thread::sync_wait`
-algorithm is constrained with the `sender` concept, the following would compile
-and do what the user wants:
-
-```c++
-task doSomeAsyncWork();
-
-int main() {
- // OK, awaitable types satisfy the requirements for 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.
-
-## Many senders can be trivially made awaitable ## {#design-senders-are-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 `execution::with_awaitable_senders` 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
-`retry` algorithm:
-
-
-
-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 `with_awaitable_senders` 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 `into_variant` sender algorithm -- before `co_await`-ing that
-sender.
-
-## Cancellation of a sender can unwind a stack of coroutines ## {#design-native-coro-unwind}
-
-When looking at the sender-based `retry` 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 `execution::set_stopped`?
-
-When your task type's promise inherits from `with_awaitable_senders`, what
-happens is this: the coroutine behaves as if an *uncatchable exception* had been
-thrown from the `co_await` 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 `with_awaitable_senders`, or more generally
-implement a member function called `unhandled_stopped`, the exception unwinds
-the chain of coroutines as if an exception were thrown except that it bypasses
-`catch(...)` clauses.
-
-In order to "catch" this uncatchable stopped exception, one of the calling
-coroutines in the stack would have to await a sender that maps the stopped
-channel into either a value or an error. That is achievable with the
-`execution::let_stopped`, `execution::upon_stopped`,
-`execution::stopped_as_optional`, or `execution::stopped_as_error` sender
-adaptors. For instance, we can use `execution::stopped_as_optional` to "catch"
-the stopped signal and map it into an empty optional as shown below:
-
-```c++
-if (auto opt = co_await execution::stopped_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
-`connect`-ing an awaitable and a receiver, the adaptation layer awaits the
-awaitable within a coroutine that implements `unhandled_stopped` in its promise
-type. The effect of this is that an "uncatchable" stopped exception propagates
-seamlessly out of awaitables, causing `execution::set_stopped` to be called on
-the receiver.
-
-Obviously, `unhandled_stopped` is a library extension of the coroutine promise
-interface. Many promise types will not implement `unhandled_stopped`. When an
-uncatchable stopped exception tries to propagate through such a coroutine, it is
-treated as an unhandled exception and `terminate` is called. The solution, as
-described above, is to use a sender adaptor to handle the stopped exception
-before awaiting it. It goes without saying that any future Standard Library
-coroutine types ought to implement `unhandled_stopped`. The author of
-[[P1056R1]], which proposes a standard coroutine task type, is in agreement.
-
-## Composition with parallel algorithms ## {#design-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 `execution::par`, 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.
-
-
-
-This function would return an object of an unspecified 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 `policy` while using `scheduler` to create any work to be run.
-The expression may be ill-formed if `scheduler` 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 `executing_on` 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 `void`
-and (2) where a customization point pairing a sender with an execution policy
-would similarly be used to obtain an object of unspecified type to be provided
-as the first argument to the algorithm.
-
-## User-facing sender factories ## {#design-sender-factories}
-
-A [=sender factory=] is an algorithm that takes no senders as parameters and
-returns a sender.
-
-### `execution::schedule` ### {#design-sender-factory-schedule}
-
-
-execution::sender auto schedule(
- execution::scheduler auto scheduler
-);
-
-
-Returns a sender describing the start of a task graph on the provided scheduler.
-See [[#design-schedulers]].
-
-
-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
-
-execution::sender auto just(
- auto ...&& values
-);
-
-
-Returns a sender with no [=completion scheduler|completion schedulers=], which
-[=send|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 `set_value`.
-
-```c++
-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(snd2, [] (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&& 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&& 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.
-```
-
-### `execution::just_error` ### {#design-sender-factory-just_error}
-
-
-execution::sender auto just_error(
- auto && error
-);
-
-
-Returns a sender with no [=completion scheduler|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 `set_error`. 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 `set_error`.
-
-### `execution::just_stopped` ### {#design-sender-factory-just_stopped}
-
-
-execution::sender auto just_stopped();
-
-
-Returns a sender with no [=completion scheduler|completion schedulers=], which
-completes immediately by calling the receiver's `set_stopped`.
-
-### `execution::read_env` ### {#design-sender-factory-read}
-
-
-execution::sender auto read_env(auto tag);
-
-
-Returns a sender that reaches into a receiver's environment and pulls out the
-current value associated with the customization point denoted by `Tag`. It then
-sends the value read back to the receiver through the value channel. For
-instance, `read_env(get_scheduler)` is a sender that asks the
-receiver for the currently suggested `scheduler` and passes it to the receiver's
-`set_value` completion-signal.
-
-This can be useful when scheduling nested dependent work. The following sender
-pulls the current schduler into the value channel and then schedules more work
-onto it.
-
-
- execution::sender auto task =
- execution::read_env(get_scheduler)
- | execution::let_value([](auto sched) {
- return execution::starts_on(sched, some nested work here);
- });
-
- this_thread::sync_wait( std::move(task) ); // wait for it to finish
-
-
-This code uses the fact that `sync_wait` associates a scheduler with the
-receiver that it connects with `task`. `read_env(get_scheduler)` reads that scheduler
-out of the receiver, and passes it to `let_value`'s receiver's `set_value`
-function, which in turn passes it to the lambda. That lambda returns a new
-sender that uses the scheduler to schedule some nested work onto `sync_wait`'s
-scheduler.
-
-## User-facing sender adaptors ## {#design-sender-adaptors}
-
-A [=sender adaptor=] is an algorithm that takes one or more senders, which it
-may `execution::connect`, 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 [[#design-sender-consumer-sync_wait]] start senders.
-
-For more implementer-centric description of starting senders, see
-[[#design-laziness]].
-
-### `execution::continues_on` ### {#design-sender-adaptor-continues_on}
-
-
-execution::sender auto continues_on(
- 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
-[[#design-transitions]].
-
-
-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::continues_on(cpu_task, gpu_sched);
-// gpu_task describes the transition of the task graph described by cpu_task to the gpu
-
-execution::sender auto then(
- execution::sender auto input,
- std::invocable<values-sent-by(input)...> function
-);
-
-
-`then` returns a sender describing the task graph described by the input sender,
-with an added node of invoking the provided function with the values
-[=send|sent=] by the input sender as arguments.
-
-`then` is **guaranteed** to not begin executing `function` until the returned
-sender is started.
-
-
-execution::sender auto input = get_input();
-execution::sender auto snd = execution::then(input, [](auto... args) {
- std::print(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.
-
-### `execution::upon_*` ### {#design-sender-adaptor-upon}
-
-
-execution::sender auto upon_error(
- execution::sender auto input,
- std::invocable<errors-sent-by(input)...> function
-);
-
-execution::sender auto upon_stopped(
- execution::sender auto input,
- std::invocable auto function
-);
-
-
-`upon_error` and `upon_stopped` are similar to `then`, but where `then` works
-with values sent by the input sender, `upon_error` works with errors, and
-`upon_stopped` is invoked when the "stopped" signal is sent.
-
-### `execution::let_*` ### {#design-sender-adaptor-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_stopped(
- execution::sender auto input,
- std::invocable auto function
-);
-
-
-`let_value` is very similar to `then`: when it is started, it invokes the
-provided function with the values [=send|sent=] by the input sender as
-arguments. However, where the sender returned from `then` sends exactly what
-that function ends up returning -
-`let_value` requires that the function return a sender, and the sender returned
-by `let_value` 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.
-
-`let_value` is **guaranteed** to not begin executing `function` until the
-returned sender is started.
-
-`let_error` and `let_stopped` are similar to `let_value`, but where `let_value`
-works with values sent by the input sender, `let_error` works with errors, and
-`let_stopped` is invoked when the "stopped" signal is sent.
-
-### `execution::starts_on` ### {#design-sender-adaptor-starts_on}
-
-
-execution::sender auto starts_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 resource associated with the provided
-scheduler. This returned sender has no [=completion scheduler|completion
-schedulers=].
-
-### `execution::into_variant` ### {#design-sender-adaptor-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.
-
-### `execution::stopped_as_optional` ### {#design-sender-adaptor-stopped_as_optional}
-
-
-execution::sender auto stopped_as_optional(
- single-sender auto snd
-);
-
-
-Returns a sender that maps the value channel from a `T` to an
-`optional>`, and maps the stopped channel to a value of an empty
-`optional>`.
-
-### `execution::stopped_as_error` ### {#design-sender-adaptor-stopped_as_error}
-
-
-template<move_constructible Error>
-execution::sender auto stopped_as_error(
- execution::sender auto snd,
- Error err
-);
-
-
-Returns a sender that maps the stopped channel to an error of `err`.
-
-### `execution::bulk` ### {#design-sender-adaptor-bulk}
-
-
-execution::sender auto bulk(
- execution::sender auto input,
- std::integral auto shape,
- 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 `function` will begin executing until the returned sender is
-started. Each invocation of `function` 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 `bulk` execute with the same guarantee. The
-number of execution agents used by `bulk` is not specified. This allows a
-scheduler to execute some invocations of the `function` in parallel.
-
-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.
-
-### `execution::split` ### {#design-sender-adaptor-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 [[#design-shot]].
-
-### `execution::when_all` ### {#design-sender-adaptor-when_all}
-
-
-execution::sender auto when_all(
- execution::sender auto ...inputs
-);
-
-execution::sender auto when_all_with_variant(
- execution::sender auto ...inputs
-);
-
-
-`when_all` 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 `set_value` 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 `when_all`. It completes
-inline on the execution resource on which the last input sender completes,
-unless stop is requested before `when_all` is started, in which case it
-completes inline within the call to `start`.
-
-`when_all_with_variant` does the same, but it adapts all the input senders using
-`into_variant`, and so it does not constrain the input arguments as `when_all`
-does.
-
-The returned sender has no [=completion scheduler|completion schedulers=].
-
-
-execution::scheduler auto sched = thread_pool.scheduler();
-
-execution::sender auto sends_1 = ...;
-execution::sender auto sends_abc = ...;
-
-execution::sender auto both = execution::when_all(
- 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"
-
-
-## User-facing sender consumers ## {#design-sender-consumers}
-
-A [=sender consumer=] is an algorithm that takes one or more senders, which it
-may `execution::connect`, as parameters, and does not return a sender.
-
-### `this_thread::sync_wait` ### {#design-sender-consumer-sync_wait}
-
-
-
-`this_thread::sync_wait` is a sender consumer that submits the work described by
-the provided sender for execution,
-blocking the current `std::thread` or thread of `main` 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
-[[#design-sender-factory-schedule]] and [[#design-sender-factory-just]] are
-meant to enter the domain of senders, `sync_wait` is one way to exit
-the domain of senders, retrieving the result of the task graph.
-
-If the provided sender sends an error instead of values, `sync_wait` throws that
-error as an exception, or rethrows the original exception if the error is of
-type `std::exception_ptr`.
-
-If the provided sender sends the "stopped" signal instead of values, `sync_wait`
-returns an empty optional.
-
-For an explanation of the `requires` clause, see [[#design-typed]]. That clause
-also explains another sender consumer, built on top of `sync_wait`:
-`sync_wait_with_variant`.
-
-Note: This function is specified inside `std::this_thread`, and not inside
-`execution`. This is because `sync_wait` 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 `std::this_thread`, 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 `std::this_fiber::sync_wait`
-would be provided. We also expect that runtimes with execution agents that use
-different synchronization mechanisms than `std::thread`'s will provide their own
-flavors of `sync_wait` as well (assuming their execution agents have the means
-to block in a non-deadlock manner).
-
-# Design - implementer side # {#design-implementer}
-
-## Receivers serve as glue between senders ## {#design-receivers}
-
-A [=receiver=] is a callback that supports more than one channel. In fact, it
-supports three of them:
-
-* `set_value`, which is the moral equivalent of an `operator()` or a function
- call, which signals successful completion of the operation its execution
- depends on;
-* `set_error`, 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; and
-* `set_stopped`, which signals that the operation completed without succeeding
- (`set_value`) and without failing (`set_error`). 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.
-
-Once an async operation has been started exactly one of these functions must be
-invoked on a receiver before it is destroyed.
-
-While the receiver interface may look novel, it is in fact very similar to the
-interface of `std::promise`, which provides the first two signals as `set_value`
-and `set_exception`, 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 [[#design-connect]].
-
-## Operation states represent work ## {#design-states}
-
-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: `start`, 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 `this_thread::sync_wait`,
-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 [[#design-connect]] must satisfy the operation state
-concept.
-
-## `execution::connect` ## {#design-connect}
-
-`execution::connect` is a customization point which [=connects=] senders with
-receivers, resulting in an operation state that will ensure that if `start` is
-called that one of the completion operations will be called on the receiver
-passed to `connect`.
-
-
-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 resource, and that execution resource will eventually call one of the
-// completion operations on rcv
-
-// operation states are not movable, and therefore this operation state object must be
-// kept alive until the operation finishes
-
-
-## Sender algorithms are customizable ## {#design-customization}
-
-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 `then`, 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 `execution::then(sender, invocable)` to be equivalent to:
-
- 1. `sender.then(invocable)`, if that expression is well-formed; otherwise
- 2. `then(sender, invocable)`, performed in a context where this call always
- performs ADL, if that expression is well-formed; otherwise
- 3. a default implementation of `then`, which returns a sender adaptor, and
- then define the exact semantics of said adaptor.
-
-However, this definition is problematic. Imagine another sender adaptor, `bulk`,
-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 `bulk` to have specialized behavior, which
-invokes a kernel of more than one thread (with its size defined by the call to
-`bulk`); therefore, we would like to customize `bulk` for CUDA senders to
-achieve this. However, there's no reason for CUDA kernels to necessarily
-customize the `then` 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 unspecified 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 `bulk` sender adaptor for our wrapped
-`schedule_sender`? 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%lt;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 `then_sender_adaptor` like in the
-example above, but another sender that overrides `bulk` 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: the expression
-`execution::(sender, args...)`, for any given sender algorithm
-that accepts a sender as its first argument, should do the following:
-
- 1. Create a sender that implements the default implementation of the sender
- algorithm. That sender is tuple-like; it can be destructured into its
- constituent parts: algorithm tag, data, and child sender(s).
-
- 2. We query the child sender for its *domain*. A **domain** is a tag type
- associated with the scheduler that the child sender will complete on.
- If there are multiple child senders, we query all of them for their
- domains and require that they all be the same.
-
- 3. We use the domain to dispatch to a `transform_sender` customization, which
- accepts the sender and optionally performs a domain-specific
- transformation on it. This customization is expected to return a new
- sender, which will be returned from `` in place of the
- original sender.
-
-## Sender adaptors are lazy ## {#design-laziness}
-
-Contrary to early revisions of this paper, we propose to make all sender
-adaptors perform strictly lazy submission, unless specified otherwise.
-
-Strictly lazy submission means that there is a guarantee
-that no work is submitted to an execution resource before a receiver is
-connected to a sender, and `execution::start` is called on the resulting
-operation state.
-
-## Lazy senders provide optimization opportunities ## {#design-fusion}
-
-Because lazy senders fundamentally *describe* work, instead of describing or
-representing the submission of said work to an execution resource, 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
-resource. 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 resources 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, an implementation could recognize two
-subsequent [[#design-sender-adaptor-bulk]]s of compatible shapes, and merge them
-together into a single submission of a GPU kernel.
-
-## Execution resource transitions are two-step ## {#design-transition-details}
-
-Because `execution::continues_on` 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 resource, 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 [[#design-customization]]),
-the type of the sender returned from `continues_on` must be controllable by the
-target scheduler. Besides, the target scheduler may itself represent a
-specialized execution resource, 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 resources
-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
-`continues_on`.
-
-To allow for such customization from both ends, we propose the inclusion of a
-secondary transitioning sender adaptor, called `schedule_from`. This adaptor is
-a form of `schedule`, 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 `continues_on`, to ensure that both schedulers have a
-say in how the transitions are made. Any scheduler that specializes
-`continues_on(snd, sch)` shall ensure that the return value of their customization
-is equivalent to `schedule_from(sch, snd2)`, where `snd2` is a successor of
-`snd` that sends values equivalent to those sent by `snd`.
-
-The default implementation of `continues_on(snd, sched)` is `schedule_from(sched,
-snd)`.
-
-## All senders are typed ## {#design-typed}
-
-All senders must advertise the types they will send when they complete. There
-are many sender adaptors that need this information. Even just transitioning
-from one execution context to another requires temporarily storing the async
-result data so it can be propagated in the new execution context. Doing that
-efficiently requires knowing the type of the data.
-
-The mechanism a sender uses to advertise its completions is the
-`get_completion_signatures` customization point, which takes an environment and
-must return a specialization of the `execution::completion_signatures` class
-template. The template parameters of `execution::completion_signatures` is a
-list of function types that represent the completion operations of the sender.
-for example, the type `execution::set_value_t(size_t, const char*)` indicates
-that the sender can complete successfully by passing a `size_t` and a `const
-char*` to the receiver's `set_value` function.
-
-This proposal includes utilities for parsing and manipulating the list of a
-sender's completion signatures. For instance, `values_of_t` is a template alias
-for accessing a sender's value completions. It takes a sender, an environment,
-and two variadic template template parameters: a tuple-like template and a
-variant-like template. You can get the value completions of `S` and `Env` with
-value_types_of_t<S, Env, tuple-like,
-variant-like>. For example, for a sender that can complete
-successfully with either `Ts...` or `Us...`, `value_types_of_t` would name the type `std::variant,
-std::tuple>`.
-
-## Customization points ## {#design-dispatch}
-
-Earlier versions of this paper used a dispatching technique known as
-`tag_invoke` (see [[P1895R0]]) to allow for customization of basis operations
-and sender algorithms. This technique used private friend functions named
-"`tag_invoke`" that are found by argument-dependent look-up. The `tag_invoke`
-overloads are distinguished from each other by their first argument, which is
-the type of the customization point object being customized. For instance, to
-customize the `execution::set_value` operation, a receiver type might do the
-following:
-
-
-
-The `tag_invoke` technique, although it had its strengths, has been replaced
-with a new (or rather, a very old) technique that uses explicit concept opt-ins
-and named member functions. For instance, the `execution::set_value` operation
-is now customized by defining a member function named `set_value` in the
-receiver type. This technique is more explicit and easier to understand than
-`tag_invoke`. This is what a receiver author would do to customize
-`execution::set_value` now:
-
-
-
-The only exception to this is the customization of queries. There is a need to
-build queryable adaptors that can forward an open and unknowable set of queries
-to some wrapped object. This is done by defining a member function named
-`query` in the adaptor type that takes the query CPO object as its first
-(and usually only) argument. A queryable adaptor might look like this:
-
-
-template<class Query, class Queryable, class... Args>
-concept query_for =
- requires (const Queryable& o, Args&&... args) {
- o.query(Query(), (Args&&) args...);
- };
-
-template<class Allocator = std::allocator<>,
- class Base = execution::empty_env>
-struct with_allocator {
- Allocator alloc{};
- Base base{};
-
- // Forward unknown queries to the wrapped object:
- template<query_for<Base> Query>
- decltype(auto) query(Query q) const {
- return base.query(q);
- }
-
- // Specialize the query for the allocator:
- Allocator query(execution::get_allocator_t) const {
- return alloc;
- }
-};
-
-
-Customization of sender algorithms such as `execution::then` and
-`execution::bulk` are handled differently because they must dispatch based on
-where the sender is executing. See the section on [[#design-customization]] for
-more information.
-
-# Specification # {#spec}
-
-Much of this wording follows the wording of [[P0443R14]].
-
-[[#spec-utilities]] is meant to be a diff relative to the wording of the
-[utilities] clause of [[N4885]].
-
-[[#spec-thread]] is meant to be a diff relative to the wording of the
-[thread] clause of [[N4885]]. This diff applies changes from [[P2175R0]].
-
-[[#spec-execution]] is meant to be added as a new library clause to the working
-draft of C++.
-
-
-
-
-
-
-
-
-
-
-
-
-# Exception handling [except] # {#spec-except}
-
-
-
-
-
-
-
-## Special functions [except.special] ## {#spec-except.special}
-
-
-
-
-### The `std::terminate` function [except.terminate] ### {#spec-except.terminate}
-
-At the end of the bulleted list in the Note in paragraph 1, add
-a new bullet as follows:
-
-
-* when a call to a `wait()`, `wait_until()`, or `wait_for()` function on a
- condition variable (33.7.4, 33.7.5) fails to meet a postcondition.
-
-
-* when a callback invocation exits via an exception when requesting stop on a
- `std::stop_source` or a `std::inplace_stop_source` ([stopsource.mem],
- [stopsource.inplace.mem]), or in the constructor of `std::stop_callback` or
- `std::inplace_stop_callback` ([stopcallback.cons],
- [stopcallback.inplace.cons]) when a callback invocation exits via an
- exception.
-
-* when a `run_loop` object is destroyed that is still in the `running` state
- ([exec.run.loop]).
-
-* when `unhandled_stopped()` is called on a `with_awaitable_senders` object
- ([exec.with.awaitable.senders]) whose continuation is not a handle to a
- coroutine whose promise type has an `unhandled_stopped()` member function.
-
-
-
-
-
-
-
-
-
-
-# Library introduction [library] # {#spec-library}
-
-At the end of [expos.only.entity], add the following:
-
-2. The following are defined for exposition only to aid in the specification of
- the library:
-
-
- namespace std {
- // ...as before...
- }
-
-
-
-3. An object `dst` is said to be decay-copied from a
- subexpression `src` if the type of `dst` is `decay_t`,
- and `dst` is copy-initialized from `src`.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
General [allocator.requirements.general]
-
-At the end of [allocator.requirements.general], add the
-following new paragraph:
-
-98. [*Example 2*: The following is an allocator class template supporting the
- minimal interface that meets the requirements of
- [allocator.requirements.general]:
-
-
-
-### Header `` synopsis [version.syn] ### {#spec-version.syn}
-
-To the `` synopsis, add the following:
-
-
-#define __cpp_lib_semaphore 201907L // also in <semaphore>
-#define __cpp_lib_senders 2024XXL // also in <execution>
-#define __cpp_lib_shared_mutex 201505L // also in <shared_mutex>
-
-
-
-
-
-
-
-
-
-
-
-
-
-# General utilities library [utilities] # {#spec-utilities}
-
-
-
-
-
-
-
-
-
-
-
-
-
-## Function objects [function.objects] ## {#spec-function.objects}
-
-
-
-
-
-### Header `` synopsis [functional.syn] ### {#spec-functional.syn}
-
-At the end of this subclause, insert the following
-declarations into the synopsis within `namespace std`:
-
-
-# Concurrency support library [thread] # {#spec-thread}
-
-
-
-
-
-
-## Stop tokens [thread.stoptoken] ## {#spec-thread.stoptoken}
-
-### Introduction [thread.stoptoken.intro] ### {#spec-thread.stoptoken.intro}
-
-1. Subclause [thread.stoptoken] describes components that can be used to
- asynchronously request that an operation stops execution in a timely manner,
- typically because the result is no longer required. Such a request is called
- a stop request.
-
-2. `stop_source`, `stop_token`, and `stop_callback` implement
- `stoppable-source`, `stoppable_token`, and
- `stoppable-callback-for` are concepts that specify the required
- syntax and semantics of shared ownership ofaccess to
- a stop state. Any `stop_source`,
- `stop_token`, or `stop_callback` object that shares ownership of the same
- stop state is an **associated** `stop_source`, `stop_token`, or
- `stop_callback`, respectively. Any object modeling
- `stoppable-source`, `stoppable_token`, or
- `stoppable-callback-for` that refers to the same stop state is an
- associated `stoppable-source`,
- `stoppable_token`, or `stoppable-callback-for`,
- respectively. The last remaining owner of the stop state
- automatically releases the resources associated with the stop state.
-
-3. An object of a type that models
- stoppable_token can be passed to an operation
- whichthat can either
-
-
-
- * actively poll the token to check if there has been a stop request, or
-
- * register a callback using the stop_callback class
- template whichthat will be called in the event that a
- stop request is made.
-
- A stop request made via a `stop_source`an object whose type
- models `stoppable-source` will be visible to all associated
- stoppable_token and `stop_source`
- `stoppable-source` objects. Once a stop request has been made
- it cannot be withdrawn (a subsequent stop request has no effect).
-
-4. Callbacks registered via a `stop_callback` objectan object
- whose type models `stoppable-callback-for` are called when a
- stop request is first made by any associated `stop_source`
- `stoppable-source` object.
-
-The following paragraph is moved to the specification of
-the new `stoppable-source` concept.
-
-
-
-5. Calls to the functions `request_stop`, `stop_requested`, and `stop_possible`
- do not introduce data races. A call to `request_stop` that returns `true`
- synchronizes with a call to `stop_requested` on an associated `stop_token`
- or `stop_source` object that returns `true`. Registration of a callback
- synchronizes with the invocation of that callback.
-
-
-
-
-
-5. The types `stop_source` and `stop_token` and the class template
- `stop_callback` implement the semantics of shared ownership of a stop state.
- The last remaining owner of the stop state automatically releases the
- resources associated with the stop state.
-
-6. An object of type `inplace_stop_source` is the sole owner of its stop state.
- An object of type `inplace_stop_token` or of a specialization of the class
- template `inplace_stop_callback` does not participate in ownership of its
- associated stop state. They are for use when all uses
- of the associated token and callback objects are known to nest within the
- lifetime of the `inplace_stop_source` object.
-
-
-
-### Header `` synopsis [thread.stoptoken.syn] ### {#spec-thread.stoptoken.syn}
-
-In this subclause, insert the following
-declarations into the `` synopsis:
-
-
-namespace std {
-
// [stoptoken.concepts], stop token concepts
- template<class CallbackFn, class Token, class Initializer = CallbackFn>
- concept stoppable-callback-for = see below; // exposition only
-
- template<class Token>
- concept stoppable_token = see below;
-
- template<class Token>
- concept unstoppable_token = see below;
-
- template<class Source>
- concept stoppable-source = see below; // exposition only
- // 33.3.3, class stop_token
- class stop_token;
-
- // 33.3.4, class stop_source
- class stop_source;
-
- // no-shared-stop-state indicator
- struct nostopstate_t {
- explicit nostopstate_t() = default;
- };
- inline constexpr nostopstate_t nostopstate{};
-
- // 33.3.5, class template stop_callback
- template<class CallbackFn>
- class stop_callback;
-
-
// [stoptoken.never], class never_stop_token
- class never_stop_token;
-
- // [stoptoken.inplace], class inplace_stop_token
- class inplace_stop_token;
-
- // [stopsource.inplace], class inplace_stop_source
- class inplace_stop_source;
-
- // [stopcallback.inplace], class template inplace_stop_callback
- template<class CallbackFn>
- class inplace_stop_callback;
-
- template<class T, class CallbackFn>
- using stop_callback_for_t = T::template callback_type<CallbackFn>;
-}
-
-
-
-Insert the following subclause as a new subclause between
-Header `` synopsis [thread.stoptoken.syn] and Class
-`stop_token` [stoptoken].
-
-
-
-### Stop token concepts [stoptoken.concepts] ### {#spec-stoptoken.concepts}
-
-1. The exposition-only `stoppable-callback-for` concept checks for a
- callback compatible with a given `Token` type.
-
-
-
-2. Let `t` and `u` be distinct, valid objects of type `Token` that reference the
- same logical stop state; let `init` be an expression such that
- `same_as` is `true`; and let `SCB` denote the
- type `stop_callback_for_t`.
-
-3. The concept stoppable-callback-for<CallbackFn, Token,
- Initializer> is modeled only if:
-
- 1. The following concepts are modeled:
-
- * `constructible_from`
- * `constructible_from`
- * `constructible_from`
-
- 2. An object of type `SCB` has an associated callback
- function of type `CallbackFn`. Let `scb` be an object of type `SCB`
- and let `callback_fn` denote `scb`'s associated callback function.
- Direct-non-list-initializing `scb` from arguments `t` and `init` shall
- execute a stoppable callback registration as
- follows:
-
- 1. If `t.stop_possible()` is `true`:
-
- 1. `callback_fn` shall be direct-initialized with `init`.
-
- 2. Construction of `scb` shall only throw exceptions thrown by the
- initialization of `callback_fn` from `init`.
-
- 3. The callback invocation `std::forward(callback_fn)()`
- shall be registered with `t`'s associated stop state as follows:
-
- 1. If `t.stop_requested()` evaluates to `false` at the time of
- registration, the callback invocation is added to the stop
- state's list of callbacks such that
- `std::forward(callback_fn)()` is evaluated if a
- stop request is made on the stop state.
-
- 2. Otherwise, `std::forward(callback_fn)()` shall be
- immediately evaluated on the thread executing `scb`'s
- constructor, and the callback invocation shall not be added
- to the list of callback invocations.
-
- 4. If the callback invocation was added to stop state's list of
- callbacks, `scb` shall be associated with the stop state.
-
- 2. If `t.stop_possible()` is `false`, there is no
- requirement that the initialization of `scb` causes the
- initialization of `callback_fn`.
-
- 3. Destruction of `scb` shall execute a stoppable callback
- deregistration as follows (in order):
-
- 1. If the constructor of `scb` did not register a callback invocation
- with `t`'s stop state, then the stoppable callback
- deregistration shall have no effect other than destroying `callback_fn`
- if it was constructed.
-
- 2. Otherwise, the invocation of `callback_fn` shall be removed from
- the associated stop state.
-
- 3. If `callback_fn` is concurrently executing on another thread
- then the stoppable callback deregistration shall block
- ([defns.block]) until the invocation of `callback_fn` returns
- such that the return from the invocation of `callback_fn`
- strongly happens before ([intro.races]) the destruction of
- `callback_fn`.
-
- 4. If `callback_fn` is executing on the current thread, then the
- destructor shall not block waiting for the return from the
- invocation of `callback_fn`.
-
- 5. A stoppable callback deregistration shall not block on the completion
- of the invocation of some other callback registered with the same
- logical stop state.
-
- 6. The stoppable callback deregistration shall destroy
- `callback_fn`.
-
-4. The `stoppable_token` concept checks for the basic interface of a stop token
- that is copyable and allows polling to see if stop has been requested and
- also whether a stop request is possible. The `unstoppable_token` concept
- checks for a `stoppable_token` type that does not allow stopping.
-
-
-
-5. An object whose type models `stoppable_token` has at most one associated
- logical stop state. A `stoppable_token` object with no associated stop
- state is said to be disengaged.
-
-6. Let `SP` be an evaluation of `t.stop_possible()` that is `false`, and let
- `SR` be an evaluation of `t.stop_requested()` that is `true`.
-
-7. The type `Token` models `stoppable_token` only if:
-
- 1. Any evaluation of `u.stop_possible()` or `u.stop_requested()`
- that happens after ([intro.races]) `SP` is `false`.
-
- 2. Any evaluation of `u.stop_possible()` or `u.stop_requested()`
- that happens after `SR` is `true`.
-
- 3. For any types `CallbackFn` and `Initializer` such that
- stoppable-callback-for<CallbackFn, Token,
- Initializer> is satisfied,
- stoppable-callback-for<CallbackFn, Token,
- Initializer> is modeled.
-
- 4. If `t` is disengaged, evaluations of `t.stop_possible()` and
- `t.stop_requested()` are `false`.
-
- 5. If `t` and `u` reference the same stop state, or if both `t` and `u` are
- disengaged, `t == u` is `true`; otherwise, it is `false`.
-
-8. An object whose type models the exposition-only `stoppable-source`
- concept can be queried whether stop has been requested (`stop_requested`)
- and whether stop is possible (`stop_possible`). It is a factory for
- associated stop tokens (`get_token`), and a stop request can be made on it
- (`request_stop`). It maintains a list of registered stop callback
- invocations that it executes when a stop request is first made.
-
-
-
- 1. An object whose type models `stoppable-source` has at most one
- associated logical stop state. If it has no associated stop state, it is
- said to be disengaged. Let `s` be an object whose type models
- `stoppable-source` and that is disengaged. `s.stop_possible()`
- and `s.stop_requested()` shall return `false`.
-
- 2. Let `t` be an object whose type models `stoppable-source`. If `t` is
- disengaged, `t.get_token()` shall return a disengaged stop token; otherwise,
- it shall return a stop token that is associated with the stop state of `t`.
-
- The following paragraph is moved from the introduction,
- with minor modifications (underlined in green).
-
- 3. Calls to the member functions `request_stop`,
- `stop_requested`, and `stop_possible` and similarly named member
- functions on associated `stoppable_token` objects do not introduce
- data races. A call to `request_stop` that returns `true` synchronizes
- with a call to `stop_requested` on an associated
- stoppable_token or `stop_source`
- `stoppable-source` object that returns `true`. Registration
- of a callback synchronizes with the invocation of that callback.
-
- The following paragraph is taken from
- [[#spec-stopsource.mem]] and modified.
-
- 4. If the `stoppable-source` is disengaged, `request_stop` shall have
- no effect and return `false`. Otherwise, it shall execute a stop request operation on the associated stop state. A
- stop request operation determines whether the stop state has received a
- stop request, and if not, makes a stop request. The determination and
- making of the stop request shall happen atomically, as-if by a
- read-modify-write operation ([intro.races]). If the request was made,
- the stop state's registered callback invocations shall be synchronously
- executed. If an invocation of a callback exits via an exception then
- `terminate` shall be invoked ([except.terminate]). No constraint is placed on the order in which the
- callback invocations are executed. `request_stop` shall return
- `true` if a stop request was made, and `false` otherwise. After a call
- to `request_stop` either a call to `stop_possible` shall return `false`
- or a call to `stop_requested` shall return `true`.
-
- A stop request includes notifying all condition
- variables of type `condition_variable_any` temporarily registered during an
- interruptible wait ([thread.condvarany.intwait]).
-
-
-
-
-Modify subclause [stoptoken] as follows:
-
-
-### Class `stop_token` [stoptoken] ### {#spec-stoptoken}
-
-#### General [stoptoken.general] #### {#spec-stoptoken.general}
-
-1. The class `stop_token` provides an interface for querying whether a stop
- request has been made (`stop_requested`) or can ever be made (`stop_possible`)
- using an associated `stop_source` object ([stopsource]). A `stop_token` can also be
- passed to a `stop_callback` ([stopcallback]) constructor to register a callback to be
- called when a stop request has been made from an associated
- `stop_source`.The class `stop_token` models the concept
- `stoppable_token`. It shares ownership of its stop state, if any, with its
- associated `stop_source` object ([stopsource]) and any `stop_token` objects
- to which it compares equal.
-
-
-namespace std {
- class stop_token {
- public:
-
template<class CallbackFn>
- using callback_type = stop_callback<CallbackFn>;
-
-1. `stop-state` refers to the `stop_token`'s associated stop state. A
- `stop_token` object is disengaged when `stop-state` is empty.
-
-
-
-
-#### Constructors, copy, and assignment [stoptoken.cons] #### {#spec-stoptoken.cons}
-
-
-stop_token() noexcept;
-
-
-1. *Postconditions:* `stop_possible()` is `false` and `stop_requested()` is
- `false`.Because the created `stop_token`
- object can never receive a stop request, no resources are allocated for a
- stop state.
-
-
-stop_token(const stop_token& rhs) noexcept;
-
-
-2. *Postconditions:* `*this == rhs` is `true`. `*this`
- and `rhs` share the ownership of the same stop state, if any.
-
-
-stop_token(stop_token&& rhs) noexcept;
-
-
-3. *Postconditions:* `*this` contains the value of `rhs` prior to the start of
- construction and `rhs.stop_possible()` is `false`.
-
-
-~stop_token();
-
-
-4. *Effects:* Releases ownership of the stop state, if any.
-
-
-
- * `*this` does not have ownership of a stop stateis disengaged, or
-
- * a stop request was not made and there are no associated `stop_source`
- objects; otherwise, `true`.
-
-
-
-The following are covered by the `equality_comparable` and
-`swappable` concepts.
-
-
-
-1. *Returns:* `true` if `lhs` and `rhs` have ownership of the same stop state or
- if both `lhs` and `rhs` do not have ownership of a stop state; otherwise
- `false`.
-
-
-### Class `stop_source` [stopsource] ### {#spec-stopsource}
-
-#### General [stopsource.general] #### {#spec-stopsource.general}
-
-1. The class `stop_source` implements the semantics of making a stop
- request. A stop request made on a `stop_source` object is visible to all
- associated `stop_source` and `stop_token` ([thread.stoptoken]) objects. Once
- a stop request has been made it cannot be withdrawn (a subsequent stop
- request has no effect).
-
-
-namespace std {
- The following definitions are already specified in the `` synopsis:
-
-1. `stop-state` refers to the `stop_source`'s associated stop state. A
- `stop_source` object is disengaged when `stop-state` is empty.
-
-2. `stop_source` models `stoppable-source`, `copyable`,
- `equality_comparable`, and `swappable`.
-
-
-
-1. *Effects:* Initialises `*this` to have ownership
- of`stop-state` with a pointer to a new stop state.
-
-2. *Postconditions:* `stop_possible()` is `true` and `stop_requested()` is `false`.
-
-3. *Throws:* `bad_alloc` if memory cannot be allocated for the stop state.
-
-
-
-explicit stop_source(nostopstate_t) noexcept;
-
-
-4. *Postconditions:* `stop_possible()` is `false` and `stop_requested()` is `false`.
- No resources are allocated for the state.
-
-
-stop_source(const stop_source& rhs) noexcept;
-
-
-5. *Postconditions:* `*this` == rhs is `true`.
- `*this` and `rhs` share the ownership of the same stop state, if any.
-
-
-stop_source(stop_source&& rhs) noexcept;
-
-
-6. *Postconditions:* `*this` contains the value of `rhs` prior to the start of construction and
- `rhs.stop_possible()` is `false`.
-
-
-~stop_source();
-
-
-7. *Effects:* Releases ownership of the stop state, if any.
-
-
-
-2. *Returns:* `stop_token()` if `stop_possible()` is `false`; otherwise a new
- associated `stop_token` object; *i.e.*, its `stop-state` member
- is equal to the `stop-state` member of `*this`.
-
-
-
-4. *Returns:* `true` if `*this` has ownership
- of`stop-state` refers to a stop state that has
- received a stop request; otherwise, `false`.
-
-
-bool request_stop() noexcept;
-
-
-
-5. *Effects:* Executes a stop request operation ([stoptoken.concepts]) on the
- associated stop state, if any.
-
-
-
-
-4. *Effects:* If `*this` does not have ownership of a stop state, returns
- `false`. Otherwise, atomically determines whether the owned stop state has
- received a stop request, and if not, makes a stop request. The determination
- and making of the stop request are an atomic read-modify-write operation
- ([intro.races]). If the request was made, the callbacks registered by
- associated `stop_callback` objects are synchronously called. If an
- invocation of a callback exits via an exception then `terminate` is invoked
- ([except.terminate]).
-
- A stop request includes notifying all condition
- variables of type `condition_variable_any` temporarily registered during an
- interruptible wait ([thread.condvarany.intwait]).
-
-5. *Postconditions:* `stop_possible()` is `false` or `stop_requested()` is
- `true`.
-
-6. *Returns:* `true` if this call made a stop request; otherwise `false`.
-
-
-
-1. *Returns:* `true` if `lhs` and `rhs` have ownership of the same stop state or if
- both `lhs` and `rhs` do not have ownership of a stop state; otherwise `false`.
-
-
-
-2. *Mandates:* `stop_callback` is instantiated with an argument for the template
- parameter CallbackFn that satisfies both `invocable`
- and `destructible`.
-
-
-3. *Preconditions:* `stop_callback` is instantiated with an argument for the
- template parameter `Callback` that models both `invocable` and
- `destructible`.
-
-
-
-
-
-3. *Remarks:* For a type Initializer, if
- stoppable-callback-for<CallbackFn, stop_token, Initializer> is
- satisfied, then stoppable-callback-for<CallbackFn, stop_token,
- Initializer> is modeled. The exposition-only `callback-fn` member is the
- associated callback function ([stoptoken.concepts]) of
- `stop_callback` objects.
-
-
-
-#### Constructors and destructor [stopcallback.cons] #### {#spec-stopcallback.cons}
-
-
-2. *Preconditions:* `Callback` and `C` model `constructible_from`.
-
-
-
-3. *Effects:* Initializes
- callbackcallback-fn with
- std::forward<CInitializer>(cbinit) and
- executes a stoppable callback registration
- ([stoptoken.concepts]). If `st.stop_requested()` is `true`, then
- `std::forward<Callback>(callback)()` is evaluated in the current thread
- before the constructor returns. Otherwise, if `st` has ownership of a stop
- state, acquires shared ownership of that stop state and registers the
- callback with that stop state such that
- `std::forward<Callback>(callback)()` is evaluated by the first call to
- `request_stop()` on an associated `stop_source`. If a callback is
- registered with `st`'s shared stop state, then `*this` acquires shared
- ownership of that stop state.
-
-
-4. *Throws:* Any exception thrown by the initialization of `callback`.
-
-5. *Remarks:* If evaluating `std::forward(callback)()` exits via an
- exception, then `terminate` is invoked ([except.terminate]).
-
-
-
-
-~stop_callback();
-
-
-6. *Effects:* Unregisters the callback from the owned stop state, if any.
- The destructor does not block waiting for the execution of another callback
- registered by an associated `stop_callback`. If `callback` is concurrently
- executing on another thread, then the return from the invocation of
- `callback` strongly happens before ([intro.races]) `callback` is destroyed.
- If `callback` is executing on the current thread, then the destructor does
- not block ([defns.block]) waiting for the return from the invocation of
- `callback`. Releases Executes a stoppable callback deregistration
- ([stoptoken.concepts]) and releases ownership of the stop state, if
- any.
-
-
-
-Insert a new subclause, Class `never_stop_token`
-[stoptoken.never], after subclause Class template `stop_callback`
-[stopcallback], as a new subclause of Stop tokens
-[thread.stoptoken].
-
-
-### Class `never_stop_token` [stoptoken.never] ### {#spec-stoptoken.never}
-
-#### General [stoptoken.never.general] #### {#spec-stoptoken.never.general}
-
-1. The class `never_stop_token` models the `unstoppable_token` concept. It
- provides a stop token interface, but also provides static information that a
- stop is never possible nor requested.
-
-
-
-Insert a new subclause, Class `inplace_stop_token`
-[stoptoken.inplace], after the subclause added above, as a new subclause
-of Stop tokens [thread.stoptoken].
-
-
-### Class `inplace_stop_token` [stoptoken.inplace] ### {#spec-stoptoken.inplace}
-
-#### General [stoptoken.inplace.general] #### {#spec-stoptoken.inplace.general}
-
-1. The class `inplace_stop_token` models the concept `stoppable_token`. It
- references the stop state of its associated `inplace_stop_source` object
- ([stopsource.inplace]), if any.
-
-
-
-#### Member functions [stoptoken.inplace.members] #### {#spec-stoptoken.inplace.members}
-
-
-void swap(inplace_stop_token& rhs) noexcept;
-
-
-1. *Effects*: Exchanges the values of `stop-source` and
- rhs.stop-source.
-
-
-bool stop_requested() const noexcept;
-
-
-1. *Effects*: Equivalent to: return stop-source != nullptr &&
- stop-source->stop_requested();
-
-2. As specified in [basic.life], the behavior of
- `stop_requested()` is undefined unless the call strongly happens before the
- start of the destructor of the associated `inplace_stop_source`, if
- any.
-
-
-bool stop_possible() const noexcept;
-
-
-3. *Returns*: stop-source != nullptr.
-
-4. As specified in [basic.stc.general], the behavior of
- `stop_possible()` is implementation-defined unless the call strongly happens
- before the end of the storage duration of the associated
- `inplace_stop_source` object, if any.
-
-
-
-
-Insert a new subclause, Class `inplace_stop_source`
-[stopsource.inplace], after the subclause added above, as a new subclause
-of Stop tokens [thread.stoptoken].
-
-
-### Class `inplace_stop_source` [stopsource.inplace] ### {#spec-stopsource.inplace}
-
-#### General [stopsource.inplace.general] #### {#spec-stopsource.inplace.general}
-
-1. The class `inplace_stop_source` models `stoppable-source`.
-
-
-
-1. *Effects*: Initializes a new stop state inside `*this`.
-
-2. *Postconditions*: `stop_requested()` is `false`.
-
-#### Members [stopsource.inplace.mem] #### {#spec-stopsource.inplace.mem}
-
-
-
-1. *Returns*: A new associated `inplace_stop_token` object. The `inplace_stop_token`
- object's `stop-source` member is equal to `this`.
-
-
-bool stop_requested() const noexcept;
-
-
-3. *Returns*: `true` if the stop state inside `*this` has received a stop
- request; otherwise, `false`.
-
-
-bool request_stop() noexcept;
-
-
-4. *Effects*: Executes a stop request operation ([stoptoken.concepts]).
-
-5. *Postconditions*: `stop_requested()` is `true`.
-
-
-
-
-Insert a new subclause, Class template
-`inplace_stop_callback` [stopcallback.inplace], after the subclause
-added above, as a new subclause of Stop tokens [thread.stoptoken].
-
-
-
-### Class template `inplace_stop_callback` [stopcallback.inplace] ### {#spec-stopcallback.inplace}
-
-#### General [stopcallback.inplace.general] #### {#spec-stopcallback.inplace.general}
-
-
-
-1. *Mandates*: `CallbackFn` satisfies both `invocable` and `destructible`.
-
-2. *Remarks:* For a type `Initializer`, if
- stoppable-callback-for<CallbackFn, inplace_stop_token,
- Initializer> is satisfied, then
- stoppable-callback-for<CallbackFn, inplace_stop_token,
- Initializer> is modeled. For an `inplace_stop_callback`
- object, the exposition-only `callback-fn` member is its associated
- callback function ([stoptoken.concepts]).
-
-#### Constructors and destructor [stopcallback.inplace.cons] #### {#spec-stopcallback.inplace.cons}
-
-
-
-# Execution control library [exec] # {#spec-execution}
-
-## General [exec.general] ## {#spec-execution.general}
-
-1. This Clause describes components supporting execution of function objects
- [function.objects].
-
-2. The following subclauses describe the requirements, concepts, and components
- for execution control primitives as summarized in Table 1.
-
-
-
Table N: Execution control library summary [tab:execution.summary]
-
-4. This clause makes use of the following exposition-only entities:
-
- 1. For a subexpression expr, let
- MANDATE-NOTHROW(expr)
- be expression-equivalent to expr.
-
- * Mandates:noexcept(expr) is
- true.
-
- 2.
-
- 3. For function types `F1` and `F2` denoting `R1(Args1...)` and
- `R2(Args2...)` respectively, MATCHING-SIG(F1, F2) is
- `true` if and only if `same_as` is
- `true`.
-
- 4. For a subexpression `err`, let `Err` be `decltype((err))` and let
- AS-EXCEPT-PTR(err) be:
-
- 1. `err` if `decay_t` denotes the type `exception_ptr`.
-
- - *Mandates:* `err != exception_ptr()` is `true`.
-
- 2. Otherwise, `make_exception_ptr(system_error(err))` if `decay_t`
- denotes the type `error_code`.
-
- 3. Otherwise, `make_exception_ptr(err)`.
-
-## Queries and queryables [exec.queryable] ## {#spec-execution.queryable}
-
-### General [exec.queryable.general] ### {#spec-execution.queryable.general}
-
-1. A queryable object is a read-only collection of
- key/value pairs where each key is a customization point object known as a
- query object. A query is an invocation of a query object with a queryable
- object as its first argument and a (possibly empty) set of additional
- arguments. A query imposes syntactic and semantic
- requirements on its invocations.
-
-2. Let `q` be a query object, let `args` be a (possibly empty) pack of
- subexpressions, let `env` be a subexpression that refers to a queryable
- object `o` of type `O`, and let `cenv` be a subexpression referring to `o`
- such that `decltype((cenv))` is `const O&`. The expression `q(env, args...)`
- is equal to ([concepts.equality]) the expression `q(cenv, args...)`.
-
-3. The type of a query expression can not be `void`.
-
-4. The expression `q(env, args...)` is equality-preserving
- ([concepts.equality]) and does not modify the query object or the arguments.
-
-5. If the expression `env.query(q, args...)` is well-formed, then it is
- expression-equivalent to `q(env, args...)`.
-
-6. Unless otherwise specified, the result of a query is valid as long as the
- queryable object is valid.
-
-### `queryable` concept [exec.queryable.concept] ### {#spec-execution.queryable.concept}
-
-
-
-1. The exposition-only `queryable` concept specifies the constraints on
- the types of queryable objects.
-
-2. Let `env` be an object of type `Env`. The type `Env` models
- `queryable` if for each callable object `q` and a pack of
- subexpressions `args`, if `requires { q(env, args...) }` is `true` then
- `q(env, args...)` meets any semantic requirements imposed by `q`.
-
-## Asynchronous operations [async.ops] ## {#spec-execution-async.ops}
-
-1. An execution resource is a program entity that manages
- a (possibly dynamic) set of execution agents
- ([thread.req.lockable.general]), which it uses to execute parallel work on
- behalf of callers. [*Example 1*: The currently active thread, a
- system-provided thread pool, and uses of an API associated with an external
- hardware accelerator are all examples of execution resources. -- *end
- example*] Execution resources execute asynchronous operations. An execution
- resource is either valid or invalid.
-
-2. An asynchronous operation is a distinct unit of
- program execution that:
-
- 1. ... is explicitly created.
-
- 2. ... can be explicitly started once at
- most.
-
- 3. ... once started, eventually completes exactly once with a (possibly empty) set
- of result datums and in exactly one of three dispositions: success, failure, or
- cancellation.
-
- - A successful completion, also known as a value
- completion, can have an arbitrary number of result datums.
-
- - A failure completion, also known as an error
- completion, has a single result datum.
-
- - A cancellation completion, also known as a stopped completion, has no result datum.
-
- An asynchronous operation's async result is its
- disposition and its (possibly empty) set of result datums.
-
- 4. ... can complete on a different execution resource than the execution
- resource on which it started.
-
- 5. ... can create and start other asynchronous operations called child operations. A child operation is an
- asynchronous operation that is created by the parent operation and, if
- started, completes before the parent operation completes. A parent operation is the asynchronous operation that
- created a particular child operation.
-
- An asynchronous operation can in fact execute
- synchronously; that is, it can complete during the execution of its start
- operation on the thread of execution that started it.
-
-3. An asynchronous operation has associated state known as its operation state.
-
-
-
-4. An asynchronous operation has an associated environment. An environment is a queryable object ([exec.queryable])
- representing the execution-time properties of the operation's caller. The
- caller of an asynchronous operation is
- its parent operation or the function that created it. An asynchronous
- operation's operation state owns the operation's environment.
-
-5. An asynchronous operation has an associated receiver. A receiver is an aggregation of three handlers for the three
- asynchronous completion dispositions: a value completion handler for a value
- completion, an error completion handler for an error completion, and a
- stopped completion handler for a stopped completion. A receiver has an
- associated environment. An asynchronous operation's operation state owns the
- operation's receiver. The environment of an asynchronous operation is equal
- to its receiver's environment.
-
-6. For each completion disposition, there is a completion
- function. A completion function is a customization point object
- ([customization.point.object]) that accepts an asynchronous operation's
- receiver as the first argument and the result datums of the asynchronous
- operation as additional arguments. The value completion function invokes the
- receiver's value completion handler with the value result datums; likewise
- for the error completion function and the stopped completion function. A
- completion function has an associated type known as its completion tag that is the unqualified type of the
- completion function. A valid invocation of a completion function is called a
- completion operation.
-
-7. The lifetime of an
- asynchronous operation, also known as the operation's async lifetime, begins when its start operation begins
- executing and ends when its completion operation begins executing. If the
- lifetime of an asynchronous operation's associated operation state ends
- before the lifetime of the asynchronous operation, the behavior is
- undefined. After an asynchronous operation executes a completion operation,
- its associated operation state is invalid. Accessing any part of an invalid
- operation state is undefined behavior.
-
-8. An asynchronous operation shall not execute a completion operation before its
- start operation has begun executing. After its start operation has begun
- executing, exactly one completion operation shall execute. The lifetime of an
- asynchronous operation's operation state can end during the execution of the
- completion operation.
-
-9. A sender is a factory for one or more asynchronous
- operations. Connecting a sender and a
- receiver creates an asynchronous operation. The asynchronous operation's
- associated receiver is equal to the receiver used to create it, and its
- associated environment is equal to the environment associated with the
- receiver used to create it. The lifetime of an asynchronous operation's
- associated operation state does not depend on the lifetimes of either the
- sender or the receiver from which it was created. A sender is started when
- it is connected to a receiver and the resulting asynchronous operation is
- started. A sender's async result is the async result of the asynchronous
- operation created by connecting it to a receiver. A sender sends its results by way of the asynchronous operation(s)
- it produces, and a receiver receives
- those results. A sender is either valid or invalid; it becomes invalid
- when its parent sender (see below) becomes invalid.
-
-10. A scheduler is an abstraction of an execution
- resource with a uniform, generic interface for scheduling work onto that
- resource. It is a factory for senders whose asynchronous operations execute
- value completion operations on an execution agent belonging to the
- scheduler's associated execution resource. A schedule-expression obtains such a sender from a
- scheduler. A schedule sender is the result of a
- schedule expression. On success, an asynchronous operation produced by a
- schedule sender executes a value completion operation with an empty set of
- result datums. Multiple schedulers can refer to the same execution resource.
- A scheduler can be valid or invalid. A scheduler becomes invalid when the
- execution resource to which it refers becomes invalid, as do any schedule
- senders obtained from the scheduler, and any operation states obtained from
- those senders.
-
-11. An asynchronous operation has one or more associated completion schedulers
- for each of its possible dispositions. A completion
- scheduler is a scheduler whose associated execution resource is used
- to execute a completion operation for an asynchronous operation. A value
- completion scheduler is a scheduler on which an asynchronous operation's
- value completion operation can execute. Likewise for error completion
- schedulers and stopped completion schedulers.
-
-12. A sender has an associated queryable object ([exec.queryable]) known as its
- attributes that describes various characteristics of
- the sender and of the asynchronous operation(s) it produces. For each
- disposition, there is a query object for reading the associated completion
- scheduler from a sender's attributes; i.e., a value completion scheduler
- query object for reading a sender's value completion scheduler, etc. If a
- completion scheduler query is well-formed, the returned completion scheduler
- is unique for that disposition for any asynchronous operation the sender
- creates. A schedule sender is required to have a value completion scheduler
- attribute whose value is equal to the scheduler that produced the schedule
- sender.
-
-13. A completion signature is a function type that
- describes a completion operation. An asynchronous operation has a finite set
- of possible completion signatures corresponding to the completion operations
- that the asynchronous operation potentially evaluates ([basic.def.odr]). For
- a completion function `set`, receiver
- `rcvr`, and pack of arguments `args`,
- let `c` be the completion operation set(rcvr,
- args...), and let `F` be the function type
- decltype(auto(set))(decltype((args))...).
- A completion signature `Sig` is associated with `c` if and only if
- MATCHING-SIG(Sig, F) is `true` ([exec.general]). Together,
- a sender type and an environment type `Env` determine the set of completion
- signatures of an asynchronous operation that results from connecting the
- sender with a receiver that has an environment of type `Env`. The type of the receiver does not affect an asynchronous
- operation's completion signatures, only the type of the receiver's
- environment.
-
-14. A sender algorithm is a function that takes and/or
- returns a sender. There are three categories of sender algorithms:
-
- * A sender factory is a function that takes
- non-senders as arguments and that returns a sender.
-
- * A sender adaptor is a function that constructs and
- returns a parent sender from a set of one or more
- child senders and a (possibly
- empty) set of additional arguments. An asynchronous operation created by
- a parent sender is a parent operation to the child operations created by
- the child senders.
-
- * A sender consumer is a function that takes one or
- more senders and a (possibly empty) set of additional arguments, and
- whose return type is not the type of a sender.
-
-## Header `` synopsis [exec.syn] ## {#spec-execution.syn}
-
-
-
-1. The exposition-only type variant-or-empty<Ts...> is
- defined as follows:
-
- 1. If `sizeof...(Ts)` is greater than zero,
- variant-or-empty<Ts...> denotes
- `variant` where `Us...` is the pack `decay_t...` with
- duplicate types removed.
-
- 2. Otherwise, variant-or-empty<Ts...> denotes the
- exposition-only class type:
-
-
-
-2. For types `Sndr` and `Env`,
- single-sender-value-type<Sndr, Env> is
- an alias for:
-
- 1. `value_types_of_t`
- if that type is well-formed,
-
- 2. Otherwise, `void` if `value_types_of_t`
- is `variant>` or `variant<>`,
-
- 3. Otherwise, value_types_of_t<Sndr, Env, decayed-tuple,
- type_identity_t> if that type is well-formed,
-
- 4. Otherwise, single-sender-value-type<Sndr, Env>
- is ill-formed.
-
-3. The exposition-only concept `single-sender` is defined as follows:
-
-
-
-## Queries [exec.queries] ## {#spec-execution.queries}
-
-### `forwarding_query` [exec.fwd.env] ### {#spec-execution.forwarding_query}
-
-1. `forwarding_query` asks a query object whether it should be forwarded
- through queryable adaptors.
-
-2. The name `forwarding_query` denotes a query object. For some query
- object `q` of type `Q`, `forwarding_query(q)` is expression-equivalent
- to:
-
- 1. MANDATE-NOTHROW(q.query(forwarding_query)) if that
- expression is well-formed.
-
- * Mandates: The expression above has type `bool` and is a core
- constant expression if `q` is a core constant expression.
-
- 2. Otherwise, `true` if `derived_from` is
- `true`.
-
- 3. Otherwise, `false`.
-
-### `get_allocator` [exec.get.allocator] ### {#spec-execution.get_allocator}
-
-1. `get_allocator` asks a queryable object for its associated allocator.
-
-2. The name `get_allocator` denotes a query object. For a subexpression `env`,
- `get_allocator(env)` is expression-equivalent to
- MANDATE-NOTHROW(as_const(env).query(get_allocator)).
-
- * Mandates: If the expression above is well-formed, its type
- satisfies `simple-allocator` ([allocator.requirements.general]).
-
-3. `forwarding_query(get_allocator)` is a core constant expression and has value
- `true`.
-
-
-### `get_stop_token` [exec.get.stop.token] ### {#spec-execution.get_stop_token}
-
-1. `get_stop_token` asks a queryable object for an associated stop token.
-
-2. The name `get_stop_token` denotes a query object. For a subexpression `env`,
- `get_stop_token(env)` is expression-equivalent to:
-
- 1. MANDATE-NOTHROW(as_const(env).query(get_stop_token))
- if that expression is well-formed.
-
- * Mandates: The type of the expression above satisfies
- `stoppable_token`.
-
- 2. Otherwise, `never_stop_token{}`.
-
-3. `forwarding_query(get_stop_token)` is a core constant
- expression and has value `true`.
-
-### `execution::get_env` [exec.get.env] ### {#spec-execution.environment.get_env}
-
-1. `execution::get_env` is a customization point object. For a subexpression
- `o`, `execution::get_env(o)` is expression-equivalent to:
-
- 1. MANDATE-NOTHROW(as_const(o).get_env()) if that expression is
- well-formed.
-
- * Mandates: The type of the expression above satisfies
- `queryable` ([exec.queryable]).
-
- 2. Otherwise, `empty_env{}`.
-
-2. The value of `get_env(o)` shall be valid while `o` is valid.
-
-3. When passed a sender object, `get_env` returns the
- sender's associated attributes. When passed a receiver, `get_env` returns the
- receiver's associated execution environment.
-
-### `execution::get_domain` [exec.get.domain] ### {#spec-execution.get_domain}
-
-1. `get_domain` asks a queryable object for its associated execution domain tag.
-
-2. The name `get_domain` denotes a query object. For a subexpression `env`,
- `get_domain(env)` is expression-equivalent to
- MANDATE-NOTHROW(as_const(env).query(get_domain)).
-
-3. `forwarding_query(execution::get_domain)` is a core constant
- expression and has value `true`.
-
-### `execution::get_scheduler` [exec.get.scheduler] ### {#spec-execution.get_scheduler}
-
-1. `get_scheduler` asks a queryable object for its associated scheduler.
-
-2. The name `get_scheduler` denotes a query object. For a
- subexpression `env`, `get_scheduler(env)` is expression-equivalent to
- MANDATE-NOTHROW(as_const(env).query(get_scheduler)).
-
- * Mandates: If the expression above is well-formed, its type
- satisfies `scheduler`.
-
-3. `forwarding_query(execution::get_scheduler)` is a core constant
- expression and has value `true`.
-
-### `execution::get_delegation_scheduler` [exec.get.delegation.scheduler] ### {#spec-execution.get_delegation_scheduler}
-
-1. `get_delegation_scheduler` asks a queryable object for a scheduler that can be
- used to delegate work to for the purpose of forward progress delegation
- ([intro.progress]).
-
-2. The name `get_delegation_scheduler` denotes a query object. For a
- subexpression `env`, `get_delegation_scheduler(env)` is expression-equivalent to
- MANDATE-NOTHROW(as_const(env).query(get_delegation_scheduler)).
-
- * Mandates: If the expression above is well-formed, its type
- satisfies `scheduler`.
-
-3. `forwarding_query(execution::get_delegation_scheduler)` is a core
- constant expression and has value `true`.
-
-### `execution::get_forward_progress_guarantee` [exec.get.forward.progress.guarantee] ### {#spec-execution.get_forward_progress_guarantee}
-
-
-
-1. `get_forward_progress_guarantee` asks a scheduler about the forward progress
- guarantee of execution agents created by that scheduler's associated
- execution resource ([intro.progress]).
-
-2. The name `get_forward_progress_guarantee` denotes a query object. For a
- subexpression `sch`, let `Sch` be `decltype((sch))`. If `Sch` does not
- satisfy `scheduler`, `get_forward_progress_guarantee` is ill-formed.
- Otherwise, `get_forward_progress_guarantee(sch)` is expression-equivalent
- to:
-
- 1. MANDATE-NOTHROW(as_const(sch).query(get_forward_progress_guarantee)),
- if that expression is well-formed.
-
- * Mandates: The type of the expression above is
- `forward_progress_guarantee`.
-
- 2. Otherwise, `forward_progress_guarantee::weakly_parallel`.
-
-3. If `get_forward_progress_guarantee(sch)` for some scheduler `sch` returns
- `forward_progress_guarantee::concurrent`, all execution agents created by
- that scheduler's associated execution resource shall provide the concurrent
- forward progress guarantee. If it returns
- `forward_progress_guarantee::parallel`, all such execution agents shall
- provide at least the parallel forward progress guarantee.
-
-### `execution::get_completion_scheduler` [exec.completion.scheduler] ### {#spec-execution.get_completion_scheduler}
-
-1. get_completion_scheduler<completion-tag> obtains the
- completion scheduler associated with a completion tag from a sender's
- attributes.
-
-2. The name `get_completion_scheduler` denotes a query object template. For a
- subexpression `q`, the expression
- get_completion_scheduler<completion-tag>(q) is
- ill-formed if `completion-tag` is not one of `set_value_t`,
- `set_error_t`, or `set_stopped_t`. Otherwise,
- get_completion_scheduler<completion-tag>(q) is
- expression-equivalent to
- MANDATE-NOTHROW(as_const(q).query(get_completion_scheduler<completion-tag>)).
-
- * Mandates: If the expression above is well-formed, its type
- satisfies `scheduler`.
-
-3. Let `completion-fn` be a completion function ([async.ops]); let
- `completion-tag` be the associated completion tag of
- `completion-fn`; let `args` be a pack of subexpressions; and let
- `sndr` be a subexpression such that `sender` is `true` and
- get_completion_scheduler<completion-tag>(get_env(sndr))
- is well-formed and denotes a scheduler `sch`. If an asynchronous operation
- created by connecting `sndr` with a receiver `rcvr` causes the evaluation of
- completion-fn(rcvr, args...), the behavior is undefined
- unless the evaluation happens on an execution agent that belongs to `sch`'s
- associated execution resource.
-
-4. The expression forwarding_query(get_completion_scheduler<completion-tag>)
- is a core constant expression and has value `true`.
-
-## Schedulers [exec.sched] ## {#spec-execution.schedulers}
-
-1. The `scheduler` concept defines the requirements of a scheduler type
- ([async.ops]). `schedule` is a customization point object that accepts a
- scheduler. A valid invocation of `schedule` is a schedule-expression.
-
-
-
-2. Let `Sch` be the type of a scheduler and let `Env` be the type of an
- execution environment for which `sender_in, Env>` is
- satisfied. Then sender-in-of<schedule_result_t<Sch>,
- Env> shall be modeled.
-
-3. None of a scheduler's copy constructor, destructor, equality comparison, or
- `swap` member functions shall exit via an exception. None of these member
- functions, nor a scheduler type's `schedule` function, shall introduce data
- races as a result of potentially concurrent ([intro.races]) invocations of
- those functions from different threads.
-
-4. For any two values `sch1` and `sch2` of some scheduler type `Sch`, `sch1 ==
- sch2` shall return `true` only if both `sch1` and `sch2` share the same
- associated execution resource.
-
-5. For a given scheduler expression `sch`, the expression
- `get_completion_scheduler(get_env(schedule(sch)))` shall
- compare equal to `sch`.
-
-6. For a given scheduler expression `sch`, if the expression `get_domain(sch)`
- is well-formed, then the expression `get_domain(get_env(schedule(sch)))`
- is also well-formed and has the same type.
-
-7. A scheduler type's destructor shall not block pending completion of any
- receivers connected to the sender objects returned from `schedule`. The ability to wait for completion of submitted function
- objects can be provided by the associated execution resource of the
- scheduler.
-
-## Receivers [exec.recv] ## {#spec-execution.receivers}
-
-### Receiver concepts [exec.recv.concepts] ### {#spec-execution.receiver_concepts}
-
-1. A receiver represents the continuation of an asynchronous operation. The
- `receiver` concept defines the requirements for a receiver type
- ([async.ops]). The `receiver_of` concept defines the requirements for a
- receiver type that is usable as the first argument of a set of completion
- operations corresponding to a set of completion signatures. The `get_env`
- customization point object is used to access a receiver's associated
- environment.
-
-
-
-2. Class types that are marked `final` do not model the `receiver` concept.
-
-3. Let `rcvr` be a receiver and let `op_state` be an operation state associated
- with an asynchronous operation created by connecting `rcvr` with a sender.
- Let `token` be a stop token equal to `get_stop_token(get_env(rcvr))`.
- `token` shall remain valid for the duration of the asynchronous operation's
- lifetime ([async.ops]). This means that, unless it
- knows about further guarantees provided by the type of `rcvr`, the
- implementation of `op_state` can not use `token` after it executes a
- completion operation. This also implies that any stop callbacks registered
- on `token` must be destroyed before the invocation of the completion
- operation.
-
-### `execution::set_value` [exec.set.value] ### {#spec-execution.receivers.set_value}
-
-1. `set_value` is a value completion function ([async.ops]). Its associated
- completion tag is `set_value_t`. The expression `set_value(rcvr, vs...)` for
- a subexpression `rcvr` and pack of subexpressions `vs` is ill-formed if `rcvr`
- is an lvalue or an rvalue of const type. Otherwise, it is expression-equivalent to
- MANDATE-NOTHROW(rcvr.set_value(vs...)).
-
-### `execution::set_error` [exec.set.error] ### {#spec-execution.receivers.set_error}
-
-1. `set_error` is an error completion function ([async.ops]). Its associated completion tag is
- `set_error_t`. The expression `set_error(rcvr, err)` for some subexpressions
- `rcvr` and `err` is ill-formed if `rcvr` is an lvalue or an rvalue of const
- type. Otherwise, it is expression-equivalent to
- MANDATE-NOTHROW(rcvr.set_error(err)).
-
-### `execution::set_stopped` [exec.set.stopped] ### {#spec-execution.receivers.set_stopped}
-
-1. `set_stopped` is a stopped completion function ([async.ops]). Its associated
- completion tag is `set_stopped_t`. The expression `set_stopped(rcvr)` for a
- subexpression `rcvr` is ill-formed if `rcvr` is an lvalue or an rvalue of
- `const` type. Otherwise, it is expression-equivalent to
- MANDATE-NOTHROW(rcvr.set_stopped()).
-
-## Operation states [exec.opstate] ## {#spec-execution.opstate}
-
-1. The `operation_state` concept defines the requirements of an operation state
- type ([async.ops]).
-
-
-
-2. If an `operation_state` object is destroyed during the lifetime of its
- asynchronous operation ([async.ops]), the behavior is undefined. The `operation_state` concept does not impose requirements
- on any operations other than destruction and `start`, including copy and
- move operations. Invoking any such operation on an object whose type models
- `operation_state` can lead to undefined behavior.
-
-3. The program is ill-formed if it performs a copy or move construction or
- assigment operation on an operation state object created by connecting a
- library-provided sender.
-
-### `execution::start` [exec.opstate.start] ### {#spec-execution.opstate.start}
-
-1. The name `start` denotes a customization point object that starts
- ([async.ops]) the asynchronous operation associated with the operation state
- object. For a subexpression `op`, the expression `start(op)` is ill-formed
- if `op` is an rvalue. Otherwise, it is expression-equivalent to
- MANDATE-NOTHROW(op.start()).
-
-2. If `op.start()` does not start ([async.ops]) the asynchronous operation
- associated with the operation state `op`, the behavior of calling
- `start(op)` is undefined.
-
-## Senders [exec.snd] ## {#spec-execution.senders}
-
-### General [exec.snd.general] ### {#spec-execution.senders.general}
-
-1. For the purposes of this subclause, a sender is an object whose type
- satisfies the `sender` concept ([async.ops]).
-
-2. Subclauses [exec.factories] and [exec.adapt] define customizable algorithms
- that return senders. Each algorithm has a default implementation. Let `sndr`
- be the result of an invocation of such an algorithm or an object equal to
- the result ([concepts.equality]), and let `Sndr` be `decltype((sndr))`. Let `rcvr`
- be a receiver of type `Rcvr` with associated environment `env` of type `Env` such that
- `sender_to` is `true`. For the default implementation of the
- algorithm that produced `sndr`, connecting `sndr` to `rcvr` and starting the
- resulting operation state ([async.ops]) necessarily results in the potential
- evaluation ([basic.def.odr]) of a set of completion operations whose first
- argument is a subexpression equal to `rcvr`. Let `Sigs` be a pack of
- completion signatures corresponding to this set of completion operations.
- Then the type of the expression `get_completion_signatures(sndr, env)` is a
- specialization of the class template `completion_signatures`
- ([exec.utils.cmplsigs]), the set of whose template arguments is `Sigs`. If a
- user-provided implementation of the algorithm that produced `sndr` is
- selected instead of the default, any completion signature that is in the set
- of types denoted by `completion_signatures_of_t` and that is not
- part of `Sigs` shall correspond to error or stopped completion operations,
- unless otherwise specified.
-
-3. This subclause makes use of the following exposition-only entities.
-
- 1. For a queryable object `env`, FWD-ENV(env) is an
- expression whose type satisfies `queryable` such that for a query object
- `q` and a pack of subexpressions `as`, the expression
- FWD-ENV(env).query(q, as...) is ill-formed if
- `forwarding_query(q)` is `false`; otherwise, it is expression-equivalent
- to `env.query(q, as...)`.
-
- 2. For a query object `q` and a subexpression `v`, MAKE-ENV(q,
- v) is an expression `env` whose type satisfies `queryable` such
- that the result of `env.query(q)` has a value equal to `v`
- ([concepts.equality]). Unless otherwise stated, the object to which
- `env.query(q)` refers remains valid while `env` remains valid.
-
- 3. For two queryable objects `env1` and `env2`, a query object `q` and a
- pack of subexpressions `as`, JOIN-ENV(env1, env2) is
- an expression `env3` whose type satisfies `queryable` such that
- `env3.query(q, as...)` is expression-equivalent to:
-
- - `env1.query(q, as...)` if that expression is well-formed,
-
- - otherwise, `env2.query(q, as...)` if that expression is
- well-formed,
-
- - otherwise, `env3.query(q, as...)` is ill-formed.
-
- 4. The results of `FWD-ENV`, `MAKE-ENV`, and `JOIN-ENV`
- can be context-dependent; i.e., they can evaluate to expressions with
- different types and value categories in different contexts for the same
- arguments.
-
- 5. For a scheduler `sch`, SCHED-ATTRS(sch) is an
- expression `o1` whose type satisfies `queryable` such that
- o1.query(get_completion_scheduler<Tag>) is a
- expression with the same type and value as `sch` where `Tag` is
- one of `set_value_t` or `set_stopped_t`, and such that
- o1.query(get_domain) is expression-equivalent to
- sch.query(get_domain). SCHED-ENV(sch)
- is an expression `o2` whose type satisfies `queryable` such that
- o1.query(get_scheduler) is a prvalue with the same type and
- value as `sch`, and such that o2.query(get_domain) is
- expression-equivalent to sch.query(get_domain).
-
- 6. For two subexpressions `rcvr` and `expr`, SET-VALUE(rcvr,
- expr) is expression-equivalent to `(expr, set_value(std::move(rcvr)))` if
- the type of `expr` is `void`; otherwise, `set_value(std::move(rcvr), expr)`.
- TRY-EVAL(rcvr, expr) is equivalent to:
-
-
-
- if `expr` is potentially-throwing; otherwise, `expr`.
- TRY-SET-VALUE(rcvr, expr) is
- TRY-EVAL(rcvr, SET-VALUE(rcvr, expr)) except
- that `rcvr` is evaluated only once.
-
- 7.
- template<class Default = default_domain, class Sndr>
- constexpr auto completion-domain(const Sndr& sndr) noexcept;
-
-
- 1. COMPL-DOMAIN(T) is the type of the
- expression `get_domain(get_completion_scheduler(get_env(sndr)))`.
-
- 2. *Effects:* If all of the types
- COMPL-DOMAIN(set_value_t),
- COMPL-DOMAIN(set_error_t), and
- COMPL-DOMAIN(set_stopped_t) are ill-formed,
- completion-domain<Default>(sndr) is a
- default-constructed prvalue of type `Default`. Otherwise, if they
- all share a common type ([meta.trans.other]) (ignoring those types
- that are ill-formed), then
- completion-domain<Default>(sndr) is a
- default-constructed prvalue of that type. Otherwise,
- completion-domain<Default>(sndr) is
- ill-formed.
-
- 8.
-
- 1. Let `e` be the expression `Tag()(env)` if that
- expression is well-formed; otherwise, it is
- `static_cast(std::forward(value))`.
-
- 2. Returns: `e`.
-
- 3. Remarks: The expression in the `noexcept` clause is
- `noexcept(e)`.
-
- 9.
-
- 1. Effects: Equivalent to: return Domain();
- where `Domain` is the decayed type of the first of the
- following expressions that is well-formed:
-
- - `get_domain(get_env(sndr))`
-
- - completion-domain(sndr)
-
- - `default_domain()`
-
- 10.
- template<class Sndr, class Env>
- constexpr auto get-domain-late(const Sndr& sndr, const Env& env) noexcept;
-
-
- 1. Effects: Equivalent to:
-
- - If sender-for<Sndr, continues_on_t> is `true`,
- then return Domain(); where `Domain` is
- the type of the following expression:
-
-
-
- The `continues_on` algorithm works in tandem
- with `schedule_from` ([exec.schedule.from])) to give scheduler
- authors a way to customize both how to transition onto
- (`continues_on`) and off of (`schedule_from`) a given execution
- context. Thus, `continues_on` ignores the domain of the predecessor
- and uses the domain of the destination scheduler to select a
- customization, a property that is unique to `continues_on`. That is
- why it is given special treatment here.
-
- - Otherwise, return Domain(); where `Domain` is
- the first of the following expressions that is well-formed and
- whose type is not `void`:
-
- - `get_domain(get_env(sndr))`
-
- - completion-domain<void>(sndr)
-
- - `get_domain(env)`
-
- - `get_domain(get_scheduler(env))`
-
- - `default_domain()`.
-
- 11.
- template<callable Fun>
- requires is_nothrow_move_constructible_v<Fun>
- struct emplace-from { // exposition only
- Fun fun; // exposition only
- using type = call-result-t<Fun>;
-
- constexpr operator type() && noexcept(nothrow-callable<Fun>) {
- return std::move(fun)();
- }
-
- constexpr type operator()() && noexcept(nothrow-callable<Fun>) {
- return std::move(fun)();
- }
- };
-
-
- 1. `emplace-from` is used to emplace
- non-movable types into `tuple`, `optional`, `variant`, and similar
- types.
-
- 12.
- template<class T0, class T1, ... class Tn>
- struct product-type { // exposition only
- T0t0; // exposition only
- T1t1; // exposition only
- ...
- Tntn; // exposition only
-
- template<size_t I, class Self>
- constexpr decltype(auto) get(this Self&& self) noexcept; // exposition only
-
- template<class Self, class Fn>
- constexpr decltype(auto) apply(this Self&& self, Fn&& fn) // exposition only
- noexcept(see below);
- };
-
-
- 1. `product-type` is presented here in
- pseudo-code form for the sake of exposition. It can be approximated in
- standard C++ with a `tuple`-like implementation that takes care
- to keep the type an aggregate that can be used as the initializer of a
- structured binding declaration.
-
- 2. An expression of type
- `product-type` is usable as the initializer of a
- structured binding declaration [dcl.struct.bind].
-
- 3.
- template<size_t I, class Self>
- constexpr decltype(auto) get(this Self&& self) noexcept;
-
-
- 2. *Requires:* The expression in the `return` statement above is
- well-formed.
-
- 4. *Remarks:* The expression in the `noexcept` clause is `true` if the
- `return` statement above is not potentially throwing; otherwise, `false`.
-
- 14.
- template<class Tag, class Data = see below, class... Child>
- constexpr auto make-sender(Tag tag, Data&& data, Child&&... child);
-
-
- 1. *Mandates:* The following expressions are `true`:
-
- - `semiregular`
-
- - movable-value<Data>
-
- - `(sender &&...)`
-
- 2. *Returns:* A prvalue of type basic-sender<Tag,
- decay_t<Data>, decay_t<Child>...> that has been
- direct-list-initialized with the forwarded arguments, where
- `basic-sender` is the following exposition-only
- class template except as noted below:
-
-
- namespace std::execution {
- template<class Tag>
- concept completion-tag = // exposition only
- same_as<Tag, set_value_t> || same_as<Tag, set_error_t> || same_as<Tag, set_stopped_t>;
-
- template<template<class...> class T, class... Args>
- concept valid-specialization = requires { typename T<Args...>; }; // exposition only
-
- struct default-impls { // exposition only
- static constexpr auto get-attrs = see below;
- static constexpr auto get-env = see below;
- static constexpr auto get-state = see below;
- static constexpr auto start = see below;
- static constexpr auto complete = see below;
- };
-
- template<class Tag>
- struct impls-for : default-impls {}; // exposition only
-
- template<class Sndr, class Rcvr> // exposition only
- using state-type = decay_t<call-result-t<
- decltype(impls-for<tag_of_t<Sndr>>::get-state), Sndr, Rcvr&>>;
-
- template<class Index, class Sndr, class Rcvr> // exposition only
- using env-type = call-result-t<
- decltype(impls-for<tag_of_t<Sndr>>::get-env), Index,
- state-type<Sndr, Rcvr>&, const Rcvr&>;
-
- template<class Sndr, size_t I = 0>
- using child-type = decltype(declval<Sndr>().template get<I+2>()); // exposition only
-
- template<class Sndr>
- using indices-for = remove_reference_t<Sndr>::indices-for; // exposition only
-
- template<class Sndr, class Rcvr>
- struct basic-state { // exposition only
- basic-state(Sndr&& sndr, Rcvr&& rcvr) noexcept(see below)
- : rcvr(std::move(rcvr))
- , state(impls-for<tag_of_t<Sndr>>::get-state(std::forward<Sndr>(sndr), rcvr)) { }
-
- Rcvr rcvr; // exposition only
- state-type<Sndr, Rcvr> state; // exposition only
- };
-
- template<class Sndr, class Rcvr, class Index>
- requires valid-specialization<env-type, Index, Sndr, Rcvr>
- struct basic-receiver { // exposition only
- using receiver_concept = receiver_t;
-
- using tag-t = tag_of_t<Sndr>; // exposition only
- using state-t = state-type<Sndr, Rcvr>; // exposition only
- static constexpr const auto& complete = impls-for<tag-t>::complete; // exposition only
-
- template<class... Args>
- requires callable<decltype(complete), Index, state-t&, Rcvr&, set_value_t, Args...>
- void set_value(Args&&... args) && noexcept {
- complete(Index(), op->state, op->rcvr, set_value_t(), std::forward<Args>(args)...);
- }
-
- template<class Error>
- requires callable<decltype(complete), Index, state-t&, Rcvr&, set_error_t, Error>
- void set_error(Error&& err) && noexcept {
- complete(Index(), op->state, op->rcvr, set_error_t(), std::forward<Error>(err));
- }
-
- void set_stopped() && noexcept
- requires callable<decltype(complete), Index, state-t&, Rcvr&, set_stopped_t> {
- complete(Index(), op->state, op->rcvr, set_stopped_t());
- }
-
- auto get_env() const noexcept -> env-type<Index, Sndr, Rcvr> {
- return impls-for<tag-t>::get-env(Index(), op->state, op->rcvr);
- }
-
- basic-state<Sndr, Rcvr>* op; // exposition only
- };
-
- constexpr auto connect-all = see below; // exposition only
-
- template<class Sndr, class Rcvr>
- using connect-all-result = call-result-t< // exposition only
- decltype(connect-all), basic-state<Sndr, Rcvr>*, Sndr, indices-for<Sndr>>;
-
- template<class Sndr, class Rcvr>
- requires valid-specialization<state-type, Sndr, Rcvr> &&
- valid-specialization<connect-all-result, Sndr, Rcvr>
- struct basic-operation : basic-state<Sndr, Rcvr> { // exposition only
- using operation_state_concept = operation_state_t;
- using tag-t = tag_of_t<Sndr>; // exposition only
-
- connect-all-result<Sndr, Rcvr> inner-ops; // exposition only
-
- basic-operation(Sndr&& sndr, Rcvr&& rcvr) noexcept(see below) // exposition only
- : basic-state<Sndr, Rcvr>(std::forward<Sndr>(sndr), std::move(rcvr))
- , inner-ops(connect-all(this, std::forward<Sndr>(sndr), indices-for<Sndr>()))
- {}
-
- void start() & noexcept {
- auto& [...ops] = inner-ops;
- impls-for<tag-t>::start(this->state, this->rcvr, ops...);
- }
- };
-
- template<class Sndr, class Env>
- using completion-signatures-for = see below; // exposition only
-
- template<class Tag, class Data, class... Child>
- struct basic-sender : product-type<Tag, Data, Child...> { // exposition only
- using sender_concept = sender_t;
- using indices-for = index_sequence_for<Child...>; // exposition only
-
- decltype(auto) get_env() const noexcept {
- auto& [_, data, ...child] = *this;
- return impls-for<Tag>::get-attrs(data, child...);
- }
-
- template<decays-to<basic-sender> Self, receiver Rcvr>
- auto connect(this Self&& self, Rcvr rcvr) noexcept(see below)
- -> basic-operation<Self, Rcvr> {
- return {std::forward<Self>(self), std::move(rcvr)};
- }
-
- template<decays-to<basic-sender> Self, class Env>
- auto get_completion_signatures(this Self&& self, Env&& env) noexcept
- -> completion-signatures-for<Self, Env> {
- return {};
- }
- };
- }
-
-
- 1. *Remarks:* The default template argument for the `Data` template parameter
- denotes an unspecified empty trivially copyable class type that models
- `semiregular`.
-
- 2. It is unspecified whether a specialization of `basic-sender` is
- an aggregate.
-
- 3. An expression of type `basic-sender` is usable as the initializer of a
- structured binding declaration [dcl.struct.bind].
-
- 4. The expression in the `noexcept` clause of the constructor of `basic-state`
- is:
-
-
-
- 1. *Requires:* The expression in the `return` statement is well-formed.
-
- 2. *Remarks:* The expression in the `noexcept` clause is `true` if
- the `return` statement is not potentially throwing; otherwise, `false`.
-
- 6. The expression in the `noexcept` clause of the constructor of `basic-operation`
- is:
-
-
-
- 13. For a subexpression `sndr` let `Sndr` be `decltype((sndr))`. Let
- `rcvr` be a receiver with an associated environment of type `Env`
- such that `sender_in` is `true`.
- completion-signatures-for<Sndr, Env> denotes
- a specialization of `completion_signatures`, the set of whose
- template arguments correspond to the set of completion operations
- that are potentially evaluated as a result of starting ([async.ops])
- the operation state that results from connecting `sndr` and `rcvr`.
- When `sender_in` is `false`, the type denoted by
- completion-signatures-for<Sndr, Env>, if any,
- is not a specialization of `completion_signatures`.
-
- *Recommended practice:* When `sender_in` is `false`,
- implementations are encouraged to use the type denoted by
- completion-signatures-for<Sndr, Env> to
- communicate to users why.
-
- 15.
- template<sender Sndr, queryable Env>
- constexpr auto write-env(Sndr&& sndr, Env&& env); // exposition only
-
-
- 1. `write-env` is an exposition-only sender adaptor that, when
- connected with a receiver `rcvr`, connects the adapted sender with a
- receiver whose execution environment is the result of joining the
- `queryable` argument `env` to the result of `get_env(rcvr)`.
-
- 2. Let `write-env-t` be an exposition-only empty class type.
-
- 3. *Returns:* make-sender(write-env-t(), std::forward<Env>(env), std::forward<Sndr>(sndr)).
-
- 4. *Remarks:* The exposition-only class template
- `impls-for` ([exec.snd.general]) is specialized for
- `write-env-t` as follows:
-
-
-
-### Sender concepts [exec.snd.concepts] ### {#spec-execution.snd.concepts}
-
-1. The `sender` concept defines the requirements for a sender type
- ([async.ops]). The `sender_in` concept defines the requirements for a sender
- type that can create asynchronous operations given an associated environment
- type. The `sender_to` concept defines the requirements for a sender type
- that can connect with a specific receiver type. The `get_env` customization
- point object is used to access a sender's associated attributes. The
- `connect` customization point object is used to connect ([async.ops]) a
- sender and a receiver to produce an operation state.
-
-
-
-
-2. Given a subexpression `sndr`, let `Sndr` be `decltype((sndr))` and let `rcvr`
- be a receiver with an associated environment whose type is `Env`.
- A completion operation is a permissible completion for `Sndr` and `Env` if its
- completion signature appears in the argument list of the specialization of
- `completion_signatures` denoted by `completion_signatures_of_t`.
- `Sndr` and `Env` model `sender_in` if all the completion
- operations that are potentially evaluated by connecting `sndr` to `rcvr` and
- starting the resulting operation state are permissible completions for
- `Sndr` and `Env`.
-
-3. A type models the exposition-only concept
- `valid-completion-signatures` if it denotes a specialization
- of the `completion_signatures` class template.
-
-4. The exposition-only concepts `sender-of` and
- `sender-in-of` define the requirements for a sender
- type that completes with a given unique set of value result types.
-
-
-
-5. Let `sndr` be an expression such that `decltype((sndr))` is `Sndr`. The type
- `tag_of_t` is as follows:
-
- - If the declaration `auto&& [tag, data, ...children] = sndr;` would be
- well-formed, `tag_of_t` is an alias for `decltype(auto(tag))`.
-
- - Otherwise, `tag_of_t` is ill-formed.
-
-6. Let `sender-for` be an exposition-only concept defined as follows:
-
-
-
-7. For a type `T`, SET-VALUE-SIG(T) denotes the type
- `set_value_t()` if `T` is *cv* `void`; otherwise, it denotes the type
- `set_value_t(T)`.
-
-8. Library-provided sender types:
-
- - Always expose an overload of a member `connect` that accepts an rvalue
- sender.
-
- - Only expose an overload of a member `connect` that accepts an lvalue
- sender if they model `copy_constructible`.
-
-### Awaitable helpers [exec.awaitables] ### {#spec.exec-awaitables}
-
-1. The sender concepts recognize awaitables as senders. For [exec], an
- awaitable is an expression that would be
- well-formed as the operand of a `co_await` expression within a given
- context.
-
-2. For a subexpression `c`, let GET-AWAITER(c, p) be
- expression-equivalent to the series of transformations and conversions
- applied to `c` as the operand of an *await-expression* in a coroutine,
- resulting in lvalue `e` as described by [expr.await], where `p`
- is an lvalue referring to the coroutine's promise, which has type `Promise`. This includes the invocation of the promise type's
- `await_transform` member if any, the invocation of the `operator co_await`
- picked by overload resolution if any, and any necessary implicit
- conversions and materializations.
-
- I have opened
- [cwg#250](https://github.com/cplusplus/CWG/issues/250) to give these
- transformations a term-of-art so we can more easily refer to it here.
-
-3. Let `is-awaitable` be the following exposition-only
- concept:
-
-
-
- await-suspend-result<T> is `true` if and only if one
- of the following is `true`:
-
- - `T` is `void`, or
- - `T` is `bool`, or
- - `T` is a specialization of `coroutine_handle`.
-
-4. For a subexpression `c` such that `decltype((c))` is type `C`, and
- an lvalue `p` of type `Promise`, await-result-type<C, Promise>
- denotes the type decltype(GET-AWAITER(c, p).await_resume()).
-
-5. Let `with-await-transform` be the exposition-only class template:
-
-
-
- Specializations of `env-promise`
- are only used for the purpose of type computation; its members need not be
- defined.
-
-### `execution::default_domain` [exec.domain.default] ### {#spec-execution.default_domain}
-
-
-
-1. Let `e` be the expression
- `tag_of_t().transform_sender(std::forward(sndr), env...)` if that
- expression is well-formed; otherwise, `std::forward(sndr)`.
-
-2. Returns:`e`.
-
-3. Remarks: The exception specification is equivalent to noexcept(e).
-
-
-
-4. Let `e` be the expression
- `tag_of_t().transform_env(std::forward(sndr),
- std::forward(env))` if that expression is well-formed; otherwise,
- `static_cast(std::forward(env))`.
-
-5. Mandates:noexcept(e) is `true`.
-
-6. Returns:`e`.
-
-
-
-7. Let `e` be the expression
- `Tag().apply_sender(std::forward(sndr), std::forward(args)...)`.
-
-8. Constraints:`e` is a well-formed expression.
-
-9. Returns:`e`.
-
-10. Remarks: The exception specification is equivalent to
- noexcept(e).
-
-### `execution::transform_sender` [exec.snd.transform] ### {#spec-execution.sender_transform}
-
-
-
-1. Let `transformed-sndr` be the expression
- `dom.transform_sender(std::forward(sndr), env...)` if that expression
- is well-formed; otherwise,
- `default_domain().transform_sender(std::forward(sndr), env...)`. Let
- `final-sndr` be the expression
- `transformed-sndr` if `transformed-sndr`
- and `sndr` have the same type ignoring *cv* qualifiers; otherwise, it is the
- expression transform_sender(dom, transformed-sndr,
- env...).
-
-2. Returns:`final-sndr`.
-
-3. Remarks: The exception specification is equivalent to
- noexcept(final-sndr).
-
-### `execution::transform_env` [exec.snd.transform.env] ### {#spec-execution.env_transform}
-
-
-
-1. Let `e` be the expression `dom.apply_sender(Tag(),
- std::forward(sndr), std::forward(args)...)` if that expression
- is well-formed; otherwise, `default_domain().apply_sender(Tag(),
- std::forward(sndr), std::forward(args)...)`.
-
-2. Constraints: The expression `e` is well-formed.
-
-3. Returns:`e`.
-
-4. Remarks: The exception specification is equivalent to
- noexcept(e).
-
-### `execution::get_completion_signatures` [exec.getcomplsigs] ### {#spec-execution.getcomplsigs}
-
-1. `get_completion_signatures` is a customization point object. Let `sndr` be an
- expression such that `decltype((sndr))` is `Sndr`, and let `env` be an
- expression such that `decltype((env))` is `Env`. Let `new_sndr` be the
- expression transform_sender(decltype(get-domain-late(sndr,
- env)){}, sndr, env), and let `NewSndr` be `decltype((new_sndr))`.
- Then `get_completion_signatures(sndr, env)` is expression-equivalent to
- (void(sndr), void(env), CS()) except that `void(sndr)`
- and `void(env)` are indeterminately sequenced, where `CS` is:
-
- 1. `decltype(new_sndr.get_completion_signatures(env))` if that
- type is well-formed,
-
- 2. Otherwise, `remove_cvref_t::completion_signatures` if that type is well-formed,
-
- 3. Otherwise, if is-awaitable<NewSndr, env-promise<Env>>
- is `true`, then:
-
-
-
- 4. Otherwise, `CS` is ill-formed.
-
-2. Let `rcvr` be an rvalue whose type `Rcvr` models `receiver`, and let `Sndr`
- be the type of a sender such that `sender_in>` is
- `true`. Let `Sigs...` be the
- template arguments of the `completion_signatures` specialization named by
- `completion_signatures_of_t>`. Let `CSO` be
- a completion function. If sender `Sndr` or its operation state cause the
- expression CSO(rcvr, args...) to be potentially evaluated
- ([basic.def.odr]) then there shall be a signature `Sig` in `Sigs...` such
- that
- MATCHING-SIG(decayed-typeof<CSO>(decltype(args)...),
- Sig) is `true` ([exec.general]).
-
-### `execution::connect` [exec.connect] ### {#spec-execution.senders.connect}
-
-1. `connect` connects ([async.ops]) a sender with a receiver.
-
-2. The name `connect` denotes a customization point object. For subexpressions
- `sndr` and `rcvr`, let `Sndr` be `decltype((sndr))` and `Rcvr` be
- `decltype((rcvr))`, let `new_sndr` be the expression
- transform_sender(decltype(get-domain-late(sndr,
- get_env(rcvr))){}, sndr, get_env(rcvr)), and let `DS` and `DR` be
- `decay_t` and `decay_t`, respectively.
-
-3. Let `connect-awaitable-promise` be the following exposition-only class:
-
-
-
-6. The expression `connect(sndr, rcvr)` is
- expression-equivalent to:
-
- 1. `new_sndr.connect(rcvr)` if that expression is well-formed.
-
- * Mandates: The type of the expression above satisfies
- `operation_state`.
-
- 2. Otherwise, connect-awaitable(new_sndr, rcvr).
-
- 3. *Mandates:* `sender && receiver` is `true`.
-
-
-### Sender factories [exec.factories] ### {#spec-execution.senders.factories}
-
-#### `execution::schedule` [exec.schedule] #### {#spec-execution.senders.schedule}
-
-1. `schedule` obtains a schedule sender ([async.ops]) from a scheduler.
-
-2. The name `schedule` denotes a customization point object. For a
- subexpression `sch`, the expression `schedule(sch)` is expression-equivalent to
- `sch.schedule()`.
-
-
- 1. If the expression get_completion_scheduler<set_value_t>(
- get_env(sch.schedule())) == sch is ill-formed or evaluates
- to `false`, the behavior of calling `schedule(sch)` is undefined.
-
- 2. Mandates: The type of `sch.schedule()` satisfies `sender`.
-
-#### `execution::just`, `execution::just_error`, `execution::just_stopped` [exec.just] #### {#spec-execution.senders.just}
-
-1. `just`, `just_error`, and `just_stopped` are sender factories whose
- asynchronous operations complete synchronously in their start operation
- with a value completion operation, an error completion operation, or a
- stopped completion operation respectively.
-
-2. The names `just`, `just_error`, and `just_stopped` denote customization
- point objects. Let `just-cpo` be one of `just`,
- `just_error`, or `just_stopped`. For a pack of subexpressions `ts`, let `Ts`
- be the pack of types `decltype((ts))`. The expression
- just-cpo(ts...) is ill-formed if:
-
- - (movable-value<Ts> &&...) is `false`, or
-
- - `just-cpo` is `just_error` and `sizeof...(ts) == 1`
- is `false`, or
-
- - `just-cpo` is `just_stopped` and `sizeof...(ts) == 0`
- is `false`;
-
- Otherwise, it is expression-equivalent to make-sender(just-cpo,
- product-type{ts...}).
-
-3. For `just`, `just_error`, and `just_stopped`, let `set-cpo`
- be `set_value`, `set_error`, and `set_stopped` respectively. The
- exposition-only class template `impls-for`
- ([exec.snd.general]) is specialized for `just-cpo` as
- follows:
-
-
-
-#### `execution::read_env` [exec.read.env] #### {#spec-execution.senders.read.env}
-
-1. `read_env` is a sender factory for a sender whose asynchronous operation
- completes synchronously in its start operation with a value completion
- result equal to a value read from the receiver's associated environment.
-
-2. `read_env` is a customization point object. For some query object `q`,
- the expression `read_env(q)` is expression-equivalent to
- make-sender(read_env, q).
-
-3. The exposition-only class template `impls-for` ([exec.snd.general])
- is specialized for `read_env` as follows:
-
-
-
-### Sender adaptors [exec.adapt] ### {#spec-execution.senders.adapt}
-
-#### General [exec.adapt.general] #### {#spec-execution.senders.adapt.general}
-
-1. [exec.adapt] specifies a set of sender adaptors.
-
-2. The bitwise inclusive OR operator is overloaded for the purpose of creating sender
- chains. The adaptors also support function call syntax with equivalent
- semantics.
-
-3. Unless otherwise specified:
-
- 1. A sender adaptor is prohibited from causing observable effects, apart
- from moving and copying its arguments, before the returned sender is
- connected with a receiver using `connect`, and `start` is called on the
- resulting operation state.
-
- 2. A parent sender ([async.ops]) with a single child
- sender `sndr` has an associated attribute object equal to
- FWD-ENV(get_env(sndr)) ([exec.fwd.env]).
-
- 3. A parent sender with more than one child sender has an
- associated attributes object equal to empty_env{}.
-
- 4. When a parent sender is connected to a receiver `rcvr`, any receiver used
- to connect a child sender has an associated environment equal to
- FWD-ENV(get_env(rcvr)).
-
- These requirements apply to any function that is selected by the implementation
- of the sender adaptor.
-
-4. If a sender returned from a sender adaptor specified in [exec.adapt] is
- specified to include `set_error_t(Err)` among its set of completion signatures
- where `decay_t` denotes the type `exception_ptr`, but the implementation
- does not potentially evaluate an error completion operation with an
- `exception_ptr` argument, the implementation is allowed to omit the
- `exception_ptr` error completion signature from the set.
-
-#### Sender adaptor closure objects [exec.adapt.objects] #### {#spec-execution.senders.adaptor.objects}
-
-1. A pipeable sender adaptor closure object is a function object that
- accepts one or more `sender` arguments and returns a `sender`. For a pipeable sender
- adaptor closure object `c` and an expression `sndr` such that
- `decltype((sndr))` models `sender`, the following expressions are equivalent
- and yield a `sender`:
-
-
- c(sndr)
- sndr | c
-
-
- Given an additional pipeable sender adaptor closure object `d`, the
- expression `c | d` produces another pipeable sender adaptor closure object
- `e`:
-
- `e` is a perfect forwarding call wrapper ([func.require]) with the following
- properties:
-
- - Its target object is an object `d2` of type `decltype(auto(d))`
- direct-non-list-initialized with `d`.
-
- - It has one bound argument entity, an object `c2` of type
- `decltype(auto(c))` direct-non-list-initialized with `c`.
-
- - Its call pattern is `d2(c2(arg))`, where `arg` is the argument used in a
- function call expression of `e`.
-
- The expression `c | d` is well-formed if and only if the initializations of
- the state entities ([func.def]) of `e` are all well-formed.
-
-2. An object `t` of type `T` is a pipeable sender adaptor closure object if `T`
- models `derived_from>`, `T` has no other base
- classes of type `sender_adaptor_closure` for any other type `U`, and `T`
- does not satisfy `sender`.
-
-3. The template parameter `D` for `sender_adaptor_closure` can be an incomplete
- type. Before any expression of type cv D appears as an
- operand to the `|` operator, `D` shall be complete and model
- `derived_from>`. The behavior of an expression
- involving an object of type cv D as an operand to the
- `|` operator is undefined if overload resolution selects a program-defined
- `operator|` function.
-
-4. A pipeable sender adaptor object is a customization point object that
- accepts a `sender` as its first argument and returns a `sender`.
-
-5. If a pipeable sender adaptor object accepts only one argument, then it is a
- pipeable sender adaptor closure object.
-
-6. If a pipeable sender adaptor object `adaptor` accepts more than one argument,
- then let `sndr` be an expression such that `decltype((sndr))` models
- `sender`, let `args...` be arguments such that `adaptor(sndr, args...)` is a
- well-formed expression as specified below, and let `BoundArgs` be a pack
- that denotes `decltype(auto(args))...`. The expression `adaptor(args...)`
- produces a pipeable sender adaptor closure object `f` that is a perfect
- forwarding call wrapper with the following properties:
-
- - Its target object is a copy of `adaptor`.
-
- - Its bound argument entities `bound_args` consist of objects of types
- `BoundArgs...` direct-non-list-initialized with
- `std::forward(args)...`, respectively.
-
- - Its call pattern is `adaptor(rcvr, bound_args...)`, where `rcvr` is the
- argument used in a function call expression of `f`.
-
- The expression `adaptor(args...)` is well-formed if and only if the
- initializations of the bound argument entities of the result, as specified
- above, are all well-formed.
-
-#### `execution::starts_on` [exec.starts.on] #### {#spec-execution.senders.adapt.starts.on}
-
-1. `starts_on` adapts an input sender into a sender that will start on an execution
- agent belonging to a particular scheduler's associated execution resource.
-
-2. The name `starts_on` denotes a customization point object. For subexpressions
- `sch` and `sndr`, if `decltype((sch))` does not satisfy `scheduler`, or
- `decltype((sndr))` does not satisfy `sender`, `starts_on(sch, sndr)` is ill-formed.
-
-3. Otherwise, the expression `starts_on(sch, sndr)` is expression-equivalent to:
-
-
-
- except that `sch` is evaluated only once.
-
-4. Let `out_sndr` and `env` be subexpressions such that `OutSndr` is `decltype((out_sndr))`. If
- sender-for<OutSndr, starts_on_t> is `false`, then the expressions
- `starts_on.transform_env(out_sndr, env)` and `starts_on.transform_sender(out_sndr, env)` are ill-formed;
- otherwise:
-
- - `starts_on.transform_env(out_sndr, env)` is equivalent to:
-
-
-
-5. Let `out_sndr` be a subexpression denoting a sender returned from `starts_on(sch, sndr)`
- or one equal to such, and let `OutSndr` be the type `decltype((out_sndr))`. Let
- `out_rcvr` be a subexpression denoting a receiver that has an environment of
- type `Env` such that `sender_in` is `true`. Let `op` be an lvalue
- referring to the operation state that results from connecting `out_sndr` with
- `out_rcvr`. Calling `start(op)` shall start `sndr` on an execution agent of the
- associated execution resource of `sch`. If scheduling onto `sch` fails, an error
- completion on `out_rcvr` shall be executed on an unspecified execution agent.
-
-#### `execution::continues_on` [exec.continues.on] #### {#spec-execution.senders.adapt.continues.on}
-
-1. `continues_on` adapts a sender into one that completes on the specified scheduler.
-
-2. The name `continues_on` denotes a pipeable sender adaptor object. For
- subexpressions `sch` and `sndr`, if `decltype((sch))` does not satisfy
- `scheduler`, or `decltype((sndr))` does not satisfy `sender`,
- `continues_on(sndr, sch)` is ill-formed.
-
-3. Otherwise, the expression `continues_on(sndr, sch)` is expression-equivalent to:
-
-
-
- except that `sndr` is evaluated only once.
-
-4. The exposition-only class template `impls-for` is specialized
- for `continues_on_t` as follows:
-
-
-
-5. Let `sndr` and `env` be subexpressions such that `Sndr` is `decltype((sndr))`. If
- sender-for<Sndr, continues_on_t> is `false`, then the expression
- `continues_on.transform_sender(sndr, env)` is ill-formed; otherwise, it
- is equal to:
-
-
-
- This causes the `continues_on(sndr, sch)` sender to become
- `schedule_from(sch, sndr)` when it is connected with a receiver whose
- execution domain does not customize `continues_on`.
-
-6. Let `out_sndr` be a subexpression denoting a sender returned from
- `continues_on(sndr, sch)` or one equal to such, and let `OutSndr` be the type
- `decltype((out_sndr))`. Let `out_rcvr` be a subexpression denoting a
- receiver that has an environment of type `Env` such that `sender_in` is `true`. Let `op` be an lvalue referring to the operation state that
- results from connecting `out_sndr` with `out_rcvr`. Calling `start(op)`
- shall start `sndr` on the current execution agent and execute completion
- operations on `out_rcvr` on an execution agent of the execution resource
- associated with `sch`. If scheduling onto `sch` fails, an error completion
- on `out_rcvr` shall be executed on an unspecified execution agent.
-
-#### `execution::schedule_from` [exec.schedule.from] #### {#spec-execution.senders.adaptors.schedule_from}
-
-1. `schedule_from` schedules work dependent on the completion of a sender onto a
- scheduler's associated execution resource. `schedule_from` is not meant to be used in user code; it is
- used in the implementation of `continues_on`.
-
-2. The name `schedule_from` denotes a customization point object. For some
- subexpressions `sch` and `sndr`, let `Sch` be `decltype((sch))` and `Sndr` be
- `decltype((sndr))`. If `Sch` does not satisfy `scheduler`, or `Sndr` does not
- satisfy `sender`, `schedule_from(sch, sndr)` is ill-formed.
-
-3. Otherwise, the expression `schedule_from(sch, sndr)` is expression-equivalent
- to:
-
-
-
- except that `sch` is evaluated only once.
-
-4. The exposition-only class template `impls-for`
- ([exec.snd.general]) is specialized for `schedule_from_t` as
- follows:
-
-
- namespace std::execution {
- template<>
- struct impls-for<schedule_from_t> : default-impls {
- static constexpr auto get-attrs = see below;
- static constexpr auto get-state = see below;
- static constexpr auto complete = see below;
- };
- }
-
-
- 1. The member impls-for<schedule_from_t>::get-attrs is initialized
- with a callable object equivalent to the following lambda:
-
-
-
- 1. Objects of the local class `state-type` can be used to
- initialize a structured binding.
-
- 2. Let `Sigs` be a pack of the arguments to the
- `completion_signatures` specialization named by
- completion_signatures_of_t<child-type<Sndr>, env_of_t<Rcvr>>. Let
- `as-tuple` be an alias template that transforms a
- completion signature `Tag(Args...)` into the `tuple`
- specialization decayed-tuple<Tag, Args...>.
- Then `variant_t` denotes the type
- variant<monostate, as-tuple<Sigs>...>,
- except with duplicate types removed.
-
- 3. `receiver_t` is an alias for the following exposition-only
- class:
-
-
-
- 4. The expression in the `noexcept` clause of the lambda is `true` if
- the construction of the returned `state-type` object is not
- potentially throwing; otherwise, `false`.
-
- 3. The member impls-for<schedule_from_t>::complete
- is initialized with a callable object equivalent to the following lambda:
-
-
-
-5. Let `out_sndr` be a subexpression denoting a sender returned from
- `schedule_from(sch, sndr)` or one equal to such, and let `OutSndr` be the type
- `decltype((out_sndr))`. Let `out_rcvr` be a subexpression denoting a
- receiver that has an environment of type `Env` such that `sender_in` is `true`. Let `op` be an lvalue referring to the operation state that
- results from connecting `out_sndr` with `out_rcvr`. Calling `start(op)`
- shall start `sndr` on the current execution agent and execute completion
- operations on `out_rcvr` on an execution agent of the execution resource
- associated with `sch`. If scheduling onto `sch` fails, an error completion
- on `out_rcvr` shall be executed on an unspecified execution agent.
-
-#### `execution::on` [exec.on] #### {#spec-execution.senders.adaptors.on}
-
-1. The `on` sender adaptor has two forms:
-
- - `on(sch, sndr)`, which starts a sender `sndr` on an execution agent
- belonging to a scheduler `sch`'s associated execution resource and that,
- upon `sndr`'s completion, transfers execution back to the execution
- resource on which the `on` sender was started.
-
- - `on(sndr, sch, closure)`, which upon completion of a sender `sndr`,
- transfers execution to an execution agent belonging to a scheduler `sch`'s
- associated execution resource, then executes a sender adaptor closure
- `closure` with the async results of the sender, and that then transfers
- execution back to the execution resource on which `sndr` completed.
-
-2. The name `on` denotes a pipeable sender adaptor object. For subexpressions
- `sch` and `sndr`, `on(sch, sndr)` is ill-formed if any of the following
- is true:
-
- * `decltype((sch))` does not satisfy `scheduler`, or
-
- * `decltype((sndr))` does not satisfy `sender` and `sndr` is not
- a pipeable sender adaptor closure object ([exec.adapt.objects]), or
-
- * `decltype((sndr))` satisfies `sender` and `sndr` is also
- a pipeable sender adaptor closure object.
-
-3. Otherwise, if `decltype((sndr))` satisfies `sender`, the expression `on(sch,
- sndr)` is expression-equivalent to:
-
-
-
- except that `sch` is evaluated only once.
-
-4. For subexpressions `sndr`, `sch`, and `closure`, if `decltype((sch))` does
- not satisfy `scheduler`, or `decltype((sndr))` does not satisfy `sender`, or
- `closure` is not a pipeable sender adaptor closure object
- ([exec.adapt.objects]), the expression `on(sndr, sch, closure)` is
- ill-formed; otherwise, it is expression-equivalent to:
-
-
-
- except that `sndr` is evaluated only once.
-
-5. Let `out_sndr` and `env` be subexpressions, let `OutSndr` be
- `decltype((out_sndr))`, and let `Env` be `decltype((env))`. If
- sender-for<OutSndr, on_t> is `false`, then the
- expressions `on.transform_env(out_sndr, env)` and
- `on.transform_sender(out_sndr, env)` are ill-formed; otherwise:
-
- 1. Let `not-a-scheduler` be an unspecified empty class type, and
- let `not-a-sender` be the exposition-only type:
-
-
- struct not-a-sender {
- using sender_concept = sender_t;
-
- auto get_completion_signatures(auto&&) const {
- return see below;
- }
- };
-
-
- where the member function `get_completion_signatures` returns an
- object of a type that is not a specialization of the
- `completion_signatures` class template.
-
- 2. The expression `on.transform_env(out_sndr, env)` has effects equivalent to:
-
-
-
- 4. Recommended practice: Implementations should use the return type
- of not-a-sender::get_completion_signatures to
- inform users that their usage of `on` is incorrect because there is no
- available scheduler onto which to restore execution.
-
-6. Let `out_sndr` be a subexpression denoting a sender returned from
- `on(sch, sndr)` or one equal to such, and let `OutSndr` be the type
- `decltype((out_sndr))`. Let `out_rcvr` be a subexpression denoting a
- receiver that has an environment of type `Env` such that `sender_in` is `true`. Let `op` be an lvalue referring to the operation state that
- results from connecting `out_sndr` with `out_rcvr`. Calling `start(op)`
- shall:
-
- 1. Remember the current scheduler, `get_scheduler(get_env(rcvr))`.
-
- 2. Start `sndr` on an execution agent belonging to `sch`'s associated
- execution resource.
-
- 4. Upon `sndr`'s completion, transfer execution back to the execution
- resource associated with the scheduler remembered in step 1.
-
- 5. Forward `sndr`'s async result to `out_rcvr`.
-
- If any scheduling operation fails, an error completion on `out_rcvr` shall
- be executed on an unspecified execution agent.
-
-7. Let `out_sndr` be a subexpression denoting a sender returned from
- `on(sndr, sch, closure)` or one equal to such, and let `OutSndr` be the type
- `decltype((out_sndr))`. Let `out_rcvr` be a subexpression denoting a
- receiver that has an environment of type `Env` such that `sender_in` is `true`. Let `op` be an lvalue referring to the operation state that
- results from connecting `out_sndr` with `out_rcvr`. Calling `start(op)`
- shall:
-
- 1. Remember the current scheduler, which is the first of the following
- expressions that is well-formed:
-
- - `get_completion_scheduler(get_env(sndr))`
-
- - `get_scheduler(get_env(rcvr))`
-
- 2. Start `sndr` on the current execution agent.
-
- 3. Upon `sndr`'s completion, transfer execution to an agent owned by `sch`'s
- associated execution resource.
-
- 4. Forward `sndr`'s async result as if by connecting and starting a
- sender `closure(S)`, where `S` is a sender that completes
- synchronously with `sndr`'s async result.
-
- 5. Upon completion of the operation started in step 4, transfer execution
- back to the execution resource associated with the scheduler remembered
- in step 1 and forward the operation's async result to `out_rcvr`.
-
- If any scheduling operation fails, an error completion on `out_rcvr` shall
- be executed on an unspecified execution agent.
-
-#### `execution::then`, `execution::upon_error`, `execution::upon_stopped` [exec.then] #### {#spec-execution.senders.adaptor.then}
-
-1. `then` attaches an invocable as a continuation for an input sender's value
- completion operation. `upon_error` and `upon_stopped` do the same for the
- error and stopped completion operations respectively, sending the result
- of the invocable as a value completion.
-
-2. The names `then`, `upon_error`, and `upon_stopped` denote pipeable sender
- adaptor objects. Let the expression `then-cpo` be one of `then`,
- `upon_error`, or `upon_stopped`. For subexpressions `sndr` and `f`, if
- `decltype((sndr))` does not satisfy `sender`, or `decltype((f))` does not
- satisfy `movable-value`,
- then-cpo(sndr, f) is ill-formed.
-
-3. Otherwise, the expression then-cpo(sndr, f) is
- expression-equivalent to:
-
-
-
- except that `sndr` is evaluated only once.
-
-4. For `then`, `upon_error`, and `upon_stopped`, let `set-cpo`
- be `set_value`, `set_error`, and `set_stopped` respectively. The
- exposition-only class template `impls-for`
- ([exec.snd.general]) is specialized for `then-cpo` as follows:
-
-
-
-5. The expression then-cpo(sndr, f) has undefined behavior
- unless it returns a sender `out_sndr` that:
-
- 1. Invokes `f` or a copy of such with the value, error, or stopped result
- datums of `sndr` for `then`, `upon_error`, and `upon_stopped`
- respectively, using the result value of `f` as `out_sndr`'s value
- completion, and
-
- 2. Forwards all other completion operations unchanged.
-
-#### `execution::let_value`, `execution::let_error`, `execution::let_stopped`, [exec.let] #### {#spec-execution.senders.adapt.let}
-
-1. `let_value`, `let_error`, and `let_stopped` transform a sender's value,
- error, and stopped completions respectively into a new child asynchronous
- operation by passing the sender's result datums to a user-specified
- callable, which returns a new sender that is connected and started.
-
-2. For `let_value`, `let_error`, and `let_stopped`, let `set-cpo`
- be `set_value`, `set_error`, and `set_stopped` respectively.
- Let the expression `let-cpo` be one of `let_value`,
- `let_error`, or `let_stopped`. For a subexpression `sndr`, let
- let-env(sndr) be expression-equivalent to the first
- well-formed expression below:
-
- - SCHED-ENV(get_completion_scheduler<decayed-typeof<set-cpo>>(get_env(sndr)))
-
- - MAKE-ENV(get_domain, get_domain(get_env(sndr)))
-
- - `(void(sndr), empty_env{})`
-
-3. The names `let_value`, `let_error`, and `let_stopped` denote pipeable sender
- adaptor objects. For subexpressions `sndr` and `f`, let `F` be the decayed
- type of `f`. If `decltype((sndr))` does not satisfy `sender` or if
- `decltype((f))` does not satisfy `movable-value`, the expression
- let-cpo(sndr, f) is ill-formed. If `F` does not satisfy
- `invocable`, the expression `let_stopped(sndr, f)` is ill-formed.
-
-4. Otherwise, the expression let-cpo(sndr, f) is
- expression-equivalent to:
-
-
-
- except that `sndr` is evaluated only once.
-
-5. The exposition-only class template `impls-for`
- ([exec.snd.general]) is specialized for `let-cpo` as
- follows:
-
-
- namespace std::execution {
- template<class State, class Rcvr, class... Args>
- void let-bind(State& state, Rcvr& rcvr, Args&&... args); // exposition only
-
- template<>
- struct impls-for<decayed-typeof<let-cpo>> : default-impls {
- static constexpr auto get-state = see below;
- static constexpr auto complete = see below;
- };
- }
-
-
- 1. Let `receiver2` denote the following exposition-only class template:
-
-
-
- 2. impls-for<decayed-typeof<let-cpo>>::get-state is
- initialized with a callable object equivalent to the following:
-
-
- []<class Sndr, class Rcvr>(Sndr&& sndr, Rcvr& rcvr) requires see below {
- auto& [_, fn, child] = sndr;
- using fn_t = decay_t<decltype(fn)>;
- using env_t = decltype(let-env(child));
- using args_variant_t = see below;
- using ops2_variant_t = see below;
-
- struct state-type {
- fn_t fn; // exposition only
- env_t env; // exposition only
- args_variant_t args; // exposition only
- ops2_variant_t ops2; // exposition only
- };
- return state-type{std::forward_like<Sndr>(fn), let-env(child), {}, {}};
- }
-
-
- 1. Let `Sigs` be a pack of the arguments to the `completion_signatures`
- specialization named by
- completion_signatures_of_t<child-type<Sndr>,
- env_of_t<Rcvr>>. Let `LetSigs` be a pack of those types in `Sigs`
- with a return type of decayed-typeof<set-cpo>. Let
- `as-tuple` be an alias template such that
- as-tuple<Tag(Args...)> denotes the type
- decayed-tuple<Args...>. Then
- `args_variant_t` denotes the type variant<monostate,
- as-tuple<LetSigs>...> except with duplicate types removed.
-
- 2. Given a type `Tag` and a pack `Args`, let `as-sndr2` be an alias template such that
- as-sndr2<Tag(Args...)> denotes the type
- call-result-t<Fn, decay_t<Args>&...>.
- Then `ops2_variant_t` denotes the type
- variant<monostate,
- connect_result_t<as-sndr2<LetSigs>,
- receiver2<Rcvr, Env>>...> except with duplicate types removed.
-
- 3. The requires-clause constraining the above lambda is
- satisfied if and only if the types `args_variant_t`
- and `ops2_variant_t` are well-formed.
-
- 3. The exposition-only function template `let-bind` has effects equivalent to:
-
-
-
-6. Let `sndr` and `env` be subexpressions, and let `Sndr` be `decltype((sndr))`.
- If sender-for<Sndr, decayed-typeof<let-cpo>> is
- `false`, then the expression let-cpo.transform_env(sndr,
- env) is ill-formed. Otherwise, it is equal to
- JOIN-ENV(let-env(sndr), FWD-ENV(env)).
-
-7. Let the subexpression `out_sndr` denote the result of the invocation
- let-cpo(sndr, f) or an object equal to such,
- and let the subexpression `rcvr` denote a receiver such that the expression
- `connect(out_sndr, rcvr)` is well-formed. The expression `connect(out_sndr,
- rcvr)` has undefined behavior unless it creates an asynchronous operation
- ([async.ops]) that, when started:
-
- - invokes `f` when `set-cpo` is called with `sndr`'s
- result datums,
-
- - makes its completion dependent on the completion of a sender returned
- by `f`, and
-
- - propagates the other completion operations sent by `sndr`.
-
-#### `execution::bulk` [exec.bulk] #### {#spec-execution.senders.adapt.bulk}
-
-1. `bulk` runs a task repeatedly for every index in an index space.
-
-2. The name `bulk` denotes a pipeable sender adaptor object. For subexpressions
- `sndr`, `shape`, and `f`, let `Shape` be `decltype(auto(shape))`. If
- `decltype((sndr))` does not satisfy `sender`, or if `Shape` does not
- satisfy `integral`, or if `decltype((f))` does not satisfy `movable-value`,
- `bulk(sndr, shape, f)` is ill-formed.
-
-3. Otherwise, the expression `bulk(sndr, shape, f)` is
- expression-equivalent to:
-
-
-
- except that `sndr` is evaluated only once.
-
-4. The exposition-only class template `impls-for`
- ([exec.snd.general]) is specialized for `bulk_t` as follows:
-
-
-
- 1. The expression in the *requires-clause* of the lambda above is
- `true` if and only if `Tag` denotes a type other than `set_value_t`
- or if the expression `f(auto(shape), args...)` is well-formed.
-
-5. Let the subexpression `out_sndr` denote the result of the invocation
- `bulk(sndr, shape, f)` or an object equal to such,
- and let the subexpression `rcvr` denote a receiver such that the expression
- `connect(out_sndr, rcvr)` is well-formed. The expression `connect(out_sndr,
- rcvr)` has undefined behavior unless it creates an asynchronous operation
- ([async.ops]) that, when started:
-
- - on a value completion operation, invokes `f(i, args...)` for every `i`
- of type `Shape` from `0` to `shape`, where `args` is a pack of lvalue
- subexpressions referring to the value completion result datums of the
- input sender, and
-
- - propagates all completion operations sent by `sndr`.
-
-#### `execution::split` [exec.split] #### {#spec-execution.senders.adapt.split}
-
-1. `split` adapts an arbitrary sender into a sender that can be connected
- multiple times.
-
-2. Let `split-env` be the type of an environment such that,
- given an instance `env`, the expression `get_stop_token(env)` is well-formed
- and has type `inplace_stop_token`.
-
-3. The name `split` denotes a pipeable sender adaptor object.
- For a subexpression `sndr`, let `Sndr` be `decltype((sndr))`.
- If sender_in<Sndr, split-env> is
- `false`, split(sndr) is ill-formed.
-
-4. Otherwise, the expression split(sndr) is
- expression-equivalent to:
-
-
-
- except that `sndr` is evaluated only once.
-
- - The default implementation of `transform_sender`
- will have the effect of connecting the sender to a receiver.
- It will return a sender with a different tag type.
-
-5. Let `local-state` denote the following exposition-only class template:
-
-
-
-7. Let `shared-state` denote the following exposition-only class
- template:
-
-
- namespace std::execution {
- template<class Sndr>
- struct shared-state {
- using variant-type = see below; // exposition only
- using state-list-type = see below; // exposition only
-
- explicit shared-state(Sndr&& sndr);
-
- void start-op() noexcept; // exposition only
- void notify() noexcept; // exposition only
- void inc-ref() noexcept; // exposition only
- void dec-ref() noexcept; // exposition only
-
- inplace_stop_source stop_src{}; // exposition only
- variant-type result{}; // exposition only
- state-list-type waiting_states; // exposition only
- atomic<bool> completed{false}; // exposition only
- atomic<size_t> ref_count{1}; // exposition only
- connect_result_t<Sndr, split-receiver<Sndr>> op_state; // exposition only
- };
- }
-
-
- 1. Let `Sigs` be a pack of the arguments to the
- `completion_signatures` specialization named by
- `completion_signatures_of_t`. For type `Tag` and pack `Args`,
- let `as-tuple` be an alias template such that
- as-tuple<Tag(Args...)> denotes the type
- decayed-tuple<Tag, Args...>. Then
- `variant-type` denotes the type
- variant<tuple<set_stopped_t>, tuple<set_error_t,
- exception_ptr>, as-tuple<Sigs>...>, but with
- duplicate types removed.
-
- 2. Let `state-list-type` be a type that stores a list of pointers
- to `local-state-base` objects and that permits atomic insertion.
-
- 3.
- explicit shared-state(Sndr&& sndr);
-
- 1. *Effects:* Initializes `op_state` with the result of
- connect(std::forward<Sndr>(sndr), split-receiver{this}).
-
- 2. *Postcondition:* `waiting_states` is empty, and `completed` is `false`.
-
- 4.
- void start-op() noexcept;
-
- 1. *Effects:* Calls inc-ref(). If
- `stop_src.stop_requested()` is `true`, calls
- notify(); otherwise, calls `start(op_state)`.
-
- 5.
- void notify() noexcept;
-
- 1. *Effects:* Atomically does the following:
-
- - Sets `completed` to `true`, and
-
- - Exchanges `waiting_states` with an empty list, storing the old
- value in a local `prior_states`.
-
- Then, for each pointer `p` in `prior_states`, calls
- p->notify(). Finally, calls
- dec-ref().
-
- 6.
- void inc-ref() noexcept;
-
- 1. *Effects:* Increments `ref_count`.
-
- 7.
- void dec-ref() noexcept;
-
- 1. *Effects:* Decrements `ref_count`. If the new value of
- `ref_count` is `0`, calls `delete this`.
-
- 2. *Synchronization:* If dec-ref() does not decrement
- the `ref_count` to `0` then synchronizes with
- the call to dec-ref() that decrements `ref_count` to `0`.
-
-8. Let `split-impl-tag` be an empty exposition-only class type.
- Given an expression `sndr`, the expression
- split.transform_sender(sndr) is equivalent to:
-
-
-
- where `shared-wrapper` is an exposition-only class that manages the
- reference count of the `shared-state` object pointed to by `sh_state`.
- `shared-wrapper` models `copyable` with move operations nulling out the
- moved-from object, copy operations incrementing the reference count by calling
- sh_state->inc-ref(), and assignment operations performing
- a copy-and-swap operation. The
- destructor has no effect if `sh_state` is null; otherwise, it
- decrements the reference count by calling
- sh_state->dec-ref().
-
-9. The exposition-only class template `impls-for`
- ([exec.snd.general]) is specialized for `split-impl-tag`
- as follows:
-
-
- namespace std::execution {
- template<>
- struct impls-for<split-impl-tag> : default-impls {
- static constexpr auto get-state = see below;
- static constexpr auto start = see below;
- };
- }
-
-
- 1. The member
- impls-for<split-impl-tag>::get-state
- is initialized with a callable object equivalent to the following lambda
- expression:
-
-
-
- 2. The member
- impls-for<split-impl-tag>::start
- is initialized with a callable object that has a function call operator
- equivalent to the following:
-
-
-
- 1. *Effects:* If `state.sh_state->completed` is `true`, calls
- state.notify() and returns. Otherwise,
- does the following in order:
-
-
- 1. Calls:
-
-
-
- 2. Then atomically does the following:
-
- - Reads the value `c` of `state.sh_state->completed`, and
-
- - Inserts `addressof(state)` into `state.sh_state->waiting_states`
- if `c` is `false`.
-
- 3. If `c` is `true`, calls state.notify() and returns.
-
- 4. Otherwise, if `addressof(state)` is the first item added
- to `state.sh_state->waiting_states`, calls
- state.sh_state->start-op().
-
-#### `execution::when_all` [exec.when.all] #### {#spec-execution.senders.adaptor.when_all}
-
-1. `when_all` and `when_all_with_variant` both adapt multiple input senders into
- a sender that completes when all input senders have completed. `when_all`
- only accepts senders with a single value completion signature and on success
- concatenates all the input senders' value result datums into its own value
- completion operation. `when_all_with_variant(sndrs...)` is semantically
- equivalent to `when_all(into_variant(sndrs)...)`, where `sndrs` is a pack of
- subexpressions whose types model `sender`.
-
-2. The names `when_all` and `when_all_with_variant` denote customization point
- objects. Let `sndrs` be a pack of subexpressions, let `Sndrs` be a pack of
- the types `decltype((sndrs))...`, and let `CD` be the type
- common_type_t<decltype(get-domain-early(sndrs))...>.
- The expressions `when_all(sndrs...)` and
- `when_all_with_variant(sndrs...)` are ill-formed if
- any of the following is true:
-
- * `sizeof...(sndrs)` is 0, or
-
- * `(sender && ...)` is `false`, or
-
- * `CD` is ill-formed.
-
-3. The expression `when_all(sndrs...)` is expression-equivalent to:
-
-
-
- and where `make-state` is the following exposition-only class template:
-
-
- template<class Sndr, class Env>
- concept max-1-sender-in = sender_in<Sndr, Env> && // exposition only
- (tuple_size_v<value_types_of_t<Sndr, Env, tuple, tuple>> <= 1);
-
- enum class disposition { started, error, stopped }; // exposition only
-
- template<class Rcvr>
- struct make-state {
- template<max-1-sender-in<env_of_t<Rcvr>>... Sndrs>
- auto operator()(auto, auto, Sndrs&&... sndrs) const {
- using values_tuple = see below;
- using errors_variant = see below;
- using stop_callback = stop_callback_of_t<stop_token_of_t<env_of_t<Rcvr>>, on-stop-request>;
-
- struct state-type {
- void arrive(Rcvr& rcvr) noexcept {
- if (0 == --count) {
- complete(rcvr);
- }
- }
-
- void complete(Rcvr& rcvr) noexcept; // see below
-
- atomic<size_t> count{sizeof...(sndrs)}; // exposition only
- inplace_stop_source stop_src{}; // exposition only
- atomic<disposition> disp{disposition::started}; // exposition only
- errors_variant errors{}; // exposition only
- values_tuple values{}; // exposition only
- optional<stop_callback> on_stop{nullopt}; // exposition only
- };
-
- return state-type{};
- }
- };
-
-
- 1. Let copy-fail be `exception_ptr` if decay-copying any of the
- child senders' result datums can potentially throw; otherwise,
- `none-such`, where `none-such` is an unspecified
- empty class type.
-
- 2. The alias `values_tuple` denotes the type
- tuple<value_types_of_t<Sndrs, env_of_t<Rcvr>,
- decayed-tuple, optional>...> if that type is well-formed;
- otherwise, `tuple<>`.
-
- 3. The alias `errors_variant` denotes the type
- variant<none-such, copy-fail,
- Es...> with duplicate types removed, where
- `Es` is the pack of the decayed types of all the
- child senders' possible error result datums.
-
- 4. The member void state::complete(Rcvr& rcvr)
- noexcept behaves as follows:
-
- 1. If `disp` is equal to disposition::started,
- evaluates:
-
-
-
- if the expression `decltype(auto(e))(e)` is potentially throwing; otherwise,
- `v.template emplace(e)`; and where TRY-EMPLACE-VALUE(c,
- o, as...), for subexpressions `c`, `o`, and pack of subexpressions `as`, is equivalent to:
-
-
-
- if the expression decayed-tuple<decltype(as)...>{as...}
- is potentially throwing; otherwise, `o.emplace(as...)`.
-
-5. The expression when_all_with_variant(sndrs...) is
- expression-equivalent to:
-
-
-
-6. Given subexpressions `sndr` and `env`, if
- sender-for<decltype((sndr)), when_all_with_variant_t> is `false`,
- then the expression `when_all_with_variant.transform_sender(sndr, env)` is
- ill-formed; otherwise, it is equivalent to:
-
-
-
- This causes the `when_all_with_variant(sndrs...)` sender
- to become `when_all(into_variant(sndrs)...)` when it is connected with a
- receiver whose execution domain does not customize `when_all_with_variant`.
-
-#### `execution::into_variant` [exec.into.variant] #### {#spec-execution.senders.adapt.into_variant}
-
-1. `into_variant` adapts a sender with multiple value completion signatures into
- a sender with just one value completion signature consisting of a `variant` of `tuple`s.
-
-2. The name `into_variant` denotes a pipeable sender adaptor object. For a
- subexpression `sndr`, let `Sndr` be `decltype((sndr))`. If `Sndr` does not
- satisfy `sender`, `into_variant(sndr)` is ill-formed.
-
-3. Otherwise, the expression `into_variant(sndr)` is expression-equivalent to:
-
-
-
- except that `sndr` is only evaluated once.
-
-5. The exposition-only class template `impls-for` ([exec.snd.general]) is
- specialized for `into_variant` as follows:
-
-
- namespace std::execution {
- template<>
- struct impls-for<into_variant_t> : default-impls {
- static constexpr auto get-state = see below;
- static constexpr auto complete = see below;
- };
- }
-
-
- 1. The member
- impls-for<into_variant_t>::get-state is
- initialized with a callable object equivalent to the following lambda:
-
-
-
-
-#### `execution::stopped_as_optional` [exec.stopped.as.optional] #### {#spec-execution.senders.adapt.stopped_as_optional}
-
-1. `stopped_as_optional` maps a sender's stopped completion operation into a
- value completion operation as an disengaged `optional`. The sender's value
- completion operation is also converted into an `optional`. The result is a
- sender that never completes with stopped, reporting cancellation by
- completing with an disengaged `optional`.
-
-2. The name `stopped_as_optional` denotes a pipeable sender adaptor
- object. For a subexpression `sndr`, let `Sndr` be `decltype((sndr))`. The
- expression `stopped_as_optional(sndr)` is expression-equivalent to:
-
-
-
- except that `sndr` is only evaluated once.
-
-3. Let `sndr` and `env` be subexpressions such that `Sndr` is `decltype((sndr))`
- and `Env` is `decltype((env))`. If sender-for<Sndr,
- stopped_as_optional_t> is `false`, or if the type
- single-sender-value-type<Sndr, Env> is ill-formed or
- `void`, then the expression `stopped_as_optional.transform_sender(sndr,
- env)` is ill-formed; otherwise, it is equivalent to:
-
-
-
-#### `execution::stopped_as_error` [exec.stopped.as.error] #### {#spec-execution.senders.adapt.stopped_as_error}
-
-1. `stopped_as_error` maps an input sender's stopped completion operation into
- an error completion operation as a custom error type. The result is a sender
- that never completes with stopped, reporting cancellation by completing with
- an error.
-
-2. The name `stopped_as_error` denotes a pipeable sender adaptor object.
- For some subexpressions `sndr` and `err`, let `Sndr` be `decltype((sndr))`
- and let `Err` be `decltype((err))`. If the type `Sndr` does not satisfy
- `sender` or if the type `Err` doesn't satisfy `movable-value`,
- `stopped_as_error(sndr, err)` is ill-formed. Otherwise, the expression
- `stopped_as_error(sndr, err)` is expression-equivalent to:
-
-
-
- except that `sndr` is only evaluated once.
-
-3. Let `sndr` and `env` be subexpressions such that `Sndr` is `decltype((sndr))` and `Env` is `decltype((env))`.
- If sender-for<Sndr, stopped_as_error_t> is `false`, then the expression
- `stopped_as_error.transform_sender(sndr, env)` is ill-formed; otherwise, it is equivalent to:
-
-
-
-### Sender consumers [exec.consumers] ### {#spec-execution.senders.consumers}
-
-#### `this_thread::sync_wait` [exec.sync.wait] #### {#spec-execution.senders.consumers.sync_wait}
-
-1. `this_thread::sync_wait` and `this_thread::sync_wait_with_variant` are used
- to block the current thread of execution until the specified sender
- completes and to return its async result. `sync_wait` mandates that the
- input sender has exactly one value completion signature.
-
-2. Let `sync-wait-env` be the following exposition-only class
- type:
-
-
-
-4. The name `this_thread::sync_wait` denotes a customization point object. For a
- subexpression `sndr`, let `Sndr` be `decltype((sndr))`. If
- sender_in<Sndr, sync-wait-env> is `false`, the
- expression `this_thread::sync_wait(sndr)` is ill-formed. Otherwise, it is
- expression-equivalent to the following, except that `sndr` is evaluated only
- once:
-
-
-
- Mandates:
-
- * The type sync-wait-result-type<Sndr> is well-formed.
-
- * same_as<decltype(e),
- sync-wait-result-type<Sndr>> is `true`, where
- `e` is the `apply_sender` expression above.
-
-5. Let `sync-wait-state` and
- `sync-wait-receiver` be the following exposition-only class
- templates:
-
-
- state->error = AS-EXCEPT-PTR(std::forward<Error>(err)); // see [exec.general]
- state->loop.finish();
-
-
- 3.
- void set_stopped() && noexcept;
-
-
- 1. *Effects:* Equivalent to state->loop.finish().
-
-
-6. For a subexpression `sndr`, let `Sndr` be `decltype((sndr))`. If
- sender_to<Sndr, sync-wait-receiver<Sndr>> is `false`, the
- expression `sync_wait.apply_sender(sndr)` is ill-formed; otherwise, it is
- equivalent to:
-
-
- sync-wait-state<Sndr> state;
- auto op = connect(sndr, sync-wait-receiver<Sndr>{&state});
- start(op);
-
- state.loop.run();
- if (state.error) {
- rethrow_exception(std::move(state.error));
- }
- return std::move(state.result);
-
-
-7. The behavior of `this_thread::sync_wait(sndr)` is undefined unless:
-
- 1. It blocks the current thread of execution ([defns.block]) with forward
- progress guarantee delegation ([intro.progress]) until the specified
- sender completes. The default implementation of
- `sync_wait` achieves forward progress guarantee delegation by providing
- a `run_loop` scheduler via the `get_delegation_scheduler` query on the
- `sync-wait-receiver`'s environment. The `run_loop` is
- driven by the current thread of execution.
-
- 2. It returns the specified sender's async results as follows:
-
- 1. For a value completion, the result datums are returned
- in a `tuple` in an engaged `optional` object.
-
- 2. For an error completion, an exception is thrown.
-
- 3. For a stopped completion, a disengaged `optional` object is returned.
-
-8. The name `this_thread::sync_wait_with_variant` denotes a customization point
- object. For a subexpression `sndr`, let `Sndr` be `decltype(into_variant(sndr))`.
- If sender_in<Sndr, sync-wait-env> is `false`,
- `this_thread::sync_wait_with_variant(sndr)` is ill-formed. Otherwise, it is
- expression-equivalent to the following, except `sndr` is evaluated only
- once:
-
-
-
- Mandates:
-
- - The type sync-wait-with-variant-result-type<Sndr> is
- well-formed.
-
- - same_as<decltype(e),
- sync-wait-with-variant-result-type<Sndr>> is `true`,
- where `e` is the `apply_sender` expression above.
-
-9. If callable<sync_wait_t, Sndr> is `false`, the
- expression `sync_wait_with_variant.apply_sender(sndr)` is ill-formed.
- Otherwise, it is equivalent to:
-
-
-
-10. The behavior of `this_thread::sync_wait_with_variant(sndr)` is undefined unless:
-
- 1. It blocks the current thread of execution ([defns.block]) with forward
- progress guarantee delegation ([intro.progress]) until the specified
- sender completes. The default implementation of
- `sync_wait_with_variant` achieves forward progress guarantee delegation
- by relying on the forward progress guarantee delegation provided by
- `sync_wait`.
-
- 2. It returns the specified sender's async results as follows:
-
- 1. For a value completion, the result datums are returned in an engaged
- `optional` object that contains a `variant` of `tuple`s.
-
- 2. For an error completion, an exception is thrown.
-
- 3. For a stopped completion, a disengaged `optional` object is returned.
-
-## Sender/receiver utilities [exec.utils] ## {#spec-execution.snd_rec_utils}
-
-### `execution::completion_signatures` [exec.utils.cmplsigs] ### {#spec-execution.snd_rec_utils.completion_sigs}
-
-1. `completion_signatures` is a type that encodes a set of completion signatures
- ([async.ops]).
-
-2. [Example:
-
- struct my_sender {
- using sender_concept = sender_t;
- using completion_signatures =
- execution::completion_signatures<
- set_value_t(),
- set_value_t(int, float),
- set_error_t(exception_ptr),
- set_error_t(error_code),
- set_stopped_t()>;
- };
-
- // Declares my_sender to be a sender that can complete by calling
- // one of the following for a receiver expression rcvr:
- // set_value(rcvr)
- // set_value(rcvr, int{...}, float{...})
- // set_error(rcvr, exception_ptr{...})
- // set_error(rcvr, error_code{...})
- // set_stopped(rcvr)
-
- -- end example]
-
-3. [exec.utils.cmplsigs] makes use of the following exposition-only entities:
-
-
- template<class Fn>
- concept completion-signature = see below;
-
- template<bool>
- struct indirect-meta-apply {
- template<template<class...> class T, class... As>
- using meta-apply = T<As...>; // exposition only
- };
-
- template<class...>
- concept always-true = true; // exposition only
-
-
- 1. A type `Fn` satisfies `completion-signature` if and
- only if it is a function type with one of the following forms:
-
- * set_value_t(Vs...), where `Vs`
- is a pack of object or reference types.
- * set_error_t(Err), where `Err` is
- an object or reference type.
- * `set_stopped_t()`
-
-
- template<class Tag,
- valid-completion-signatures Completions,
- template<class...> class Tuple,
- template<class...> class Variant>
- using gather-signatures = see below;
-
-
- 2. Let `Fns` be a pack of the arguments of the
- `completion_signatures` specialization named by `Completions`, let
- `TagFns` be a pack of the function
- types in `Fns` whose return types are `Tag`, and let
- Tsn be a pack
- of the function argument types in the `n`-th type in
- `TagFns`. Then, given two variadic templates
- `Tuple` and `Variant`, the type
- gather-signatures<Tag, Completions, Tuple,
- Variant> names the type
- META-APPLY(Variant, META-APPLY(Tuple,
- Ts0...), META-APPLY(Tuple,
- Ts1...), ... META-APPLY(Tuple,
- Tsm-1...)), where `m`
- is the size of the pack `TagFns` and
- META-APPLY(T, As...) is equivalent to:
-
-
-
- 3. The purpose of `META-APPLY` is
- to make it valid to use non-variadic templates as
- `Variant` and `Tuple` arguments to
- `gather-signatures`.
-
-4.
-
-
-### `execution::transform_completion_signatures` [exec.utils.tfxcmplsigs] ### {#spec-execution.snd_rec_utils.transform_completion_sigs}
-
-1. `transform_completion_signatures` is an alias template used to transform one
- set of completion signatures into another. It takes a set of completion
- signatures and several other template arguments that apply modifications to
- each completion signature in the set to generate a new specialization of
- `completion_signatures`.
-
-2. [Example:
-
- // Given a sender Sndr and an environment Env, adapt the completion
- // signatures of Sndr by lvalue-ref qualifying the values, adding an additional
- // exception_ptr error completion if its not already there, and leaving the
- // other completion signatures alone.
- template<class... Args>
- using my_set_value_t =
- completion_signatures<
- set_value_t(add_lvalue_reference_t<Args>...)>;
-
- using my_completion_signatures =
- transform_completion_signatures<
- completion_signatures_of_t<Sndr, Env>,
- completion_signatures<set_error_t(exception_ptr)>,
- my_set_value_t>;
-
- -- end example]
-
-3. [exec.utils.tfxcmplsigs] makes use of the following exposition-only entities:
-
-
- template<class... As>
- using default-set-value =
- completion_signatures<set_value_t(As...)>;
-
- template<class Err>
- using default-set-error =
- completion_signatures<set_error_t(Err)>;
-
-
- 1. `SetValue` shall name an alias template such that for any
- pack of types `As`, the type `SetValue` is either ill-formed
- or else
- valid-completion-signatures<SetValue<As...>>
- is satisfied.
- 2. `SetError` shall name an alias template such that for any type `Err`,
- `SetError` is either ill-formed or else
- valid-completion-signatures<SetError<Err>> is
- satisfied.
-
- Then:
-
- 3. Let `Vs...` be a pack of the types in the `type-list`
- named by gather-signatures<set_value_t, InputSignatures,
- SetValue, type-list>.
-
- 4. Let `Es...` be a pack of the types in the `type-list`
- named by gather-signatures<set_error_t, InputSignatures,
- type_identity_t, error-list>, where
- `error-list` is an alias template such that
- error-list<Ts...> is
- type-list<SetError<Ts>...>.
-
- 5. Let `Ss` name the type `completion_signatures<>` if
- gather-signatures<set_stopped_t, InputSignatures,
- type-list, type-list> is an alias for the type
- type-list<>; otherwise, `SetStopped`.
-
- Then:
-
- 6. If any of the above types are ill-formed, then
- `transform_completion_signatures` is ill-formed.
-
- 7. Otherwise, `transform_completion_signatures` is the type
- `completion_signatures` where `Sigs...` is the unique set of
- types in all the template arguments of all the `completion_signatures`
- specializations in the set `AdditionalSignatures, Vs..., Es..., Ss`.
-
-## Execution contexts [exec.ctx] ## {#spec-execution.contexts}
-
-### `execution::run_loop` [exec.run.loop] ### {#spec-execution.contexts.run_loop}
-
-1. A `run_loop` is an execution resource on which work can be scheduled. It
- maintains a thread-safe first-in-first-out queue of work. Its `run()`
- member function removes elements from the queue and executes them in a loop
- on the thread of execution that calls `run()`.
-
-2. A `run_loop` instance has an associated count that corresponds to the
- number of work items that are in its queue. Additionally, a `run_loop` instance has an
- associated state that can be one of starting, running,
- or finishing.
-
-3. Concurrent invocations of the member functions of `run_loop` other than
- `run` and its destructor do not introduce data races. The member functions
- `pop_front`, `push_back`, and `finish` execute atomically.
-
-4. *Recommended practice:* Implementations are encouraged to use an intrusive
- queue of operation states to hold the work units to make scheduling
- allocation-free.
-
-
-
- namespace std::execution {
- class run_loop {
- // [exec.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; // exposition only
- run_loop* loop; // exposition only
- run-loop-opstate-base* next; // exposition only
- };
- template<class Rcvr>
- using run-loop-opstate = unspecified; // exposition only
-
- // [exec.run.loop.members] Member functions:
- run-loop-opstate-base* pop-front(); // exposition only
- void push-back(run-loop-opstate-base*); // exposition only
-
- public:
- // [exec.run.loop.ctor] construct/copy/destroy
- run_loop() noexcept;
- run_loop(run_loop&&) = delete;
- ~run_loop();
-
- // [exec.run.loop.members] Member functions:
- run-loop-scheduler get_scheduler();
- void run();
- void finish();
- };
- }
-
-
-1. `run-loop-scheduler` is an unspecified type that models
- `scheduler`.
-
-2. Instances of `run-loop-scheduler` remain valid until the
- end of the lifetime of the `run_loop` instance from which they were
- obtained.
-
-3. Two instances of `run-loop-scheduler` compare equal if
- and only if they were obtained from the same `run_loop` instance.
-
-4. Let `sch` be an expression of type
- `run-loop-scheduler`. The expression
- schedule(sch) has type
- `run-loop-sender` and is not potentially-throwing
- if `sch` is not potentially-throwing.
-
-
-class run-loop-sender;
-
-
-1. `run-loop-sender` is an exposition-only type that satisfies `sender`.
- For any type `Env`,
- completion_signatures_of_t<run-loop-sender, Env> is:
-
-
-
-2. An instance of `run-loop-sender` remains valid until the
- end of the lifetime of its associated `run_loop` instance.
-
-3. Let `sndr` be an expression of type
- `run-loop-sender`, let `rcvr` be an
- expression such that receiver_of<decltype((rcvr)), CS>
- is `true` where `CS` is the `completion_signatures` specialization above. Let
- `C` be either `set_value_t` or `set_stopped_t`. Then:
-
- * The expression connect(sndr, rcvr) has type
- run-loop-opstate<decay_t<decltype((rcvr))>>
- and is potentially-throwing if and only if (void(sndr),
- auto(rcvr)) is potentially-throwing.
-
- * The expression
- get_completion_scheduler<C>(get_env(sndr)) is
- potentially-throwing if and only if `sndr` is
- potentially-throwing, has type
- `run-loop-scheduler`, and compares equal to the
- `run-loop-scheduler` instance from which
- `sndr` was obtained.
-
-
-
-1. run-loop-opstate<Rcvr> inherits privately and unambiguously
- from `run-loop-opstate-base`.
-
-2. Let `o` be a non-`const` lvalue of type
- run-loop-opstate<Rcvr>, and let
- REC(o) be a non-`const` lvalue reference to an
- instance of type `Rcvr` that was initialized with the
- expression `rcvr` passed to the invocation of `connect`
- that returned `o`. Then:
-
- * The object to which REC(o) refers remains
- valid for the lifetime of the object to which `o`
- refers.
-
- * The type run-loop-opstate<Rcvr> overrides
- run-loop-opstate-base::execute() such that
- o.execute() is equivalent to:
-
-
-
-#### Constructor and destructor [exec.run.loop.ctor] #### {#spec-execution.contexts.run_loop.ctor}
-
-
-run_loop() noexcept;
-
-
-1. Postconditions:count is `0` and state is
- starting.
-
-
-~run_loop();
-
-
-1. Effects: If count is not `0` or if state is
- running, invokes `terminate()`. Otherwise, has no effects.
-
-#### Member functions [exec.run.loop.members] #### {#spec-execution.contexts.run_loop.members}
-
-
-run-loop-opstate-base* pop-front();
-
-
-1. Effects: Blocks ([defns.block]) until one of the following conditions
- is `true`:
-
- * count is `0` and state is finishing, in which case
- `pop-front` returns `nullptr`; or
-
- * count is greater than `0`, in which case an item is removed from
- the front of the queue, count is decremented by `1`, and the
- removed item is returned.
-
-
-void push-back(run-loop-opstate-base* item);
-
-
-1. Effects: Adds `item` to the back of the queue and increments
- count by `1`.
-
-2. Synchronization: This operation synchronizes with the `pop-front`
- operation that obtains `item`.
-
-
-run-loop-scheduler get_scheduler();
-
-
-1. Returns: An instance of `run-loop-scheduler` that
- can be used to schedule work onto this `run_loop` instance.
-
-
-void run();
-
-
-1. Precondition:state is starting.
-
-2. Effects: Sets the state to running. Then,
- equivalent to:
-
-
- while (auto* op = pop-front()) {
- op->execute();
- }
-
-
-3. Remarks: When state changes, it does so without introducing
- data races.
-
-
-void finish();
-
-
-1. Effects: Changes state to finishing.
-
-2. Synchronization: `finish` synchronizes with the `pop-front`
- operation that returns `nullptr`.
-
-## Coroutine utilities [exec.coro.utils] ## {#spec-execution.coro_utils}
-
-### `execution::as_awaitable` [exec.as.awaitable] ### {#spec-execution.coro_utils.as_awaitable}
-
-1. `as_awaitable` transforms an object into one that is awaitable within a
- particular coroutine. [exec.coro.utils] makes use of the following
- exposition-only entities:
-
-
-
- 2. The type sender-awaitable<Sndr, Promise> is
- equivalent to:
-
-
- namespace std::execution {
- template<class Sndr, class Promise>
- class sender-awaitable {
- struct unit {}; // exposition only
- using value-type = // exposition only
- single-sender-value-type<Sndr, env_of_t<Promise>>;
- using result-type = // exposition only
- conditional_t<is_void_v<value-type>, unit, value-type>;
- struct awaitable-receiver; // exposition only
-
- variant<monostate, result-type, exception_ptr> result{}; // exposition only
- connect_result_t<Sndr, awaitable-receiver> state; // exposition only
-
- public:
- sender-awaitable(Sndr&& sndr, Promise& p);
- static constexpr bool await_ready() noexcept { return false; }
- void await_suspend(coroutine_handle<Promise>) noexcept { start(state); }
- value-type await_resume();
- };
- }
-
-
- 1. `awaitable-receiver` is equivalent to:
-
-
- struct awaitable-receiver {
- using receiver_concept = receiver_t;
- variant<monostate, result-type, exception_ptr>* result-ptr; // exposition only
- coroutine_handle<Promise> continuation; // exposition only
- // ... see below
- };
-
-
- Let `rcvr` be an rvalue expression of type
- `awaitable-receiver`, let `crcvr` be a `const`
- lvalue that refers to `rcvr`, let `vs` be a pack of subexpressions,
- and let `err` be an expression of type `Err`.
- Then:
-
- 1. If constructible_from<result-type, decltype((vs))...>
- is satisfied, the expression `set_value(rcvr, vs...)` is
- equivalent to:
-
-
-
- 4. For any expression `tag` whose type satisfies
- `forwarding-query` and for any pack of
- subexpressions `as`, `get_env(crcvr).query(tag, as...)` is
- expression-equivalent to
- tag(get_env(as_const(crcvr.continuation.promise())),
- as...).
-
- 2. sender-awaitable(Sndr&& sndr, Promise& p);
-
- 1. Effects: Initializes `state` with
- connect(std::forward<Sndr>(sndr),
- awaitable-receiver{addressof(result),
- coroutine_handle<Promise>::from_promise(p)}).
-
- 3. value-type await_resume();
-
- 1. Effects: Equivalent to:
-
-
- if (result.index() == 2)
- rethrow_exception(get<2>(result));
- if constexpr (!is_void_v<value-type>)
- return std::forward<value-type>(get<1>(result));
-
-
-2. `as_awaitable` is a customization point object. For subexpressions
- `expr` and `p` where `p` is an lvalue, `Expr` names the type
- `decltype((expr))` and `Promise` names the type `decay_t`,
- `as_awaitable(expr, p)` is expression-equivalent to:
-
- 1. `expr.as_awaitable(p)` if that expression is well-formed.
-
- * Mandates:is-awaitable<A, Promise> is
- `true`, where `A` is the type of the expression above.
-
- 2. Otherwise, `(void(p), expr)` if is-awaitable<Expr, U>
- is `true`, where `U` is an unspecified class type that
- is not `Promise` and that lacks a member named `await_transform`.
-
- * Preconditions:is-awaitable<Expr,
- Promise> is `true` and the expression `co_await expr` in a
- coroutine with promise type `U` is
- expression-equivalent to the same expression in a coroutine with
- promise type `Promise`.
-
- 3. Otherwise, sender-awaitable{expr, p} if
- awaitable-sender<Expr, Promise> is `true`.
-
- 4. Otherwise, `(void(p), expr)`.
-
- except that the evaluations of `expr` and `p` are indeterminately sequenced.
-
-### `execution::with_awaitable_senders` [exec.with.awaitable.senders] ### {#spec-execution.coro_utils.with_awaitable_senders}
-
-1. `with_awaitable_senders`, when used as the base class of a coroutine promise
- type, makes senders awaitable in that coroutine type.
-
- In addition, it provides a default implementation of `unhandled_stopped()`
- such that if a sender completes by calling `set_stopped`, it is treated as
- if an uncatchable "stopped" exception were thrown from the
- await-expression. The coroutine is never resumed, and
- the `unhandled_stopped` of the coroutine caller's promise type is called.
-
-