Skip to content

Should set_stopped completions of children be ignored? #40

Description

@jiixyj

First: Thank you for making structured concurrency in standard C++ a reality! I've been implementing the proposal to play around with it and get a feeling for how it works.

At the moment, set_stopped completions of children are ignored. I'm wondering if the better option would be to send a stop request to the scope instead. This is especially important for "naked" set_stopped completions, i.e. completions that don't simply propagate a cancellation request when ex::get_stop_token(rcvr).stop_requested().

Those "naked" stop completions in my mind are ones that come from "outside" the system in some way -- one prime example is the user pressing ctrl+c resulting in a SIGINT signal. I can write a sender which listens to this and results in a set_stopped completion (with some imaginary signal_set class and a async_wait function):

    extra::signal_set sset{SIGINT, SIGTERM, SIGHUP};
    extra::signal_blocker _{SIGINT, SIGTERM, SIGHUP};

    ex::sender auto signal_waiter = extra::async_wait(&sset) | ex::let_value([](auto sig, const auto& /* info */) {
        std::println("caught signal {}!", sig);
        return ex::just_stopped();
    });

I would like to spawn this sender in a counting_scope and have it automatically request_stop the scope when a signal arrives.

This would match the design of when_any, in that it also calls request_stop() on its stop source when a child exits with a stop completion. If I understand the design intent of this correctly, this is just for those "naked" set_stopped completions.

I'm implementing it like this:

  • simple_counting_scope would not allow spawning a child with set_stopped completions. This would strip down the responsibility of this class to just counting children -- stop requests would have to be handled manually. This is the "boilerplate option" of "6.1.3", but I think for simple_counting_scope this is OK. Dropping stop requests may in some cases as unexpected as dropping errors, especially when you think about cancellations as "uncatchable exceptions".
  • counting_scope would allow children that complete with set_stopped. Unhandled set_stopped completions would lead to a request_stop() call on the stop source of the scope. I've implemented this by giving the counting_scope::assoc class a request_stop() method and calling it like this after the "opstate + assoc" struct of the spawned child task is destroyed:
        void cleanup(association_from<Token> assoc, rebound_allocator_t alloc, bool stopped) && noexcept
        {
            this->opstate_and_assoc_t::~opstate_and_assoc_t();
            alloc.deallocate(this, 1);

            if constexpr (stoppable_association<association_from<Token>>) {
                if (stopped) {
                    assoc.request_stop();
                }
            }
        }

...where stoppable_association is a async_scope_association that supports request_stop():

template<class Assoc>
concept stoppable_association =       //
    async_scope_association<Assoc> && //
    requires(const Assoc& assoc) {
        { assoc.request_stop() } noexcept -> std::same_as<void>;
    };
  • I also gave a request_stop() method to the counting_scope::token class. This is just a useful thing to have and would be equivalent to spawn(just_stopped(), token).

What do you think?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions