spsc-ring
Lock-free SPSC ring buffer. Sequence-number protocol, cache-line padded, zero dependencies.
Background
This crate implements the single-producer/single-consumer (SPSC) variant of the LMAX Disruptor pattern — a lock-free ring buffer built around sequence numbers and Acquire/Release memory barriers, with no CAS operations on the hot path.
The key insight from the Disruptor paper: in the SPSC case, coordination requires no mutex and no compare-and-swap. Each slot carries a sequence stamp. The producer writes a value then releases the stamp; the consumer acquires the stamp then reads the value. The stamp is the only synchronisation point.
False sharing — the invisible performance killer — is eliminated by padding the producer and consumer cursors onto separate cache lines. Slots are aligned to prevent adjacent-slot invalidation under load.
The pattern is compelling enough that I had to explore it hands-on. spsc-ring is the result.
If you need more — multi-producer, consumer dependency graphs, pipeline fan-out, async, or static allocation — the Rust ecosystem has you covered: disruptor-rs, rtrb, ringbuf.
Features
- Lock-free SPSC — sequence-number protocol, Acquire/Release only
- Cache-line padded — producer/consumer cursors on separate cache lines (false-sharing eliminated by design)
- Zero dependencies — pure
std, no optional feature flags pulling in extra crates - Compile-time SPSC contract —
Producer<T>andConsumer<T>areSendbut notCloneand notSync— sharing a half across threads is a compile error - Minimal surface —
try_push,try_pop,push_slice,pop_into_slice,len— nothing else to audit
Performance
Measured on PoC-E, 1024-slot buffer.
| Benchmark | Mean (ms) | ± σ (ms) | Throughput |
|---|---|---|---|
| spsc_1M_events | 7.14 | ± 0.56 | ~140M ev/s |
| spsc_push_slice_1M_chunk64 | 5.92 | ± 0.20 | ~169M ev/s |
| spsc_push_slice_chunk_size/1 | 8.01 | ± 0.52 | ~125M ev/s |
| spsc_push_slice_chunk_size/8 | 3.69 | ± 0.25 | ~271M ev/s |
| spsc_push_slice_chunk_size/32 | 4.19 | ± 0.48 | ~239M ev/s |
| spsc_push_slice_chunk_size/64 | 4.18 | ± 0.27 | ~239M ev/s |
| spsc_push_slice_chunk_size/256 | 4.25 | ± 0.62 | ~235M ev/s |
Median of 3 runs, ±σ across runs. Results vary by hardware and thermal state.
Slice ops yield ~1.9× throughput over single-item path at chunk≥8.
Usage
[]
= "0.1"
use ;
use thread;
let = .unwrap;
spawn;
let mut received = Vecnew;
while received.len < 100
assert_eq!;
Bulk throughput with push_slice / pop_into_slice
For T: Copy, slice ops amortise the per-item overhead and yield ~1.9× throughput at chunk≥8.
use ring;
use thread;
let = .unwrap;
let producer = spawn;
let consumer = spawn;
producer.join.unwrap;
consumer.join.unwrap;
Disconnect detection
Both halves set a shared closed flag on drop. The other side observes it on the next call.
use ;
use thread;
let = .unwrap;
spawn;
loop
API
| Symbol | Description |
|---|---|
ring<T>(capacity) |
Create buffer (capacity must be power of 2). Returns Ok((Producer<T>, Consumer<T>)) or Err(InvalidCapacity). |
Producer::try_push(T) |
Push value. Returns Err(TrySendError::Full(T)) if full, Err(TrySendError::Disconnected(T)) if consumer dropped. |
Consumer::try_pop() |
Pop value. Returns Err(TryRecvError::Empty) if empty, Err(TryRecvError::Disconnected) if producer dropped. |
Producer::push(T, &WaitStrategy) |
Blocking push. Returns Err(SendError(T)) if consumer dropped. |
Consumer::pop(&WaitStrategy) |
Blocking pop. Returns Err(RecvError) if producer dropped and buffer empty. |
Producer::is_disconnected() |
Returns true if consumer has been dropped. |
Consumer::is_disconnected() |
Returns true if producer has been dropped. |
{Producer,Consumer}::len() |
Approximate item count (Relaxed snapshot — use for hints only, not correctness). |
{Producer,Consumer}::capacity() |
Buffer capacity. |
Producer::is_empty() |
Returns true if buffer appears empty. |
Producer::is_full() |
Returns true if buffer appears full. |
Consumer::is_empty() |
Returns true if buffer appears empty. |
Producer::push_slice(&[T]) |
Push items from slice until full or disconnected. Returns count pushed. Requires T: Copy. |
Consumer::pop_into_slice(&mut [T]) |
Pop items into slice until empty, full, or disconnected. Returns count popped. Requires T: Copy. |
Examples
| File | Demonstrates |
|---|---|
examples/basic.rs |
try_push / try_pop single-item loop |
examples/bulk.rs |
push_slice / pop_into_slice batch throughput |
examples/wait_strategy.rs |
blocking push / pop with WaitStrategy |
examples/disconnect.rs |
producer drops early; consumer drains then exits on disconnect |
MSRV
Rust 1.95 (stable). No nightly required.
License
MIT OR Apache-2.0