Skip to content

troubles with the customizations of continues_on and schedule_from #337

Open
@ericniebler

Description

@ericniebler

Background: continues_on and schedule_from:

std::execution has two customizable algorithms for transfering execution from one context to another: continues_on and schedule_from. The reason for having two is due to the fact there are two execution contexts in play: the context we're transitioning from and the one we're transitioning to. The source context may need special sauce to transfer execution back to the CPU, and the destination context may need special sauce to transition from the CPU onto it.

The schedule_from algorithm looks for customizations based on the domain of the destination, and the continues_on algorithm dispatches based on the domain of the source. A "domain" is a tag type associated with an execution context that is used to find algorithm customizations for that context. The continues_on algorithm is required to lower to the result of a call to schedule_from. In this way, every context transition gets all the special sauce it needs to get from one arbitrary context to another, even if the two contexts know nothing about each other.

We can see this in the definitions of the continues_on and schedule_from customizations points:

Algorithm Returns
continues_on(sndr, sched) transform_sender(get-domain-early(sndr), make-sender(continues_on, sched, sndr))
schedule_from(sched, sndr) transform_sender(query-or-default(get_domain, sched, default_domain{}), make-sender(schedule_from, sched, sndr))

By asking for the predecessor sender's domain, continues_on uses the domain of the source to find its customization. And by asking for the scheduler's domain, schedule_from uses the domain of the destination.

The final piece is the transformation, within the connect customization point, of the continues_on sender to the schedule_from sender, which is done with the continues_on.transform_sender(Sndr, Env) member function (see [exec.continues.on] p5).


Problem 1

When connect-time customization was added to std::execution in
[@P3303], the logic of continues_on/schedule_from
customization accidentally got reversed: The exposition-only get-domain-late function,
which is called from connect, is used to determine the domain to use to find a sender
transform function. It says:

template<class Sndr, class Env>
  constexpr auto get-domain-late(const Sndr& sndr, const Env& env) noexcept;
  1. Effects: Equivalent to:

    1. If sender-for<Sndr, continues_on_t> is true, then
    return Domain();
    

    where Domain is the type of the following expression:

    [] {
      auto [_, sch, _] = sndr;
      return query-or-default(get_domain, sch, default_domain());
    }();
    

    [Note 1: 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. — end
    note
    ]

    1. 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()

Paragraph 14.1 above gets the roles of continues_on and schedule_from mixed up. They should be reversed.


Problem 2

All of the adaptor algorithm CPOs use the domain of the predecessor(s) to find customizations. For example, then(sndr, fn) returns transform_sender(get-domain-early(sndr), make-sender(then, fn, sndr)); i.e., the domain is pulled from sndr. A sender that advertizes a domain is making an assertion about where it will complete. Where the predecessor completes is where the current sender will start. Therefore, then(sndr, fn) is dispatching based on where the then sender will start.

If we look at the connect customization point at how a late customization is found, we see that before it does anything else, it transforms the input sender as follows:

transform_sender(decltype(get-domain-late(sndr, get_env(rcvr))){}, sndr, get_env(rcvr))

We can see that when passed a then sender, we ask the then sender for its domain (and use the domain of the receiver's env as a fallback). That means that for then senders, connect dispatches to a customization based on the domain of the then sender itself. That is different from early customization, which used the domain of the predecessor. The inconsistency is not intentional.

For then and most other adaptors, it doesn't make any difference. The then sender completes wherever its predecessor completes, so the "start" and "complete" domains are the same. That is not the case, however, for continues_on. The domain on which it starts can be different from the domain on which it completes.

The tl;dr of problem 2 is that connect uses the wrong domain to find a customization for continues_on. The domain of the continues_on(sndr, sched) sender is the domain on which it will complete, which is the domain of sched. But continues_on should be using the domain on which it starts to find a customization.

A principled solution would be to recognize that a sender really has 2 associated domains: the starting domain and the completing domain. it follows that we should have two queries: get_starting_domain and get_completion_domain. Early customization would use the completion domain of the predecessor, whereas late customization would use the starting domain of the sender itself.

A simpler solution recognizes that for all adaptors besides continues_on, the starting domain and the completion domain are the same, so separate queries are not needed. (This is even true for the schedule_from sender: early or late, it should always use the domain of the destination scheduler). So we can leave connect alone and treat continues_on as special in get-domain-late.


Back to First Principles

Given what we already know about why schedule_from and continues_on both exist and find customization the way they do, we can build a table that shows how customizations should be selected for all the scenarios of interest. Consider the sender expression A | continues_on(Sch) | B. The table below shows the domain that each algorithm should use to find a customization.

Table 2: Domain that should be used to find customizations in A | continues_on(Sch) | B

Algorithm Early customization uses... Late customization uses... get_domain(get_env(ALGO(A...))) should return...
schedule_from domain of Sch or default_domain domain of Sch or default_domain domain of Sch
continues_on domain of A or default_domain domain of A or domain of Env domain of Sch
B domain of Sch or default_domain domain of Sch or domain of Env n/a

We can use this table to correct the effected parts of the spec:

  1. get-domain-late(sndr, env) should be changed to:

    1. when sndr is completes_on(Pred,Sch), return the domain of Pred and use domain of env as a fallback.
    2. when sndr is schedule_from(Sch,Pred), return the domain of Sch and use default_domain as a fallback (ignoring env).
    3. otherwise, return the domain of sndr and use domain of env as a fallback.
  2. Change the SCHED-ATTRS and SCHED-ENV pseudo-macros from [exec.snd.expos] to
    accept a fallback environment in addition to a scheduler, but never use the fallback
    env to answer the get_domain query.

  3. Change all uses of SCHED-ATTRS and SCHED-ENV as appropriate.

Proposed Resolution

[Editorial note: Change [exec.snd.expos]/p6 as follows:]

  1. For a scheduler sch and environment env,
    SCHED-ATTRS(sch, env) is an expression o1 whose type
    satisfies queryable such that: [Editorial note: reformatted as a list.]

    1. o1.query(get_completion_scheduler<Tag
      set_value_t>)
      is an expression with the same type and value as
      sch where Tag is one of set_value_t or set_stopped_t, and such
      that

    2. o1.query(get_completion_scheduler<Tag>) is ill-formed for Tag
      other than set_value_t,

    3. o1.query(get_domain) is expression-equivalent to sch.query(get_domain),
      and

    4. For a pack of subexpressions as and query object Q such that
      forwarding_query(Q) is true, o1.query(Q, as...) is
      expression-equivalent to env.query(Q, as...)
      .

    SCHED-ATTRS(sch) is expression-equivalent to
    SCHED-ATTRS(sch, execution::env<>{}).

    SCHED-ENV(sch, env) is an expression o2 whose type
    satisfies queryable such that: [Editorial note: reformatted as a list.]

    1. o2.query(get_scheduler) is a prvalue with the same type and value as sch, and
      such that

    2. o2.query(get_domain) is expression-equivalent to
      sch.query(get_domain)., and

    3. For a pack of subexpressions as and query object Q such that
      forwarding_query(Q) is true, o1.query(Q, as...) is
      expression-equivalent to env.query(Q, as...)
      .

    SCHED-ENV(sch) is expression-equivalent to
    SCHED-ENV(sch, execution::env<>{}).

[Editorial note: Change [exec.snd.expos]/p14 as follows:]

template<class Sndr, class Env>
  constexpr auto get-domain-late(const Sndr& sndr, const Env& env) noexcept;
  1. Effects: Equivalent to:

    1. [Editorial note: Taken from 14.3 with edits.] Let
      DEFAULT-LATE-DOMAIN(S, E) be
      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()
    2. If sender-for<Sndr,
      continues_on_tschedule_from_t>
      is true, then

      return Domain();
      

      where Domain is the type of the following expression:

      
      [] {
        auto [_, sch, _] = sndr;
        return query-or-default(get_domain, sch, default_domain());
      }();
      

      [Note 1: The continues_onschedule_from algorithm works
      in tandem with schedule_from ([exec.schedule.from])continues_on
      ([exec.continues.on])
      to give scheduler authors a way to customize both how
      to transition ontooff of (continues_on) and off
      of
      onto (schedule_from) a given execution context. Thus,
      continues_onschedule_from 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_onschedule_from. That is why it is given
      special treatment here. — end note]

    1. Otherwise, if sender-for<Sndr, continues_on_t> is
      true, then

      return Domain();
      

      where Domain is the type of the following expression:

      [] {
        const auto& [_, _, child] = sndr;
        return DEFAULT-LATE-DOMAIN(child, env);
      }();
      
    1. Otherwise, DEFAULT-LATE-DOMAIN(sndr, env)

      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()

[Editorial note: Change [exec.starts.on]/p4.1 as follows:]

  1. Let [Editorial note: ...as before...]; otherwise:

    1. starts_on.transform_env(out_sndr, env) is equivalent to:

      auto&& [_, sch, _] = out_sndr;
      return JOIN-ENV(SCHED-ENV(sch), FWD-ENV(env));
      return SCHED-ENV(sch, env);

[Editorial note: Change [exec.continues.on]/p4 as follows:]

  1. The exposition-only class template impls-for is specialized for continues_on_t as follows:

    namespace std::execution {
      template<>
      struct impls-for<continues_on_t> : default-impls {
        static constexpr auto get-attrs =
          [](const auto& data, const auto& child) noexcept -> decltype(auto) {
            return JOIN-ENV(SCHED-ATTRS(data), FWD-ENV(get_env(child)));
            return SCHED-ATTRS(data, get_env(child));
          };
      };
    }

[Editorial note: Change [exec.on]/p7 as follows:]

  1. The expression on.transform_env(out_sndr, env) has effects equivalent to:

    auto&& [_, data, _] = out_sndr;
    if constexpr (scheduler<decltype(data)>) {
      return JOIN-ENV(SCHED-ENV(std::forward_like<OutSndr>(data)), FWD-ENV(std::forward<Env>(env)));
      return SCHED-ENV(std::forward_like<OutSndr>(data), std::forward<Env>(env));
    } else {
      return std::forward<Env>(env);
    }

Metadata

Metadata

Assignees

No one assigned

    Labels

    P0bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions