fix: deliver EOF sentinel only when the ring is freshly empty
Channel<T>::pop() surfaced the out-of-band sentinel from its empty branch using the tail_ snapshot taken at the top of the loop. Under contention the producer can push more values *and* the sentinel in the window between that snapshot and take_sentinel(), so pop() could return the sentinel while real values still sat in the ring — the sentinel jumping ahead of values pushed before it. No value was lost (a consumer that keeps draining still receives them, and approx_size() keeps counting them so a PoolNode reschedules), but a consumer treating the sentinel as a hard "last message" barrier would act on EOF early. Re-confirm emptiness against a fresh tail_ load before taking the sentinel. Costs one acquire-load on the empty-ring path only; never runs in steady state. The spin and post-spin takes already reload tail_ on the line above them; try_pop_now() already reads tail_ fresh in the same branch — both were correct and are unchanged. The two sentinel stress cases now assert the strict "sentinel is last, after every value" ordering (previously relaxed to avoid the flake this fixes). Verified TSan-clean (2606 assertions, no data races) over repeated runs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+10
-1
@@ -180,7 +180,16 @@ public:
|
||||
if (h == t) {
|
||||
// Ring drained — deliver any pending out-of-band sentinel (EOF)
|
||||
// now, so it always arrives after the data pushed before it.
|
||||
{ T s; if (take_sentinel(s)) return s; }
|
||||
//
|
||||
// Re-confirm emptiness against a fresh tail_ first: the snapshot
|
||||
// at the top of the loop may be stale (the producer can push more
|
||||
// values *and* the sentinel in the window since), and the sentinel
|
||||
// must never jump ahead of ring values pushed before it. The spin
|
||||
// and post-spin takes below already reload tail_ on the line above
|
||||
// them; this is the one take that used the loop-top snapshot.
|
||||
if (h == tail_.load(std::memory_order_acquire)) {
|
||||
T s; if (take_sentinel(s)) return s;
|
||||
}
|
||||
|
||||
if (!accepting_.load(std::memory_order_acquire))
|
||||
throw ChannelClosedError{};
|
||||
|
||||
Reference in New Issue
Block a user