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?
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_stoppedcompletions 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_stoppedcompletions, i.e. completions that don't simply propagate a cancellation request whenex::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
SIGINTsignal. I can write a sender which listens to this and results in aset_stoppedcompletion (with some imaginarysignal_setclass and aasync_waitfunction):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_scopeand have it automaticallyrequest_stopthe scope when a signal arrives.This would match the design of
when_any, in that it also callsrequest_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_stoppedcompletions.I'm implementing it like this:
simple_counting_scopewould not allow spawning a child withset_stoppedcompletions. 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 forsimple_counting_scopethis is OK. Dropping stop requests may in some cases as unexpected as dropping errors, especially when you think about cancellations as "uncatchable exceptions".counting_scopewould allow children that complete withset_stopped. Unhandledset_stoppedcompletions would lead to arequest_stop()call on the stop source of the scope. I've implemented this by giving thecounting_scope::assocclass arequest_stop()method and calling it like this after the "opstate + assoc" struct of the spawned child task is destroyed:...where
stoppable_associationis aasync_scope_associationthat supportsrequest_stop():request_stop()method to thecounting_scope::tokenclass. This is just a useful thing to have and would be equivalent tospawn(just_stopped(), token).What do you think?