run_loop::finish puts the run_loop into the finishing state so that the next time the work queue is empty, run_loop::run will return instead of waiting for more work.
calling .finish() on a run_loop instance can potentially throw (finish() is not marked noexcept). that is because one valid implementation involves acquiring a lock on a std::mutex -- a potentially throwing operation.
but failing to put the run_loop into the finishing state is problematic in the same way that a failing destructor is problematic: shutdown and clean-up code depends on it succeeding.
consider sync_wait's use of run_loop:
sync-wait-state<Sndr> state;
auto op = connect(sndr, sync-wait-receiver<Sndr>{&state});
start(op);
state.loop.run();
if (state.error) {
rethrow_exception(std::move(state.error));
}
return std::move(state.result);
it is the job of sync-wait-receiver to put the run_loop into the finishing state so that the invocation of state.loop.run() will return. it does that in its completion functions, like so:
void set_stopped() && noexcept;
Effects: Equivalent to state->loop.finish().
here we are not handling the fact that state->loop.finish() is potentially throwing. given that this function is noexcept, this will lead to the application getting terminated. not good.
but even if we handle the exception and save it into state.result to be rethrown later, we still have a problem. since run_loop::finish() threw, the run_loop has not been placed into the finishing state. that means that state.loop.run() will never return, and sync_wait will hang forever.
simply put, run_loop::finish() has to be noexcept. the implementation must find a way to put the run_loop into the finishing state. if it cannot, it should terminate. throwing an exception and foisting the problem on the caller -- who has no recourse -- is simply wrong.
Proposed Resolution:
Change run_loop::finish() no-throw. in [exec.run.loop.general], change the definition of the run_loop class as follows:
namespace std::execution {
class run_loop {
// [exec.run.loop.types], associated types
class run-loop-scheduler; // exposition only
class run-loop-sender; // exposition only
struct run-loop-opstate-base { // exposition only
virtual void execute() = 0; // exposition only
run_loop* loop; // exposition only
run-loop-opstate-base* next; // exposition only
};
template<class Rcvr>
using run-loop-opstate = unspecified; // exposition only
// [exec.run.loop.members], member functions
run-loop-opstate-base* pop-front(); // exposition only
void push-back(run-loop-opstate-base*); // exposition only
public:
// [exec.run.loop.ctor], constructor and destructor
run_loop() noexcept;
run_loop(run_loop&&) = delete;
~run_loop();
// [exec.run.loop.members], member functions
run-loop-scheduler get_scheduler();
void run();
- void finish();
+ void finish() noexcept;
};
}
change [exec.run.loop.mempers] p8 as follows:
-void finish();
+void finish() noexcept;
- Preconditions: state is either starting or running.
- Effects: Changes state to finishing.
- Synchronization:
finish synchronizes with the pop-front operation that returns nullptr.
run_loop::finishputs therun_loopinto the finishing state so that the next time the work queue is empty,run_loop::runwill return instead of waiting for more work.calling
.finish()on arun_loopinstance can potentially throw (finish()is not markednoexcept). that is because one valid implementation involves acquiring a lock on astd::mutex-- a potentially throwing operation.but failing to put the
run_loopinto the finishing state is problematic in the same way that a failing destructor is problematic: shutdown and clean-up code depends on it succeeding.consider
sync_wait's use ofrun_loop:it is the job of
sync-wait-receiverto put therun_loopinto the finishing state so that the invocation ofstate.loop.run()will return. it does that in its completion functions, like so:here we are not handling the fact that
state->loop.finish()is potentially throwing. given that this function isnoexcept, this will lead to the application getting terminated. not good.but even if we handle the exception and save it into
state.resultto be rethrown later, we still have a problem. sincerun_loop::finish()threw, therun_loophas not been placed into the finishing state. that means thatstate.loop.run()will never return, andsync_waitwill hang forever.simply put,
run_loop::finish()has to benoexcept. the implementation must find a way to put therun_loopinto the finishing state. if it cannot, it shouldterminate. throwing an exception and foisting the problem on the caller -- who has no recourse -- is simply wrong.Proposed Resolution:
Change
run_loop::finish()no-throw. in [exec.run.loop.general], change the definition of therun_loopclass as follows:change [exec.run.loop.mempers] p8 as follows: