diff --git a/docs/P3941-affinity.md b/docs/P3941-affinity.md index 69d175f..3fc1f2d 100644 --- a/docs/P3941-affinity.md +++ b/docs/P3941-affinity.md @@ -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) @@ -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` @@ -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: @@ -565,6 +578,8 @@ namespace std::execution { struct get_forward_progress_guarantee_t { @_unspecified_@ }; template struct get_completion_scheduler_t { @_unspecified_@ }; + template + struct get_completion_domain_t { @_unspecified_@ }; struct get_await_completion_adaptor_t { @_unspecified_@ }; inline constexpr get_domain_t get_domain{}; @@ -575,6 +590,8 @@ namespace std::execution { inline constexpr get_forward_progress_guarantee_t get_forward_progress_guarantee{}; template constexpr get_completion_scheduler_t get_completion_scheduler{}; + template + constexpr get_completion_domain_t get_completion_domain{}; inline constexpr get_await_completion_adaptor_t get_await_completion_adaptor{}; ... } @@ -609,13 +626,30 @@ 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 SCHED-ENV +use `get_start_scheduler` instead of `get_scheduler`: ::: +[8]{.pnum} SCHED-ENV(sch) is an expression `o2` whose type +satisfies queryable 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`: :::

@@ -623,41 +657,31 @@ to use `get_start_scheduler`:

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

``` auto&& [_, data, child] = out_sndr; if constexpr (scheduler) { auto orig_sch = - @_query-with-default_@(get_@[start_]{.add}@scheduler, env, @_not-a-scheduler_@()); - - if constexpr (same_as) { - return @_not-a-sender_@{}; - } else { - return continues_on( - starts_on(std::forward_like(data), std::forward_like(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(data), std::forward_like(child)), + std::move(orig_sch)); } else { auto& [sch, closure] = data; - auto orig_sch = @_query-with-default_@( - get_completion_scheduler, - get_env(child), - @_query-with-default_@(get_@[start_]{.add}@scheduler, env, @_not-a-scheduler_@())); - - if constexpr (same_as) { - return @_not-a-sender_@{}; - } else { - return write_env( - continues_on( - std::forward_like(closure)( - continues_on( - write_env(std::forward_like(child), @_SCHED-ENV_@(orig_sch)), - sch)), - orig_sch), - @_SCHED-ENV_@(sch)); - } + auto orig_sch = @_call-with-default_@( + get_completion_scheduler, @_not-a-scheduler_@(), get_env(child), env); + + return continues_on( + @[write_env(]{.rm}@ + std::forward_like(closure)( + continues_on( + @[write_env(]{.rm}@std::forward_like(child)@[,]{.rm}@ @[_SCHED-ENV_(orig_sch))]{.rm}@, + sch)), + @[_SCHED-ENV_(sch)),]{.rm}@ + orig_sch); } ```

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

    +
  • +

    [7.1]{.pnum} +`expr.as_awaitable(p)` if that expression is well-formed. +

    +

    +Mandates: `@_is-awaitable_@` is `true`, where `A` is +the type of the expression above. +

    +
  • +
  • + +[7.?]{.pnum} + +[`@_adapt-for-await-completion_@(transform_sender(expr, get_env(p))).as_awaitable(p)` +if this expression is well-formed, `sender_in>` is `true`, +and `@_single-sender-value-type_@>` is well-formed.]{.add} + +
  • +
  • +[7.2]{.pnum} Otherwise, `(void(p), expr)` if +`decltype(@_GET-AWAITER_@(expr))` satisfies `@_is-awaiter_@`. +
  • +
  • +

    +[7.3]{.pnum} [Otherwise, `@_sender-awaitable_@{@_adapted-expr_@, p}` if]{.rm} +

    +

    [`@_has-queryable-await-completion-adaptor_@`]{.rm}

    +

    [and]{.rm}

    +

    [`@_awaitable-sender_@`]{.rm}

    +

    +[are both satisfied, where `@_adapted-expr_@` is +`get_await_completion_adaptor(get_env(expr))(expr)`, except that +`expr` is evaluated only once.]{.rm} +

    +
  • +
  • +[7.4]{.pnum} Otherwise, [`@_sender-awaitable_@{expr, p}` if +`@_awaitable-sender_@` is `true`.]{.rm} +[`@_sender-awaitable_@{@_adapt-for-await-completion_@(transform_sender(expr, get_env(p))), P}` if `sender_in>` is `true` and `@_single-sender-value-type_@>` is well-formed]{.add} + +
  • +
  • [7.5]{.pnum} Otherwise, `(void(p), expr)`. +
  • +
+ +[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 @@ -770,26 +859,8 @@ satisfy sender, affine_on(sndr[, sch]{.rm}) is ill-formed. [3]{.pnum} Otherwise, the expression affine_on(sndr[, sch]{.rm}) -is expression-equivalent to: -transform_sender(_get-domain-early_(sndr), _make-sender_(affine_on, -[sch]{.rm}[env<>()]{.add}, sndr)) except that `sndr` -is evaluated only once. - -[4]{.pnum} -The exposition-only class template _impls-for_ -([exec.snd.expos]) is specialized for `affine_on_t` as follows: - -```c++ -namespace std::execution { - template<> - struct impls-for : 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<>()]{.add}@, sndr)`. :::{.add} [?]{.pnum} @@ -805,18 +876,19 @@ using child_tag_t = tag_of_t>; 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: +
    +
  • [?.1]{.pnum} `schedule(e)` is expression-equivalent to `unstoppable(schedule(sch))`.
  • +
  • [?.2]{.pnum} For any query object `q` and pack of subexpressions `args...`, `e.query(q, args...)` +is expression-equivalent to `sch.query(q, args...)`.
  • +
  • [?.3]{.pnum} Let `f` be the subexpression `@_UNSTOPPABLE-SCHEDULER_@(other)`. `e == f` +is expression-equivalent to `sch == other`.
  • +
[?]{.pnum} _Recommended Practice_: Implementations should provide `affine_on` @@ -875,26 +947,8 @@ satisfy sender, affine_on(sndr[, sch]{.rm}) is ill-formed. [3]{.pnum} Otherwise, the expression affine_on(sndr[, sch]{.rm}) -is expression-equivalent to: -transform_sender(_get-domain-early_(sndr), _make-sender_(affine_on, -[sch]{.rm}[env<>()]{.add}, sndr)) except that `sndr` -is evaluated only once. - -[4]{.pnum} -The exposition-only class template _impls-for_ -([exec.snd.expos]) is specialized for `affine_on_t` as follows: - -```c++ -namespace std::execution { - template<> - struct impls-for : 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<>()]{.add}@, sndr)`. :::{.add} [?]{.pnum} @@ -910,18 +964,19 @@ using child_tag_t = tag_of_t>; 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: +
    +
  • [?.1]{.pnum} `schedule(e)` is expression-equivalent to `unstoppable(schedule(sch))`.
  • +
  • [?.2]{.pnum} For any query object `q` and pack of subexpressions `args...`, `e.query(q, args...)` +is expression-equivalent to `sch.query(q, args...)`.
  • +
  • [?.3]{.pnum} Let `f` be the subexpression `@_UNSTOPPABLE-SCHEDULER_@(other)`. `e == f` +is expression-equivalent to `sch == other`.
  • +
[?]{.pnum} _Recommended Practice_: Implementations should provide `affine_on` @@ -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` is `true`, then -the type `completion_signatures_of_t` +_Mandates_: Let `E` be the type of a queryable. +If `unstoppable_token>` is `true`, then +the type `completion_signatures_of_t, E>` only includes `set_value_t()`, otherwise it may additionally include `set_stopped_t()`. ::: @@ -1105,7 +1160,7 @@ namespace std::execution { ts-sender is an exposition-only class that models `sender` ([exec.snd]) and for which completion_signatures_of_t<ts-sender[, E]{.add}> -denotes[:]{.rm}[ `completion_signatures` if `unstoppable_token()))>` is `true`, and +denotes[:]{.rm}[ `completion_signatures` if `unstoppable_token>` is `true`, and otherwise `completion_signatures`.]{.add} ::: rm @@ -1128,9 +1183,9 @@ In [exec.run.loop.types] change the paragraph defining the completion signatures class run-loop-sender; ``` -[5]{.pnum} +[6]{.pnum} run-loop-sender is an exposition-only type that satisfies `sender`. -[Let `E` be the type of an environment. If `unstoppable_token()))>` is `true`, +[Let `E` be the type of an environment. If `unstoppable_token>` is `true`, then ]{.add} completion_signatures_of_t<run-loop-sender[, E]{.add}> is ::: rm @@ -1149,7 +1204,7 @@ Otherwise it is ``` ::: -[6]{.pnum} An instance of run-loop-sender remains +[7]{.pnum} An instance of run-loop-sender remains valid until the end of the lifetime of its associated `run_loop` instance. diff --git a/examples/issue-symmetric-transfer.cpp b/examples/issue-symmetric-transfer.cpp index a029cd7..f6455cf 100644 --- a/examples/issue-symmetric-transfer.cpp +++ b/examples/issue-symmetric-transfer.cpp @@ -4,27 +4,26 @@ #include #include #include -#define NODEBUG -#include namespace ex = beman::execution; template ex::task test() { for (std::size_t i{}; i < 1000000; ++i) { - // co_await std::invoke([]() -> ex::task<> { co_await ex::just(); }); - co_await std::invoke([]() -> ex::task<> { co_return; }); + co_await std::invoke([]() -> ex::task<> { co_await ex::when_all(ex::just()); }); } } -int main() { - struct affine_env {}; - struct inline_env { - using scheduler_type = ex::inline_scheduler; - }; - [[maybe_unused]] auto env = affine_env{}; - [[maybe_unused]] auto sched = inline_env::scheduler_type{}; +struct affine_env {}; +struct inline_env { + using scheduler_type = ex::inline_scheduler; +}; - // ex::sync_wait(test()); // OK +int main() { + [[maybe_unused]] affine_env ae{}; + [[maybe_unused]] inline_env ie{}; +#ifndef _MSC_VER + ex::sync_wait(test()); // OK +#endif // ex::sync_wait(test()); // error: stack overflow without symmetric transfer }