Notify the closer while still holding the gate

A call scope used to drop the lock after decrementing the in-flight count and
notify afterwards. That left a window: the closer's predicate is already true
once the count reaches zero, so a scope preempted between the unlock and the
notify could come back to find the runtime torn down and signal a condition
variable that no longer exists. ThreadSanitizer reports it as a broadcast racing
the destroy.

The decrement and the notify now happen under one lock guard. The closer cannot
return from its wait until the scope releases the gate, so the condition
variable is alive for as long as anything can signal it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-16 23:18:26 +02:00
commit a4753f4e9f

View file

@ -190,12 +190,16 @@ public:
++runtime_->active_calls;
entered_ = true;
}
// The notify stays under `gate`, and that is the whole point of it. Released
// first, the closer's predicate is already true, so it can return from its
// wait, run the teardown and destroy the runtime — condition variable
// included — before this thread reaches the notify. Holding the lock across
// both means the closer cannot leave `signal.wait` until this scope has let
// go, so the condition variable is still alive when it is signalled.
~CallScope() {
if (!entered_) return;
{
std::lock_guard<std::mutex> lock(runtime_->gate);
--runtime_->active_calls;
}
std::lock_guard<std::mutex> lock(runtime_->gate);
--runtime_->active_calls;
runtime_->signal.notify_all();
}
CallScope(const CallScope&) = delete;