Lock-free ring buffers that overwrite the oldest data when full — designed for real-time audio and similar domains where freshness matters more than completeness.
Two flavours, separate modules, separate types:
| Module | Pattern | Topology |
|---|---|---|
rt_ring (root) |
SPSC | one producer ↔ one consumer, producer-coordinated eviction with exact accounting |
rt_ring::broadcast |
SPMC fan-out | one producer → many consumers via Consumer::clone(), lapping (Disruptor-style) |
[dependencies]
rt-ring = "0.2"MSRV is Rust 1.87 (enforced in CI across all targets, including tests and benches). No runtime dependencies, no features to enable.
- Overwrite-oldest semantics: new writes discard the oldest sample instead of failing. SPSC discards only when the buffer is actually full; broadcast's effective window is
capacity − 1, so the push that merely fills the buffer already costs the oldest sample (see broadcast semantics). - Lock-free, no allocations or IO in the hot path. Broadcast
pushandpopare additionally wait-free —popwith a bounded retry budget (see the lap-boundary note). SPSCpush/popCAS-retry only when the other side has just made progress. - Wrap-safe positions: the monotonic position counters use wrapping arithmetic, so both rings keep working after
usizewraps (relevant on 32-bit targets — ~25 hours of continuous 48 kHz audio). - Safe Rust API: no
unsafecode - Zero runtime dependencies
use rt_ring::{new, Producer, Consumer};
let (producer, consumer) = rt_ring::new(1024);
// Producer side (e.g., audio input callback)
producer.push(0.5);
producer.push_slice(&[0.1, 0.2, 0.3]);
// Consumer side (e.g., processing thread)
while let Some(sample) = consumer.pop() {
// process sample
}
println!("overwrites: {}", consumer.overwrite_count());- FIFO when not full: if the consumer keeps up, samples arrive in exact push order with no gaps
- Overwrite-oldest when full: the producer never blocks; it discards the oldest unread sample to make room
- Monotonic with gaps: popped values are always in push order, but consecutive values may skip when overwrites occur
- Accounting invariant:
overwrite_count() + popped == pushed— exact, every interleaving - SPSC only: exactly one producer thread and one consumer thread (
ProducerandConsumerare!Cloneto enforce this at the type level) - Capacity is a power of two: rounded up at construction
use rt_ring::broadcast;
let (producer, consumer) = broadcast::new(1024);
// Add a second consumer at any time — it joins from "now",
// no replay of older samples.
let consumer2 = consumer.clone();
producer.push(0.5);
assert_eq!(consumer.pop(), Some(0.5));
assert_eq!(consumer2.pop(), Some(0.5)); // both see every sample- Every consumer sees every sample it does not fall behind on. The effective readable window is
capacity − 1; pushing exactlycapacitysamples between pops already costs the oldest one (strict-lag < capacitysafety condition, see the lap-boundary note below). - Producer is oblivious — it never reads consumer state. Push is 3 atomic ops, no CAS, no branch, regardless of how many consumers exist.
- Per-consumer accounting: each consumer's invariant is
overwrites + popped == samples_pushed_since_clone. Updated lazily by the consumer atpoptime, not by the producer. Cloneis the registration mechanism. Cloning just bumps theArcrefcount and initialises a freshCell<usize>cursor +Cell<u64>overwrite counter on the newConsumer— no heap allocation. The producer never enumerates consumers, so drop is also free.Producer: Send + !SyncandConsumer: Send + !Sync— move them across threads, do not share either by&. The types rejectArc<Producer>/Arc<Consumer>concurrent use at compile time.- Capacity is a power of two with a floor of 2. A requested capacity of
1is silently raised to2. The lap-detection protocol requires the consumer's lag to be strictly less than capacity; withcap == 1, every non-empty state would be flagged as a lap. If you want keep-latest-1 semantics, use the SPSC API.
Producer push is wait-free (3 atomic ops, no CAS). Consumer pop is wait-free with a bounded retry budget: when lapped, pop skips ahead, and each consecutive lap within one call doubles how far ahead of the producer the cursor lands, so a producer fast enough to keep re-lapping the consumer is outrun within log2(capacity) retries. If the producer wins even those races, pop returns None — with every skipped sample already counted in overwrite_count — instead of spinning. None therefore means "nothing readable right now": empty ring, or a producer overwriting faster than this consumer can safely read (distinguish via overwrite_count).
At the exact lap boundary (lag == capacity), the consumer's pop retries to avoid a producer/consumer race on the slot the producer is about to overwrite. Under tight pacing at small capacities (≤ 4), this can over-count overwrites by 1 occasionally — the accounting invariant still holds, the consumer just loses one extra sample at the boundary. For typical audio buffer sizes (≥ 256) this is unobservable.
- SPSC for the classic audio-callback ↔ DSP-thread case: tighter accounting, no lap-boundary trade-off, slightly cheaper consumer pop.
- Broadcast when you need fan-out to multiple readers (DSP + visualizer + recorder) with dynamic lifetimes. Cheaper producer push (no CAS), at the cost of slightly more bookkeeping in
pop.
- Overwrite counter tracks dropped samples (global in SPSC, per-consumer in broadcast)
- Bulk
push_slice/pop_sliceoperations available()to query how much is readable (SPSC: buffer occupancy, up tocapacity; broadcast: per-consumer poppable count, capped atcapacity − 1)- Capacity automatically rounds up to the next power of two
make test # full suite: unit, integration, proptest, doctests
make test-loom # loom model-checking of the memory orderings (RUSTFLAGS="--cfg loom")
make fuzz # libFuzzer targets for both rings (nightly)
make ci # clippy -D warnings + fmt check + tests
cargo +nightly miri test # UB checkThe loom job swaps std::sync::atomic for loom::sync::atomic inside the library via --cfg loom, so the orderings themselves are model-checked — not just thread schedules.
Licensed under either of Apache License, Version 2.0 or MIT license at your option.