SPSCQueue¶
SPSCQueue is a lock-free, bounded single-producer/single-consumer queue. Supports in-place construction and cache-line isolation.
Purpose¶
- Provide low-latency, zero-contention messaging between one writer and one reader.
Requirements¶
Enforced by static_assert:
Capacity > 1.Capacitymust be a power of two.Tmust be nothrow-destructible.Tmust be nothrow-move-constructible.
Not enforced by the compiler:
- Only one producer and one consumer may operate concurrently.
Key Features¶
| Method | Description |
|---|---|
push(const T&) |
Enqueues a copy of an object. |
emplace(T&&) |
Enqueues an rvalue object (move). |
try_emplace(...) |
Constructs object in-place with arguments. |
pop(T&) |
Pops and moves the front element into out. |
try_pop() |
Returns a borrowed pointer to the front element, or nullptr if empty. See Borrowed pointers. |
try_pop_ref() |
Returns std::optional<std::reference_wrapper<T>> for inline access. Same borrowing rules. |
empty() / full() |
Check queue state. |
clear() |
Destroys and drains all pending elements. |
size() |
Returns current number of elements. |
Borrowed pointers¶
try_pop() and try_pop_ref() hand back a pointer into the ring and republish
the slot to the producer immediately. The pointer is borrowed, not owned. It is
valid until the consumer's next call that moves the tail (pop, try_pop,
try_pop_ref, clear), and the caller destroys the element through it before
that point.
With exactly one pointer outstanding the ring invariant protects it. One slot
is always left empty, so a producer filling the queue stops one short of the
slot just handed out. Hold two at once and that protection is gone: the
producer can construct a new element into the first slot, the stale pointer
then reads the new element instead of the old one, and destroying it through
that pointer destroys an element still queued for delivery. The next pop
reads the wreckage.
To consume several elements at a time, use the segment API instead. It hands
out a contiguous run and holds the slots until commit_read:
T* items = nullptr;
const size_t n = q.read_segment(items);
for (size_t i = 0; i < n; ++i) { process(items[i]); items[i].~T(); }
q.commit_read(n);
Internal Design¶
- Ring buffer implementation with
Capacityentries, using moduloMASK = Capacity - 1. _headand_tailarestd::atomic<size_t>and are false-shared-safe viaalignas(64).- Uses placement
newfor in-place construction, avoids heap entirely.
Notes¶
- Optimized for predictable, sub-microsecond latency in tight loops.
- No memory reclamation or ABA protection — not suitable for multi-producer/multi-consumer setups.
- All methods use
memory_order_acquire/releaseto ensure visibility across cores. - Destruction ensures safe draining of remaining elements via
~T()call.