◐ Off-By-One · answer catalog

go-mpsc-channel-lockfree-drain

2 answer(s)gogo1.26gogo1.26

go-mpsc-channel-lockfree-drain

📦 Source in repository (JSON)

Answer 1

Solution complete and verified. Summary:

Diagnosis — naive MPSC/drain implementations fail four ways: 1. Lost items: a producer's push is two steps (tail.Swap(n) then prev.next.Store(n)); a consumer seeing next == nil while tail != head concludes "empty" and drops the in-flight item. 2. ABA on tail: the classic consumer "tail fast-forward" CAS compares a stale, possibly-recycled address — catastrophic without hazard pointers/epochs, which safe Go can't do. 3. Torn reads: publishing the value after linking (or non-atomic next fields) breaks release/acquire ordering → -race failures. 4. Duplicate pops: unowned/shared head pointers.

Fix (~/mpsc-lockfree/mpsc/queue.go, also in SOLUTION.md): - Producers only tail.Swap (xchg compares nothing) → no CAS on any shared pointer → no ABA window at all; the only comparison (head == tail) is safe because GC keeps head's node alive, so equality means genuine emptiness. - Pop spins with Gosched backoff on the in-progress-push window instead of reporting empty → nothing ever lost; Drain still never waits for new arrivals (returns immediately on empty). - Value written before the release link store, read after the acquire link load (sync/atomic is sequentially consistent) → no torn/raced reads. - Consumer exclusively owns monotonic head → no duplicates.

Verification (go1.26.0, linux/amd64):

go test -race -count=20 -shuffle=on ./...        → ok (2.6s)
GOMAXPROCS=1 go test -race -count=5 ./...        → ok (4.2s)   # preemption in swap/link window
GOGC=1 go test -race -count=3 ./...              → ok          # aggressive address recycling

Plus go vet/gofmt clean, and stress tests asserting no loss/duplication with a watchdog proving Drain doesn't block on empty.

Evidence & signatures

# Evidence
- Problem class: go-mpsc-channel-lockfree-drain
- Model: openrouter/deepseek/deepseek-v4-flash-0731
- Solved: 2026-08-31T22:07:04.609Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Implement a lock-free MPSC (multi-producer single-consumer) queue in Go using only sync/atomic, with a drain() method that returns all currently-enqueued items without blocking and without losing items enqueued concurrently during the drain. Producers must be safe under the Go memory model (release/acquire ordering), the consumer must never observe torn or duplicated nodes, and the implementation must pass go test -race under a producer/consumer stress test. Handle the ABA problem on the tail pointer without relying on GC finalizers or mutexes.", "environment": "go1.26", "language": "go", "model": "openrouter/deepseek/deepseek-v4-flash-0731", "problem_class": "go-mpsc-channel-lockfree-drain", "provider": "openrouter", "solved_at": "2026-08-31T22:07:04.609Z", "version": "1.26"}

Answer 2

Solution complete and verified. Summary:

Diagnosis — naive MPSC/drain implementations fail four ways: 1. Lost items: a producer's push is two steps (tail.Swap(n) then prev.next.Store(n)); a consumer seeing next == nil while tail != head concludes "empty" and drops the in-flight item. 2. ABA on tail: the classic consumer "tail fast-forward" CAS compares a stale, possibly-recycled address — catastrophic without hazard pointers/epochs, which safe Go can't do. 3. Torn reads: publishing the value after linking (or non-atomic next fields) breaks release/acquire ordering → -race failures. 4. Duplicate pops: unowned/shared head pointers.

Fix (~/mpsc-lockfree/mpsc/queue.go, also in SOLUTION.md): - Producers only tail.Swap (xchg compares nothing) → no CAS on any shared pointer → no ABA window at all; the only comparison (head == tail) is safe because GC keeps head's node alive, so equality means genuine emptiness. - Pop spins with Gosched backoff on the in-progress-push window instead of reporting empty → nothing ever lost; Drain still never waits for new arrivals (returns immediately on empty). - Value written before the release link store, read after the acquire link load (sync/atomic is sequentially consistent) → no torn/raced reads. - Consumer exclusively owns monotonic head → no duplicates.

Verification (go1.26.0, linux/amd64):

go test -race -count=20 -shuffle=on ./...        → ok (2.6s)
GOMAXPROCS=1 go test -race -count=5 ./...        → ok (4.2s)   # preemption in swap/link window
GOGC=1 go test -race -count=3 ./...              → ok          # aggressive address recycling

Plus go vet/gofmt clean, and stress tests asserting no loss/duplication with a watchdog proving Drain doesn't block on empty.

Evidence & signatures

# Evidence
- Problem class: go-mpsc-channel-lockfree-drain
- Model: openrouter/deepseek/deepseek-v4-flash-0731
- Solved: 2026-08-31T22:07:04.609Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Implement a lock-free MPSC (multi-producer single-consumer) queue in Go using only sync/atomic, with a drain() method that returns all currently-enqueued items without blocking and without losing items enqueued concurrently during the drain. Producers must be safe under the Go memory model (release/acquire ordering), the consumer must never observe torn or duplicated nodes, and the implementation must pass go test -race under a producer/consumer stress test. Handle the ABA problem on the tail pointer without relying on GC finalizers or mutexes.", "environment": "go1.26", "language": "go", "model": "openrouter/deepseek/deepseek-v4-flash-0731", "problem_class": "go-mpsc-channel-lockfree-drain", "provider": "openrouter", "solved_at": "2026-08-31T22:07:04.609Z", "version": "1.26"}
Generated from the verified corpus · MIT licensedBack to the catalog