---
title: Scheduler Affinity
document: P3941R3
date: 2026-03-21
audience: Concurrency Working Group (SG1),Library Evolution Working Group (LEWG),Library Working Group (LWG)
reply-to:
  - "Dietmar Kühl (Bloomberg) <dkuhl@bloomberg.net>"
paper-type: proposal
---

One important design of `std::execution::task` is that a coroutine resumes after a `co_await` on the same scheduler as the one it was executing on prior to the `co_await`. To achieve this, `task` transforms the awaited object `*obj*` using `affine_on(*obj*, *sched*)` where `*sched*` is the corresponding scheduler. There were multiple concerns raised against the specification of `affine_on` and discussed as part of [P3796R1](https://wg21.link/P3796R1). This proposal is intended to specifically address the concerns raised relating to `task`’s scheduler affinity and in particular `affine_on`. The gist of this proposal is impose constraints on `affine_on` to guarantee it can meet its objective at run-time.

# Change History

## R3

- rebase changes on the customization changes [P3826r3](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p3826r3.html)
- use `transform_sender` in `as_awaitable` to locate possible customisation of nested senders in [[exec.as.awaitable](https://wg21.link/exec.as.awaitable#7)]

## R2

- added requirement on `get_scheduler`/`get_start_scheduler`
- fixed typo in the wording for `affine_on::transform_sender`
- fixed various typos in the text

## R1

- added wording

## R0

- initial revision

# Overview of Changes

There are a few NB comments raised about the way `affine_on` works:

- [US 232-366](https://github.com/cplusplus/nbballot/issues/941): specify customization of `affine_on` when the scheduler doesn’t change.
- [US 233-365](https://github.com/cplusplus/nbballot/issues/940): clarify `affine_on` vs. `continues_on`.
- [US 234-364](https://github.com/cplusplus/nbballot/issues/939): remove scheduler parameter from `affine_on`.
- [US 235-363](https://github.com/cplusplus/nbballot/issues/938): `affine_on` should not forward the stop token to the scheduling operation.
- [US 236-362](https://github.com/cplusplus/nbballot/issues/937): specify default implementation of `affine_on`.

The discussion on `affine_on` revealed some aspects which were not quite clear previously and taking these into account points towards a better design than was previously specified:

1. To implement scheduler affinity the algorithm needs to know the scheduler on which it was started itself. The correct scheduler may actually be hard to determine while building the work graph. However, this scheduler can be communicated using `get_scheduler(get_env(*rcvr*))` when an algorithm is `start`ed. This requirement is more general than just `affine_on` and is introduced by [P3718R0](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3718r0.html): with this guarantee in place, `affine_on` only needs one parameter, i.e., the sender for the work to be executed. [P3718R0](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3718r0.html) is, however, discontinued (for unrelated reasons) and adding the guarantee to get the current scheduler from a receiver query is proposed here.
2. The scheduler `*sched*` on which the work needs to resume has to guarantee that it is possible to resume on the correct execution agent. The implication is that scheduling work needs to be infallible, i.e., the completion signatures of `scheduler(*sched*)` cannot contain a `set_error_t(E)` completion signature. This requirement should be checked statically.
3. The work needs to be resumed on the correct scheduler even when the work is stopped, i.e., the scheduling operation shall be `connect`ed to a receiver whose environment’s `get_stop_token` query yields an `unstoppable_token`. In addition, the scheduling operation shall not have a `set_stopped_t()` completion signature if the environment’s `get_stop_token` query yields an `unstoppable_token`. This requirement should also be checked statically.
4. When a sender knows that it will complete on the scheduler it was started on, it should be possible to customize the `affine_on` algorithm to avoid rescheduling. This customization can be achieved by `connect`ing to the result of an `affine_on` member function called on the child sender, if such a member function is present, when `connect`ing an `affine_on` sender.

None of these changes really contradict any earlier design: the shape and behavior of the `affine_on` algorithm wasn’t fully fleshed out. Tightening the behavior of scheduler affinity and the `affine_on` algorithm has some implications on some other components:

1. If `affine_on` requires an infallible scheduler at least `inline_scheduler`, `task_scheduler`, and `run_loop::scheduler` should be infallible (i.e., they always complete successfully with `set_value()`). `parallel_scheduler` can probably not be made infallible.
2. The scheduling semantics when changing a `task`’s scheduler using `co_await change_coroutine_scheduler(*sch*)` become somewhat unclear and this functionality should be removed. Similar semantics are better modeled using `co_await on(*sch*, *nested-task*)`.
3. The name `affine_on` isn’t particular good and wasn’t designed. It may be worth renaming the algorithms to something different.

# Discussion of Changes

## affine_on Shape

The original proposal for `task` used `continues_on` to schedule the work back on the original scheduler. This algorithm takes the work to be executed and the scheduler on which to continue as arguments. When SG1 requested that a similar but different algorithms is to be used to implement scheduler affinity, `continues_on` was just replaced by `affine_on` with the same shape but the potential to get customized differently.

The scheduler used for affinity is the scheduler communicated via the `get_scheduler` query on the receiver’s environment: the scheduler argument passed to the `affine_on` algorithm would need to match the scheduler obtained from `get_scheduler` query. In the context of the `task` coroutine this scheduler can be obtained via the promise type but in general it is actually not straight forward to get hold of this scheduler because the receiver and hence its associated scheduler is only provided by `connect`. It is much more reasonable to have `affine_on` only take the work, i.e., a sender, as argument and determine the scheduler to resume on from the receiver’s environment in `connect`.

Thus, instead of using `affine_on(*sndr*, *sch*)` the algorithm is used just with the sender: `affine_on(*sndr*)`

Note that this change implies that an operation state resulting from `connect`ing `affine_on` to a receiver `*rcvr*` is `start`ed on the execution agent associated with the scheduler obtained from `get_scheduler(get_env(*rcvr*))`. The same requirement is also assumed to be met when `start`ing the operation state resulting from `connect`ing a `task`. While it is possible to statically detect whether the query is valid and provides a scheduler it cannot be detected if the scheduler matches the execution agent on which `start` was called. [P3718r0](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3718r0.html) proposed to add this exact requirement to [[exec.get.scheduler]](https://wg21.link/exec.get.scheduler) which is no moved to this proposal.

This change addresses [US 234-364](https://github.com/cplusplus/nbballot/issues/939) ([LWG4331](https://cplusplus.github.io/LWG/issue4331)).

## Scheduler An Operation Was
Started On

The `on` and `affine_on` algorithms both use the scheduler returned from the `get_scheduler` query on the receiver to determine where to resume execution after some other work completed. When using these algorithms it is not always possible to determine which scheduler an operation gets started on when creating a work graph. Thus, the algorithms determine the scheduler from context. To actually do that, it needs to be required that the scheduler is made accessible when using these algorithms. [P3718r0](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3718r0.html) proposed that `get_scheduler` should be required to yield the scheduler an operation was started on.

It was pointed out that the meaning for the `get_scheduler` query isn’t clear and it is used for two separate purposes:

1. The `get_scheduler` query is used to get a scheduler to schedule new work on. The `get_scheduler` query is used to get the scheduler an operation was `start`ed on.

This dual use doesn’t necessary align. It would be reasonable to add a new query, e.g., `get_start_scheduler` which is used to determine the scheduler an operation was `start`ed on. The requirement for getting the scheduler an operation was started on would be on the `get_start_scheduler` query rather than on the `get_scheduler` query. At the same time the `get_start_scheduler` query could be defaulted to use the `get_scheduler` query.

## Infallible Schedulers

The objective of `affine_on(*sndr*)` is to execute `*sndr*` and to complete on the execution agent on which the operation was `start`ed. Let `sch` be the scheduler obtained from `get_scheduler(get_env(*rcvr*))` where `*rcvr*` is the receiver used when `connect`ing `affine_on(*sndr*)` (the discussion in this section also applies if the scheduler would be taken as a parameter, i.e., if the previous change isn’t applied this discussion still applies). If `connect`ing the result of `schedule(*sch*)` fails (i.e., `connect(schedule(*sch*), *rcvr*)` throws where `*rcvr*` is a suitable receiver), `affine_on` can avoid `start`ing the main work and fail on the execution agent where it was `start`ed. Otherwise, if it obtained an operation state `*os*` from `connect(scheduler(*sch*), *rcvr*)`, `affine_on` would `start` its main work and would `start(*os*)` on the execution agent where the main work completed. If `start(*os*)` is always successful, `affine_on` can achieve its objective. However, if this scheduling operation fails, i.e., it completes with `set_error(*e*)`, or if it gets cancelled, i.e., it completes with `set_stopped()`, the execution agent on which the scheduling operation resumes is unclear and `affine_on` cannot guarantee its promise. Thus, it seems reasonable to require that a scheduler used with `affine_on` is infallible, at least when used appropriately (i.e., when providing a receiver whose associated stop token is an `unstoppable_token`).

The current working draft specifies 4 schedulers:

1. [`inline_scheduler`](https://wg21.link/exec.inline.scheduler) which just completes with `set_value()` when `start()`ed, i.e., this scheduler is already infallible.
2. [`task_scheduler`](https://wg21.link/exec.task.scheduler) is a type-erased scheduler delegating to another scheduler. If the underlying scheduler is infallible, the only error case for `task_scheduler` is potential memory allocation during `connect` of its `*ts-sender*`. If `affine_on` creates an operation state for the scheduling operation during `connect`, it can guarantee that any necessary scheduling operation succeeds. Thus, this scheduler can be made infallible.
3. The [`run_loop::*run-loop-scheduler*`](https://wg21.link/exec.run.loop) is used by [`run_loop`](https://wg21.link/exec.run.loop). The current specification allows the scheduling operation to fail with `set_error_t(std::exception_ptr)`. This permission allows an implementation to use [`std::mutex`](https://wg21.link/thread.mutex) and [`std::condition_variable`](https://wg21.link/thread.condition) whose operations may throw. It is possible to implement the logic using atomic operations which can’t throw. The `set_stopped()` completion is only used when the receiver’s stop token, i.e. the result of `get_stop_token(get_env(*rcvr*))`, was stopped. This receiver is controlled by `affine_on`, i.e., it can provide a [`never_stoptoken`](https://wg21.link/stoptoken.never) and this scheduler won’t complete with `set_stopped()`. If the [`get_completion_signatures`](https://wg21.link/exec.getcomplsigs) for the corresponding sender takes the environment into account, this scheduler can also be made infallible.
4. The [`parallel_scheduler`](https://wg21.link/exec.par.scheduler) provides an interface to a replaceable implementation of a thread pool. The current interface allows [`parallel_scheduler`](https://wg21.link/exec.par.scheduler) to complete with `set_error_t(std::exception_ptr)` as well as with `set_stopped_t()`. It seems unlikely that this interface can be constrained to make it infallible.

In general it seems unlikely that all schedulers can be constrained to be infallible. As a result `affine_on` and, by extension, `task` won’t be usable with all schedulers if `affine_on` insists on using only infallible schedulers. If there are fallible schedulers, there aren’t any good options for using them with a `task`. Note that `affine_on` can fail and get cancelled (due to the main work failing or getting cancelled) but `affine_on` can still guarantee that execution resumes on the expect execution agent when it uses an infallible scheduler.

This change addresses [US 235-363](https://github.com/cplusplus/nbballot/issues/938) ([LWG4332](https://cplusplus.github.io/LWG/issue4332)). This change goes beyond the actual issue and clarifies that the scheduling operation used be `affine_on` needs to be always successful.

### Require Infallible Schedulers
For affine_on

If `affine_on` promises in all cases that it resumes on the original scheduler it can only work with infallible schedulers. If a users wants to use a fallible scheduler with `affine_on` or `task` the scheduler will need to be adapted. The adapted scheduler can define what it means when the underlying scheduler fails. There are conceptually only two options (the exact details may vary) on how to deal with a failed scheduling operation:

1. The user can transform the scheduling failure into a call to `std::terminate`.
2. The user can consider resuming on an execution agent where the adapting scheduler can schedule to infallibly (e.g., the execution agent on which operation completed) but which is different from execution agent associated with the adapted scheduler to be suitable to continue running. In that case the scheduling operation would just succeed without necessarily running on the correct execution agent. However, there is no indication that scheduling to the adapted scheduler failed and the scheduler affinity may be impacted in this failure case.

The standard library doesn’t provide a way to adapt schedulers easily. However, it can certainly be done.

### Allow Fallible Schedulers For
affine_on

If the scheduler used with `affine_on` is allowed to fail, `affine_on` can’t guarantee that it completes on the correct scheduler in case of an error completion. It could be specified that `affine_on` completes with `set_error(*rcvr*, scheduling_error{*e*})` when the scheduling operation completes with `set_error(*r*, *e*)` to make it detectable that it didn’t complete on the correct scheduler. This situation is certainly not ideal but, at least, only affects the error completion and it can be made detectable.

A use of `affine_on` which always needs to complete on a specific scheduler is still possible: in that case the user will need to make sure that the used scheduler is infallible. The main issue here is that there is no automatic static checking whether that is the case.

### Considerations On Infallible
Schedulers

In an ideal world, all schedulers would be infallible. It is unclear if that is achievable. If schedulers need to be allowed to be fallible, it may be viable to require that all standard library schedulers are infallible. As outlined above that should be doable for all current schedulers except, possibly, `parallel_scheduler`. So, the proposed change is to require schedulers to be infallible when being used with `affine_on` (and, thus, being used by `task`) and to change as many of the standard C++ libraries to be infallible as possible.

If constraining `affine_on` to only infallible schedulers turns out to be too strong, the constraint can be relaxed in a future revision of the standard by explicitly opting out of that constraints, e.g., using an additional argument. For `task` to make use of it, it too would need an explicit mechanisms to indicate that its `affine_on` use should opt out of the constraint, e.g., by adding a suitable `static` member to the environment template argument.

## affine_on Customization

Senders which don’t cause the execution agent to be changed like `just` or the various queries should be able to customize `affine_on` to avoid unnecessary scheduling. Sadly, a proposal ([P3206](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3206r0.pdf)) to standardize properties which could be used to determine how a sender completes didn’t make much progress, yet. An implementation can make use of similar techniques using an implementation-specific protocol. If a future standard defines a standard approach to determine the necessary properties the implementation can pick up on those.

The idea is to have `affine_on` define a `transform_sender(s)` member function which determines what sender should be returned. By default the argument is returned but if the child sender indicates that it doesn’t actually change the execution agent the function would return the child sender. There are a number of senders for which this can be done:

- `just`, `just_error`, and `just_stopped`
- `read_env` and `write_env`
- `then`, `upon_error`, and `upon_stopped` if the child sender doesn’t change the execution agent

The proposal is to define a `transform_sender` member which uses an implementation-specific property to determine that a sender completes on the same execution agent as the one it was started on. In addition, it is recommended that this property gets defined by the various standard library senders where it can make a difference.

This change addresses [US 232-366](https://github.com/cplusplus/nbballot/issues/941) ([LWG4329](https://cplusplus.github.io/LWG/issue4329)), although not in a way allowing application code to plug into this mechanism. Such an approach can be designed in a future revision of the standard.

## Removing
change_coroutine_scheduler

The current working paper specifies `change_coroutine_scheduler` to change the scheduler used by the coroutine for scheduler affinity. It turns out that this use is somewhat problematic in two ways:

1. Changing the scheduler affects the coroutine until the end of the coroutine or until `change_coroutine_scheduler` is `co_await`ed again. It doesn’t automatically reset. Thus, local variables constructed before `change_coroutine_scheduler(s)` was `co_await`ed were constructed on the original scheduler and are destroyed on the replaced scheduler.
2. The `task`’s execution may finish on a different than the original scheduler. To allow symmetric transfer between two `task`s each `task` needs to complete on the correct scheduler. Thus, the `task` needs to be prepared to change to the original scheduler before actually completing. To do so, it is necessary to know the original scheduler and also to have storage for the state needed to change to a different scheduler. It can’t be statically detected whether `change_coroutine_scheduler(s)` is `co_await`ed in the body of a coroutine and, thus, the necessary storage and checks are needed even for `task`s which don’t use `change_coroutine_scheduler`.

If there were no way to change the scheduler it would still be possible to execute using a different scheduler, although not as direct: instead of using `co_await change_coroutine_scheduler(s)` to change the scheduler used for affinity to `s` a nested `task` executing on `s` could be `co_await`ed:

```cpp
co_await ex::starts_on(s, [](parameters)->task<T, E> { logic }(arguments));
```

Using this approach the use of the scheduler `s` is clearly limited to the nested coroutine. The scheduler affinity is fully taken care of by the use of `affine_on` when `co_await`ing work. There is no need to provide storage or checks needed for the potential of having a `task` return to the original scheduler if the scheduler isn’t actually changed by a `task`.

The proposal is remove `change_coroutine_scheduler` and the possibility of changing the scheduler within a `task`. The alternative to controlling the scheduler used for affinity from within a `task` is a bit verbose. This need under the control of the coroutine is likely relatively rare. Replacing the used scheduler for an existing `task` by nesting it within `on(s, t)` or `starts_on(s, t)` is fairly straightforward.

This functionality was originally included because it is present for, at least, one of the existing libraries, although in a form which was recommended against. The existing use changes the scheduler of a coroutine when `co_await`ing the result of `schedule(s)`; this exact approach was found to be fragile and surprising and the recommendation was to provide the functionality more explicit.

This change is not associated with any national body comment. However, it is still important to do! It isn’t adding any new functionality but removes a problematic way to achieve something which can be better achieved differently. If this change is not made the inherent cost of having the possibility of having `change_routine_scheduler` can’t be removed later without breaking existing code.

## affine_on Default Implementation

Using the previous discussion leads to a definition of `affine_on` which is quite different from effectively just using `continues_on`:

1. The class `affine_on` should define a `transform_sender` member function which returns the child sender if this child sender indicates via an implementation specific way that it doesn’t change the execution agent. It should be recommended that some of the standard library sender algorithms (see above) to indicate that they don’t change the execution agent.
2. The `affine_on` algorithm should only allow to get `connect`ed to a receiver `r` whose scheduler `sched` obtained by `get_scheduler(get_env(r))` is infallible, i.e., `get_completion_signatures(schedule(sched), e)` with an environment `e` where `get_stop_token(e)` yields `never_stop_token` returns `completion_signatures<set_value_t()>`.
3. When `affine_on` gets `connect`ed, the scheduling operation state needs to be created by `connect`ing the scheduler’s sender to a suitable receiver to guarantee that the completion can be scheduled on the execution agent. The stop token `get_stop_token(get_env(r))` for the receiver `r` used for this `connect` shall be an `unstoppable_token`. The child sender also needs to be `connect`ed with a receiver which will capture the respective result upon completion and start the scheduling operation.
4. When the result operation state gets `start`ed it `start`s the operation state from the child operation.
5. Upon completion of the child operation the kind of completion and the parameters, if any, are stored. If this operation throws, the storage is set up to be as if `set_error(current_exception)` were called. Once the parameters are stored, the scheduling operation is started.
6. Upon completion of the scheduling operation, the appropriate completion function with the respective arguments is invoked.

This behavior is similar to `continues_on` but is subtly different with respect to when the scheduling operation state needs to be created and that any stop token from the receiver doesn’t get forwarded. In addition `affine_on` is more constrained with respect to the schedulers it supports and the shape of the algorithm is different: `affine_on` gets the scheduler to execute on from the receiver it gets `connect`ed to.

This change addresses [US 233-365](https://github.com/cplusplus/nbballot/issues/940) ([LWG4330](https://cplusplus.github.io/LWG/issue4330)) and [US 236-362](https://github.com/cplusplus/nbballot/issues/937) ([LWG](https://cplusplus.github.io/LWG/issue4344); the proposed resolution in this issue is incomplete).

## Name Change

The name `affine_on` isn’t great. It may be worth giving the algorithm a better name.

# Wording Changes

[ Editor's note: This wording is relative to [N5032](https://wg21.link/N5032) with the changes from [P3826r3](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p3826r3.html) applied. ]

[ Editor's note: If `get_start_scheduler` is introduced add it to the synopsis in [execution.syn] after `get_scheduler` as follows: ]

```cpp
...
namespace std::execution {
  // [exec.queries], queries
  struct get_domain_t { unspecified };
  struct get_scheduler_t { unspecified };
  struct get_start_scheduler_t { unspecified };
  struct get_delegation_scheduler_t { unspecified };
  struct get_forward_progress_guarantee_t { unspecified };
  template<class CPO>
    struct get_completion_scheduler_t { unspecified };
  template<class CPO = void>
    struct get_completion_domain_t { unspecified };
  struct get_await_completion_adaptor_t { unspecified };

  inline constexpr get_domain_t get_domain{};
  inline constexpr get_scheduler_t get_scheduler{};
  inline constexpr get_start_scheduler_t get_start_scheduler{};
  inline constexpr get_delegation_scheduler_t get_delegation_scheduler{};
  enum class forward_progress_guarantee;
  inline constexpr get_forward_progress_guarantee_t get_forward_progress_guarantee{};
  template<class CPO>
    constexpr get_completion_scheduler_t<CPO> get_completion_scheduler{};
  template<class CPO = void>
    constexpr get_completion_domain_t<CPO> get_completion_domain{};
  inline constexpr get_await_completion_adaptor_t get_await_completion_adaptor{};
  ...
}
```

[ Editor's note: If `get_start_scheduler` is introduced add a new section after [exec.get.scheduler] as follows: ]

### execution::get_start_scheduler [exec.get.start.scheduler]

1 `get_start_scheduler` asks a queryable object for the scheduler an operation got started on.

2 The name `get_start_scheduler` denotes a query object. For a subexpression `env`, `get_start_scheduler(env)` is expression-equivalent to `*MANDATE-NOTHROW*(*AS-CONST*(env).query(get_start_scheduler))` if this expression is well-formed. Otherwise, `get_start_scheduler(env)` is expression-equivalent to `get_scheduler(env)`.

*Mandates*: If the expression above is well-formed, its type satisfies `scheduler`.

3 `forwarding_query(execution::get_start_scheduler)` is a core constant expression and has value true.

? Given subexpressions `sndr` and `rcvr` such that `sender_to<decltype((sndr)), decltype((rcvr))>` is `true` and the expression `get_start_scheduler(get_env(rcvr))` is well-formed, an operation state that is the result of calling `connect(sndr, rcvr)` shall, if it is started, be started on an execution agent associated with the scheduler `get_start_scheduler(get_env(rcvr))`.

[ Editor's note: If `get_start_scheduler` is introduced change [[exec.snd.expos](https://wg21.link/exec.snd.expos)] paragraph 8 to have `*SCHED-ENV*` use `get_start_scheduler` instead of `get_scheduler`: ]

8 `*SCHED-ENV*(sch)` is an expression `o2` whose type satisfies `*queryable*` such that `o2.query(<del>get_scheduler</del><ins>get_start_scheduler</ins>)` 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)`.

[ Editor's note: The specification of `on` [[exec.on](https://wg21.link/exec.on)] shouldn’t use `write_env` as it does, i.e., this change removes these (not removing them was an oversight in [P3826](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p3826r3.html)). In addition, if `get_start_scheduler` is introduced change how `on` gets its scheduler in [[exec.on](https://wg21.link/exec.on)], i.e., change the use from `get_scheduler` to use `get_start_scheduler`: ]

…

8 Otherwise, the expression `on.transform_sender(set_value, out_sndr, env)` has effects equivalent to:

```cpp
auto&& [_, data, child] = out_sndr;
if constexpr (scheduler<decltype(data)>) {
  auto orig_sch =
    call-with-default(get_schedulerget_start_scheduler, not-a-scheduler(), env);

  return continues_on(
    starts_on(std::forward_like<OutSndr>(data), std::forward_like<OutSndr>(child)),
    std::move(orig_sch));
} else {
  auto& [sch, closure] = data;
  auto orig_sch = call-with-default(
    get_completion_scheduler<set_value_t>, not-a-scheduler(), get_env(child), env);

  return continues_on(
    write_env(
      std::forward_like<OutSndr>(closure)(
        continues_on(
          write_env(std::forward_like<OutSndr>(child), SCHED-ENV(orig_sch)),
          sch)),
      SCHED-ENV(sch)),
    orig_sch);
}
```

9 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 (9.1) remember the current scheduler, `get_<ins>start_</ins>scheduler(get_env(rcvr))`; (9.2) start `sndr` on an execution agent belonging to `sch`’s associated execution resource; (9.3) upon `sndr`’s completion, transfer execution back to the execution resource associated with the scheduler remembered in step 1; and (9.4) 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.

10 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<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

- (10.1) remember the current scheduler, which is the first of the following expressions that is well-formed:
  - (10.1.1) `get_completion_scheduler<set_value_t>(get_env(sndr))`
  - (10.1.2) `get_<ins>start_</ins>scheduler(get_env(rcvr));`
- (10.2) start `sndr` on the current execution agent;
- (10.3) upon `sndr`’s completion, transfer execution to an agent owned by sch’s associated execution resource;
- (10.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`; and
- (10.5) upon completion of the operation started in the previous step, 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.

[ Editor's note: If `get_start_scheduler` is not introduced change [exec.get.scheduler] as follows: ]

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.

? Given subexpressions `sndr` and `rcvr` such that `sender_to<decltype((sndr)), decltype((rcvr))>` is `true` and the expression `get_scheduler(get_env(rcvr))` is well-formed, an operation state that is the result of calling `connect(sndr, rcvr)` shall, if it is started, be started on an execution agent associated with the scheduler `get_scheduler(get_env(rcvr))`.

[ Editor's note: Change [[exec.as.awaitable](https://wg21.link/exec.as.awaitable#7)] paragraph 7 such that it tries to locate a customisation for `as_awaitable` on the transformed nested sender. ]

7 `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<decltype((p))>, as_awaitable(expr, p)` is expression-equivalent to, except that the evaluations of `expr` and `p` are indeterminately sequenced:

- (7.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.
- (7.?) <ins>`*adapt-for-await-completion*(transform_sender(expr, get_env(p))).as_awaitable(p)` if this expression is well-formed, `sender_in<Expr, env_of_t<Promise>>` is `true`, and `*single-sender-value-type*<Expr, env_of_t<Promise>>` is well-formed.</ins>
- (7.2) Otherwise, `(void(p), expr)` if `decltype(*GET-AWAITER*(expr))` satisfies `*is-awaiter*<Promise>`.
- (7.3) <del>Otherwise, `*sender-awaitable*{*adapted-expr*, p}` if</del> <del>`*has-queryable-await-completion-adaptor*<Expr>`</del> <del>and</del> <del>`*awaitable-sender*<decltype((*adapted-expr*)), Promise>`</del> <del>are both satisfied, where `*adapted-expr*` is `get_await_completion_adaptor(get_env(expr))(expr)`, except that `expr` is evaluated only once.</del>
- (7.4) Otherwise, <del>`*sender-awaitable*{expr, p}` if `*awaitable-sender*<Expr, Promise>` is `true`.</del> <ins>`*sender-awaitable*{*adapt-for-await-completion*(transform_sender(expr, get_env(p))), P}` if `sender_in<Expr, env_of_t<Promise>>` is `true` and `*single-sender-value-type*<Expr, env_of_t<Promise>>` is well-formed</ins>
- (7.5) Otherwise, `(void(p), expr)`.

8 <ins>The expression `*adapt-for-await-completion*(s)` is `get_await_completion_adaptor(get_env(s))(s)` if that is well-formed, and `s` otherwise.</ins>

[ Editor's note: Change [exec.affine.on] to use only one parameter, require an infallible scheduler from the receiver, and add a default implementation which allows customization of `affine_on` for child senders. If `get_start_scheduler` is introduced the algorithms should use `get_start_scheduler` to get the start scheduler: ]

1 `affine_on` adapts a sender into one that completes on a <del>specified scheduler</del><ins>receiver’s scheduler</ins>. If the algorithm determines that the adapted sender already completes on the correct scheduler it can avoid any scheduling operation.

2 The name `affine_on` denotes a pipeable sender adaptor object. For <ins>a</ins> subexpression<del>s sch and</del> `sndr`, if <del>`decltype((sch))` does not satisfy scheduler, or</del> `decltype((sndr))` does not satisfy sender, `affine_on(sndr<del>, sch</del>)` is ill-formed.

3 Otherwise, the expression `affine_on(sndr<del>, sch</del>)` is expression-equivalent to `*make-sender*(affine_on, <del>sch</del><ins>env<>()</ins>, sndr)`.

? Let `sndr` and `ev` be subexpressions such that `Sndr` is `decltype((sndr))`. If `*sender-for*<Sndr, affine_on_t>` is `false`, then the expression `affine_on.transform_sender(sndr, ev)` is ill-formed; otherwise, it is equal to:

```cpp
auto&[_, _, child] = sndr;
using child_tag_t = tag_of_t<remove_cvref_t<decltype(child)>>;
if constexpr (requires(const child_tag_t& t){ t.affine_on(child, ev); })
    return t.affine_on(child, ev);
else
    return continues_on(child, UNSTOPPABLE-SCHEDULER(get_start_scheduler(ev)));
```

?

For a subexpression

sch

whose
type satisfies

scheduler

, let

*UNSTOPPABLE-SCHEDULER*(sch)

be an expression

e

whose type
satisfies

scheduler

such that:

- (?.1) `schedule(e)` is expression-equivalent to `unstoppable(schedule(sch))`.
- (?.2) For any query object `q` and pack of subexpressions `args...`, `e.query(q, args...)` is expression-equivalent to `sch.query(q, args...)`.
- (?.3) Let `f` be the subexpression `*UNSTOPPABLE-SCHEDULER*(other)`. `e == f` is expression-equivalent to `sch == other`.

? *Recommended Practice*: Implementations should provide `affine_on` member functions for senders which are known to resume on the scheduler where they were started. Example senders for which that is the case are `just`, `just_error`, `just_stopped`, `read_env`, and `write_env`.

5 Let `*out_sndr*` be a subexpression denoting a sender returned from `affine_on(sndr<del>, sch</del>)` 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`. <ins>Let `*sch*` be the result of the expression `get_start_scheduler(get_env(*out_rcvr*))`. If the completion signatures of `schedule(*sch*)` contain a different completion signature than `set_value_t()` when using an environment where `get_stop_token()` returns an `unstoppable_token`, the expression `connect(*out_sndr*, *out_rcvr*)` is ill-formed.</ins> Let `op` be an lvalue referring to the operation state that results from connecting `*out_sndr*` to `*out_rcvr*`. Calling `start(*op*)` will start `sndr` on the current execution agent and execute completion operations on `*out_rcvr*` on an execution agent of the execution resource associated with <del>`sch`</del><ins>`*sch*`</ins>. If the current execution resource is the same as the execution resource associated with <del>`sch`</del><ins>`*sch*`</ins>, the completion operation on `*out_rcvr*` may be called before `start(*op*)` completes. <del>If scheduling onto `sch` fails, an error completion on `*out_rcvr*` shall be executed on an unspecified execution agent.</del>

[ Editor's note: Change [exec.affine.on] to use only one parameter, require an infallible scheduler from the receiver, and add a default implementation which allows customization of `affine_on` for child senders. If `get_start_scheduler` is not introduced the algorithms should use `get_scheduler` to get the start scheduler: ]

1 `affine_on` adapts a sender into one that completes on a <del>specified scheduler</del><ins>receiver’s scheduler</ins>. If the algorithm determines that the adapted sender already completes on the correct scheduler it can avoid any scheduling operation.

2 The name `affine_on` denotes a pipeable sender adaptor object. For <ins>a</ins> subexpression<del>s sch and</del> `sndr`, if <del>`decltype((sch))` does not satisfy scheduler, or</del> `decltype((sndr))` does not satisfy sender, `affine_on(sndr<del>, sch</del>)` is ill-formed.

3 Otherwise, the expression `affine_on(sndr<del>, sch</del>)` is expression-equivalent to `*make-sender*(affine_on, <del>sch</del><ins>env<>()</ins>, sndr)`.

? Let `sndr` and `ev` be subexpressions such that `Sndr` is `decltype((sndr))`. If `*sender-for*<Sndr, affine_on_t>` is `false`, then the expression `affine_on.transform_sender(sndr, ev)` is ill-formed; otherwise, it is equal to:

```cpp
auto&[_, _, child] = sndr;
using child_tag_t = tag_of_t<remove_cvref_t<decltype(child)>>;
if constexpr (requires(const child_tag_t& t){ t.affine_on(child, ev); })
    return t.affine_on(child, ev);
else
    return continues_on(child, UNSTOPPABLE-SCHEDULER(get_scheduler(ev)));
```

?

For a subexpression

sch

whose
type satisfies

scheduler

, let

*UNSTOPPABLE-SCHEDULER*(sch)

be an expression

e

whose type
satisfies

scheduler

such that:

- (?.1) `schedule(e)` is expression-equivalent to `unstoppable(schedule(sch))`.
- (?.2) For any query object `q` and pack of subexpressions `args...`, `e.query(q, args...)` is expression-equivalent to `sch.query(q, args...)`.
- (?.3) Let `f` be the subexpression `*UNSTOPPABLE-SCHEDULER*(other)`. `e == f` is expression-equivalent to `sch == other`.

? *Recommended Practice*: Implementations should provide `affine_on` member functions for senders which are known to resume on the scheduler where they were started. Example senders for which that is the case are `just`, `just_error`, `just_stopped`, `read_env`, and `write_env`.

5 Let `*out_sndr*` be a subexpression denoting a sender returned from `affine_on(sndr<del>, sch</del>)` 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`. <ins>Let `*sch*` be the result of the expression `get_scheduler(get_env(*out_rcvr*))`. If the completion signatures of `schedule(*sch*)` contain a different completion signature than `set_value_t()` when using an environment where `get_stop_token()` returns an `unstoppable_token`, the expression `connect(*out_sndr*, *out_rcvr*)` is ill-formed.</ins> Let `op` be an lvalue referring to the operation state that results from connecting `*out_sndr*` to `*out_rcvr*`. Calling `start(*op*)` will start `sndr` on the current execution agent and execute completion operations on `*out_rcvr*` on an execution agent of the execution resource associated with <del>`sch`</del><ins>`*sch*`</ins>. If the current execution resource is the same as the execution resource associated with <del>`sch`</del><ins>`*sch*`</ins>, the completion operation on `*out_rcvr*` may be called before `start(*op*)` completes. <del>If scheduling onto `sch` fails, an error completion on `*out_rcvr*` shall be executed on an unspecified execution agent.</del>

[ Editor's note: Remove `change_coroutine_scheduler` from [execution.syn]: ]

```cpp
namespace std::execution {
  ...
  // [exec.task.scheduler], task scheduler
  class task_scheduler;

  template<class E>
  struct with_error {
    using type = remove_cvref_t<E>;
    type error;
  };
  template<class E>
    with_error(E) -> with_error<E>;
```

```cpp
  template<scheduler Sch>
  struct change_coroutine_scheduler {
    using type = remove_cvref_t<Sch>;
    type scheduler;
  };
  template<scheduler Sch>
    change_coroutine_scheduler(Sch) -> change_coroutine_scheduler<Sch>;
```

```cpp
  // [exec.task], class template task
  template<class T, class Environment>
    class task;
  ...
}
```

[ Editor's note: Adjust the use of `affine_on` and remove `change_coroutine_scheduler` from [task.promise]: ]

```cpp
namespace std::execution {
  template<class T, class Environment>
  class task<T, Environment>::promise_type {
  public:
    ...

    template<class A>
      auto await_transform(A&& a);
```

```cpp
    template<class Sch>
      auto await_transform(change_coroutine_scheduler<Sch> sch);
```

```cpp
    unspecified get_env() const noexcept;

    ...
  }
};
```

…

```cpp
template<sender Sender>
  auto await_transform(Sender&& sndr) noexcept;
```

9 *Returns*: If `same_as<inline_scheduler, scheduler_type>` is `true` returns `as_awaitable(std::forward<Sender>(sndr), *this);` otherwise returns `as_awaitable(affine_on(std::forward<Sender>(sndr)<del>, SCHED(*this)</del>), *this)`.

```cpp
template<class Sch>
  auto await_transform(change_coroutine_scheduler<Sch> sch) noexcept;
```

10 *Effects*: Equivalent to:

```cpp
return await_transform(just(exchange(SCHED(*this), scheduler_type(sch.scheduler))), *this);
```

```cpp
void unhandled_exception();
```

11 *Effects*: If the signature `set_error_t(exception_ptr)` is not an element of `error_types`, calls `terminate()` ([except.terminate]). Otherwise, stores `current_exception()` into `*errors*`.

…

[ Editor's note: In [exec.task.scheduler] change the constructor of `task_scheduler` to require that the scheduler passed is infallible ]

```cpp
template<class Sch, class Allocator = allocator<void>>
  requires(!same_as<task_scheduler, remove_cvref_t<Sch>>) && scheduler<Sch>
explicit task_scheduler(Sch&& sch, Allocator alloc = {});
```

? *Mandates*: Let `E` be the type of a queryable. If `unstoppable_token<stop_token_of_t<E>>` is `true`, then the type `completion_signatures_of_t<schedule_result_t<Sch>, E>` only includes `set_value_t()`, otherwise it may additionally include `set_stopped_t()`.

2 *Effects*: Initialize `*sch_*` with `allocate_shared<remove_cvref_t<Sch>>(alloc, std::forward<Sch>(sch))`.

3 *Recommended practice*: Implementations should avoid the use of dynamically allocated memory for small scheduler objects.

4 *Remarks*: Any allocations performed by construction of `*ts-sender*` or `*state*` objects resulting from calls on `*this` are performed using a copy of `alloc`.

[ Editor's note: In [exec.task.scheduler] change the `*ts-sender*` completion signatures to indicate that `task_scheduler` is infallible: ]

8

```cpp
namespace std::execution {
  class task_scheduler::ts-sender {     // exposition only
  public:
    using sender_concept = sender_t;

    template<receiver Rcvr>
      state<Rcvr> connect(Rcvr&& rcvr) &&;
  };
}
```

`*ts-sender*` is an exposition-only class that models `sender` ([exec.snd]) and for which `completion_signatures_of_t<*ts-sender*<ins>, E</ins>>` denotes<del>:</del><ins>`completion_signatures<set_value_t()>` if `unstoppable_token<stop_token_of_t<E>>` is `true`, and otherwise `completion_signatures<set_value_t(), set_stopped_t()>`.</ins>

```cpp
completion_signatures<
  set_value_t(),
  set_error_t(error_code),
  set_error_t(exception_ptr),
  set_stopped_t()>
```

[ Editor's note: In [exec.run.loop.types] change the paragraph defining the completion signatures: ]

…

```cpp
class run-loop-sender;
```

6 `*run-loop-sender*` is an exposition-only type that satisfies `sender`. <ins>Let `E` be the type of an environment. If `unstoppable_token<stop_token_of_t<E>>` is `true`, then</ins> `completion_signatures_of_t<*run-loop-sender*<ins>, E</ins>>` is

```cpp
  completion_signatures<set_value_t(), set_error_t(exception_ptr), set_stopped_t()>
```

```cpp
  completion_signatures<set_value_t()>
```

Otherwise it is

```cpp
  completion_signatures<set_value_t(), set_stopped_t()>
```

7 An instance of `*run-loop-sender*` remains valid until the end of the lifetime of its associated `run_loop` instance.

…
