Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
253 changes: 154 additions & 99 deletions docs/P3941-affinity.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: Scheduler Affinity
document: P3941R2
date: 2026-02-23
document: P3941R3
date: 2026-03-21
audience:
- Concurrency Working Group (SG1)
- Library Evolution Working Group (LEWG)
Expand Down Expand Up @@ -31,6 +31,12 @@ 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 customization of nested
senders in [[exec.as.awaitable](https://wg21.link/exec.as.awaitable#7)]

## R2

- added requirement on `get_scheduler`/`get_start_scheduler`
Expand Down Expand Up @@ -549,6 +555,13 @@ algorithm a better name.

# Wording Changes

::: ednote
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.
:::

::: ednote
If `get_start_scheduler` is introduced add it to the synopsis in
[execution.syn] after `get_scheduler` as follows:
Expand All @@ -565,6 +578,8 @@ namespace std::execution {
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{};
Expand All @@ -575,6 +590,8 @@ namespace std::execution {
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{};
...
}
Expand Down Expand Up @@ -609,55 +626,62 @@ 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))`.
:::

::: ednote

If `get_start_scheduler` is introduced change
[[exec.snd.expos](https://wg21.link/exec.snd.expos)] paragraph 8 to have <code><i>SCHED-ENV</i></code>
use `get_start_scheduler` instead of `get_scheduler`:

:::

[8]{.pnum} <code><i>SCHED-ENV</i>(sch)</code> is an expression `o2` whose type
satisfies <code><i>queryable</i></code> such that `o2.query(@[get_scheduler]{.rm}@@[get_start_scheduler]{.add}@)`
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)`.


::: ednote
If `get_start_scheduler` is introduced change how `on` gets its
scheduler in [exec.on], i.e., change the use from `get_scheduler`
to use `get_start_scheduler`:
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`:
:::

<p>
...
</p>

<p>
The expression `on.transform_sender(out_sndr, env)` has effects equivalent to:
[8]{.pnum} Otherwise, the expression `on.transform_sender(set_value, out_sndr, env)` has effects equivalent to:
</p>

```
auto&& [_, data, child] = out_sndr;
if constexpr (scheduler<decltype(data)>) {
auto orig_sch =
@_query-with-default_@(get_@[start_]{.add}@scheduler, env, @_not-a-scheduler_@());

if constexpr (same_as<decltype(orig_sch), @_not-a-scheduler_@>) {
return @_not-a-sender_@{};
} else {
return continues_on(
starts_on(std::forward_like<OutSndr>(data), std::forward_like<OutSndr>(child)),
std::move(orig_sch));
}
@_call-with-default_@(@[get_scheduler]{.rm}@@[get_start_scheduler]{.add}@, @_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 = @_query-with-default_@(
get_completion_scheduler<set_value_t>,
get_env(child),
@_query-with-default_@(get_@[start_]{.add}@scheduler, env, @_not-a-scheduler_@()));

if constexpr (same_as<decltype(orig_sch), @_not-a-scheduler_@>) {
return @_not-a-sender_@{};
} else {
return write_env(
continues_on(
std::forward_like<OutSndr>(closure)(
continues_on(
write_env(std::forward_like<OutSndr>(child), @_SCHED-ENV_@(orig_sch)),
sch)),
orig_sch),
@_SCHED-ENV_@(sch));
}
auto orig_sch = @_call-with-default_@(
get_completion_scheduler<set_value_t>, @_not-a-scheduler_@(), get_env(child), env);

return continues_on(
@[write_env(]{.rm}@
std::forward_like<OutSndr>(closure)(
continues_on(
@[write_env(]{.rm}@std::forward_like<OutSndr>(child)@[,]{.rm}@ @[_SCHED-ENV_(orig_sch))]{.rm}@,
sch)),
@[_SCHED-ENV_(sch)),]{.rm}@
orig_sch);
}
```
<p>
Expand Down Expand Up @@ -748,6 +772,71 @@ scheduler `get_scheduler(get_env(rcvr))`.

:::

::: ednote

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

:::

[7]{.pnum} `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:

<ul>
<li>
<p>[7.1]{.pnum}
`expr.as_awaitable(p)` if that expression is well-formed.
</p>
<p>
Mandates: `@_is-awaitable_@<A, Promise>` is `true`, where `A` is
the type of the expression above.
</p>
</li>
<li>

[7.?]{.pnum}

[`@_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.]{.add}

</li>
<li>
[7.2]{.pnum} Otherwise, `(void(p), expr)` if
`decltype(@_GET-AWAITER_@(expr))` satisfies `@_is-awaiter_@<Promise>`.
</li>
<li>
<p>
[7.3]{.pnum} [Otherwise, `@_sender-awaitable_@{@_adapted-expr_@, p}` if]{.rm}
</p>
<p>[`@_has-queryable-await-completion-adaptor_@<Expr>`]{.rm}</p>
<p>[and]{.rm}</p>
<p>[`@_awaitable-sender_@<decltype((@_adapted-expr_@)), Promise>`]{.rm}</p>
<p>
[are both satisfied, where `@_adapted-expr_@` is
`get_await_completion_adaptor(get_env(expr))(expr)`, except that
`expr` is evaluated only once.]{.rm}
</p>
</li>
<li>
[7.4]{.pnum} Otherwise, [`@_sender-awaitable_@{expr, p}` if
`@_awaitable-sender_@<Expr, Promise>` is `true`.]{.rm}
[`@_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]{.add}

</li>
<li>[7.5]{.pnum} Otherwise, `(void(p), expr)`.
</li>
</ul>

[8]{.pnum} [The expression `@_adapt-for-await-completion_@(s)` is
`get_await_completion_adaptor(get_env(s))(s)` if that is well-formed,
and `s` otherwise.]{.add}

::: ednote
Change [exec.affine.on] to use only one parameter, require an
infallible scheduler from the receiver, and add a default implementation
Expand All @@ -770,26 +859,8 @@ satisfy sender, <code>affine_on(sndr[, sch]{.rm})</code> is ill-formed.

[3]{.pnum}
Otherwise, the expression <code>affine_on(sndr[, sch]{.rm})</code>
is expression-equivalent to:
<code>transform_sender(_get-domain-early_(sndr), _make-sender_(affine_on,
[sch]{.rm}[env&lt;&gt;()]{.add}, sndr))</code> except that `sndr`
is evaluated only once.

[4]{.pnum}
The exposition-only class template <code>_impls-for_</code>
([exec.snd.expos]) is specialized for `affine_on_t` as follows:

```c++
namespace std::execution {
template<>
struct impls-for<affine_on_t> : default-impls {
static constexpr auto get-attrs =
[](const auto&@[ data]{.rm}]@, const auto& child) noexcept -> decltype(auto) {
return @[_JOIN-ENV_(_SCHED-ATTRS_(data),_FWD-ENV_(]{.rm}@get_env(child)@[))]{.rm}@;
};
};
}
```
is expression-equivalent to
`@_make-sender_@(affine_on, @[sch]{.rm}@@[env&lt;&gt;()]{.add}@, sndr)`.

:::{.add}
[?]{.pnum}
Expand All @@ -805,18 +876,19 @@ 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 write_env(
schedule_from(get_start_scheduler(get_env(ev)), write_env(std::move(child), ev)),
JOIN-ENV(env{prop{get_stop_token, never_stop_token()}}, ev)
);
return continues_on(child, @_UNSTOPPABLE-SCHEDULER_@(get_start_scheduler(ev)));
```

[Note 1: This causes the `affine_on(sndr)` sender to become
`schedule_from(sch, sndr)` when it is connected with a receiver
`rcvr` whose execution domain does not customize `affine_on`,
for which `get_start_scheduler(get_env(rcvr))` is `sch`, and `affine_on`
isn't specialized for the child sender.
end note]
[?]{.pnum} For a subexpression `sch` whose type satisfies `scheduler`,
let `@_UNSTOPPABLE-SCHEDULER_@(sch)` be an expression `e` whose type
satisfies `scheduler` such that:
<ul>
<li>[?.1]{.pnum} `schedule(e)` is expression-equivalent to `unstoppable(schedule(sch))`.</li>
<li>[?.2]{.pnum} For any query object `q` and pack of subexpressions `args...`, `e.query(q, args...)`
is expression-equivalent to `sch.query(q, args...)`.</li>
<li>[?.3]{.pnum} Let `f` be the subexpression `@_UNSTOPPABLE-SCHEDULER_@(other)`. `e == f`
is expression-equivalent to `sch == other`.</li>
</ul>

[?]{.pnum}
_Recommended Practice_: Implementations should provide `affine_on`
Expand Down Expand Up @@ -875,26 +947,8 @@ satisfy sender, <code>affine_on(sndr[, sch]{.rm})</code> is ill-formed.

[3]{.pnum}
Otherwise, the expression <code>affine_on(sndr[, sch]{.rm})</code>
is expression-equivalent to:
<code>transform_sender(_get-domain-early_(sndr), _make-sender_(affine_on,
[sch]{.rm}[env&lt;&gt;()]{.add}, sndr))</code> except that `sndr`
is evaluated only once.

[4]{.pnum}
The exposition-only class template <code>_impls-for_</code>
([exec.snd.expos]) is specialized for `affine_on_t` as follows:

```c++
namespace std::execution {
template<>
struct impls-for<affine_on_t> : default-impls {
static constexpr auto get-attrs =
[](const auto&@[ data]{.rm}]@, const auto& child) noexcept -> decltype(auto) {
return @[_JOIN-ENV_(_SCHED-ATTRS_(data),_FWD-ENV_(]{.rm}@get_env(child)@[))]{.rm}@;
};
};
}
```
is expression-equivalent to
`@_make-sender_@(affine_on, @[sch]{.rm}@@[env&lt;&gt;()]{.add}@, sndr)`.

:::{.add}
[?]{.pnum}
Expand All @@ -910,18 +964,19 @@ 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 write_env(
schedule_from(get_scheduler(get_env(ev)), write_env(std::move(child), ev)),
JOIN-ENV(env{prop{get_stop_token, never_stop_token()}}, ev)
);
return continues_on(child, @_UNSTOPPABLE-SCHEDULER_@(get_scheduler(ev)));
```

[Note 1: This causes the `affine_on(sndr)` sender to become
`schedule_from(sch, sndr)` when it is connected with a receiver
`rcvr` whose execution domain does not customize `affine_on`,
for which `get_scheduler(get_env(rcvr))` is `sch`, and `affine_on`
isn't specialized for the child sender.
end note]
[?]{.pnum} For a subexpression `sch` whose type satisfies `scheduler`,
let `@_UNSTOPPABLE-SCHEDULER_@(sch)` be an expression `e` whose type
satisfies `scheduler` such that:
<ul>
<li>[?.1]{.pnum} `schedule(e)` is expression-equivalent to `unstoppable(schedule(sch))`.</li>
<li>[?.2]{.pnum} For any query object `q` and pack of subexpressions `args...`, `e.query(q, args...)`
is expression-equivalent to `sch.query(q, args...)`.</li>
<li>[?.3]{.pnum} Let `f` be the subexpression `@_UNSTOPPABLE-SCHEDULER_@(other)`. `e == f`
is expression-equivalent to `sch == other`.</li>
</ul>

[?]{.pnum}
_Recommended Practice_: Implementations should provide `affine_on`
Expand Down Expand Up @@ -1065,9 +1120,9 @@ explicit task_scheduler(Sch&& sch, Allocator alloc = {});

::: add
[?]{.pnum}
_Mandates_: Let `e` be an environment and let `E` be `decltype(e)`.
If `unstoppable_token<decltype(get_stop_token(e))>` is `true`, then
the type `completion_signatures_of_t<decltype(schedule(sch)), E>`
_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()`.
:::
Expand Down Expand Up @@ -1105,7 +1160,7 @@ namespace std::execution {
<code><i>ts-sender</i></code> is an exposition-only class that
models `sender` ([exec.snd]) and for which
<code>completion_signatures_of_t&lt;<i>ts-sender</i>[, E]{.add}&gt;</code>
denotes[:]{.rm}[ `completion_signatures<set_value_t()>` if `unstoppable_token<decltype(get_stop_token(declval<E>()))>` is `true`, and
denotes[:]{.rm}[ `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()>`.]{.add}

::: rm
Expand All @@ -1128,9 +1183,9 @@ In [exec.run.loop.types] change the paragraph defining the completion signatures
class run-loop-sender;
```

[5]{.pnum}
[6]{.pnum}
<code><i>run-loop-sender</i></code> is an exposition-only type that satisfies `sender`.
[Let `E` be the type of an environment. If `unstoppable_token<decltype(get_stop_token(declval<E>()))>` is `true`,
[Let `E` be the type of an environment. If `unstoppable_token<stop_token_of_t<E>>` is `true`,
then ]{.add} <code>completion_signatures_of_t&lt;<i>run-loop-sender</i>[, E]{.add}&gt;</code> is

::: rm
Expand All @@ -1149,7 +1204,7 @@ Otherwise it is
```
:::

[6]{.pnum} An instance of <code><i>run-loop-sender</i></code> remains
[7]{.pnum} An instance of <code><i>run-loop-sender</i></code> remains
valid until the end of the lifetime of its associated `run_loop`
instance.

Expand Down
Loading
Loading