Skip to main content

subms_spsc_ring_buffer/
lib.rs

1//! Wait-free SPSC ring buffer. Single producer, single consumer, bounded.
2//!
3//! Sized to a power of two; index modulo is a bitmask, not a `%`. Head and tail
4//! counters live on their own cache lines (128-byte padding) to keep the
5//! producer's writes from invalidating the consumer's read line. Each side
6//! caches the opposite index and only re-reads through the atomic when its own
7//! cache says full / empty.
8//!
9//! ```
10//! use subms_spsc_ring_buffer::SpscRingBuffer;
11//!
12//! let (mut tx, mut rx) = SpscRingBuffer::with_capacity(64);
13//! tx.try_push(42u32).unwrap();
14//! assert_eq!(rx.try_pop(), Some(42));
15//! assert_eq!(rx.try_pop(), None);
16//! ```
17//!
18//! Full writeup, design notes and measured benchmarks:
19//! <https://www.submillisecond.com/cookbook/recipes/subms-spsc-ring-buffer>
20
21use std::cell::UnsafeCell;
22use std::mem::MaybeUninit;
23use std::sync::Arc;
24use std::sync::atomic::{AtomicUsize, Ordering};
25
26/// 128 bytes is the right padding for Apple Silicon + recent x86 (some
27/// prefetchers operate on pairs of cache lines). Wastes ~96 bytes on 64-byte
28/// line machines; the cost is negligible against the false-sharing tax.
29#[repr(align(128))]
30pub(crate) struct Padded(pub(crate) AtomicUsize);
31
32pub(crate) struct Inner<T> {
33    /// Next slot the consumer will read. Written by the consumer only.
34    pub(crate) head: Padded,
35    /// Next slot the producer will write. Written by the producer only.
36    pub(crate) tail: Padded,
37    /// Power-of-two slot count.
38    pub(crate) capacity: usize,
39    /// `capacity - 1`; lets `idx & mask` replace `idx % capacity`.
40    pub(crate) mask: usize,
41    /// Slot storage. Each cell is touched by exactly one of producer/consumer
42    /// at any instant; `UnsafeCell` makes that explicit to the compiler.
43    pub(crate) buf: Box<[UnsafeCell<MaybeUninit<T>>]>,
44}
45
46// Producer/Consumer split is the type-level enforcement of SPSC. Inner itself
47// is `Sync` because the two sides touch disjoint state.
48unsafe impl<T: Send> Sync for Inner<T> {}
49unsafe impl<T: Send> Send for Inner<T> {}
50
51impl<T> Drop for Inner<T> {
52    fn drop(&mut self) {
53        // Drain remaining entries so their Drop impls fire.
54        let head = self.head.0.load(Ordering::Relaxed);
55        let tail = self.tail.0.load(Ordering::Relaxed);
56        for i in head..tail {
57            unsafe {
58                (*self.buf[i & self.mask].get()).assume_init_drop();
59            }
60        }
61    }
62}
63
64/// Producer handle. Move it to the producing thread; the type system enforces
65/// the SPSC invariant.
66pub struct Producer<T> {
67    pub(crate) inner: Arc<Inner<T>>,
68    /// Last-known consumer head; re-read on under-run only.
69    pub(crate) cached_head: usize,
70}
71
72unsafe impl<T: Send> Send for Producer<T> {}
73
74/// Consumer handle. Move it to the consuming thread.
75pub struct Consumer<T> {
76    pub(crate) inner: Arc<Inner<T>>,
77    /// Last-known producer tail; re-read on under-run only.
78    pub(crate) cached_tail: usize,
79}
80
81unsafe impl<T: Send> Send for Consumer<T> {}
82
83/// Constructor namespace. Use [`SpscRingBuffer::with_capacity`].
84pub struct SpscRingBuffer;
85
86impl SpscRingBuffer {
87    /// Build a (producer, consumer) pair backed by a buffer of at least
88    /// `requested_capacity` slots, rounded up to the next power of two.
89    /// A floor of 2 is enforced.
90    pub fn with_capacity<T>(requested_capacity: usize) -> (Producer<T>, Consumer<T>) {
91        let cap = requested_capacity.max(2).next_power_of_two();
92        let mut buf = Vec::with_capacity(cap);
93        for _ in 0..cap {
94            buf.push(UnsafeCell::new(MaybeUninit::<T>::uninit()));
95        }
96        let inner = Arc::new(Inner {
97            head: Padded(AtomicUsize::new(0)),
98            tail: Padded(AtomicUsize::new(0)),
99            capacity: cap,
100            mask: cap - 1,
101            buf: buf.into_boxed_slice(),
102        });
103        (
104            Producer {
105                inner: inner.clone(),
106                cached_head: 0,
107            },
108            Consumer {
109                inner,
110                cached_tail: 0,
111            },
112        )
113    }
114}
115
116impl<T> Producer<T> {
117    /// Push a value. Returns the input back as `Err(value)` if the buffer is
118    /// full. Wait-free: at most one atomic load + one atomic store.
119    pub fn try_push(&mut self, value: T) -> Result<(), T> {
120        let tail = self.inner.tail.0.load(Ordering::Relaxed);
121
122        // First check against the cached head - no atomic traffic if it's stale-but-not-full.
123        if tail.wrapping_sub(self.cached_head) == self.inner.capacity {
124            // Cache says full; re-read the real consumer head.
125            self.cached_head = self.inner.head.0.load(Ordering::Acquire);
126            if tail.wrapping_sub(self.cached_head) == self.inner.capacity {
127                return Err(value);
128            }
129        }
130
131        // Slot is ours; write the value, then publish the new tail.
132        unsafe {
133            (*self.inner.buf[tail & self.inner.mask].get()).write(value);
134        }
135        self.inner
136            .tail
137            .0
138            .store(tail.wrapping_add(1), Ordering::Release);
139        Ok(())
140    }
141
142    /// Total slot count (power of two; not the requested capacity).
143    pub fn capacity(&self) -> usize {
144        self.inner.capacity
145    }
146
147    /// Items currently buffered. A snapshot: the consumer runs concurrently, so
148    /// the true count can only be lower by the time the caller acts on it. Use
149    /// it for occupancy alarms and sizing, never to decide whether a push will
150    /// succeed - `try_push` already answers that without a race.
151    pub fn len(&self) -> usize {
152        occupancy(&self.inner)
153    }
154
155    /// True when no items are buffered. Snapshot semantics, as [`Producer::len`].
156    pub fn is_empty(&self) -> bool {
157        self.len() == 0
158    }
159
160    /// True when the ring holds `capacity` items. Snapshot semantics.
161    pub fn is_full(&self) -> bool {
162        self.len() == self.inner.capacity
163    }
164}
165
166/// Buffered count from the two published counters. Both loads are `Acquire`
167/// because either side may ask, so neither counter is reliably the caller's own.
168fn occupancy<T>(inner: &Inner<T>) -> usize {
169    let tail = inner.tail.0.load(Ordering::Acquire);
170    let head = inner.head.0.load(Ordering::Acquire);
171    tail.wrapping_sub(head)
172}
173
174impl<T> Consumer<T> {
175    /// Pop a value. Returns `None` if the buffer is empty. Wait-free.
176    pub fn try_pop(&mut self) -> Option<T> {
177        let head = self.inner.head.0.load(Ordering::Relaxed);
178
179        if head == self.cached_tail {
180            self.cached_tail = self.inner.tail.0.load(Ordering::Acquire);
181            if head == self.cached_tail {
182                return None;
183            }
184        }
185
186        let value = unsafe { (*self.inner.buf[head & self.inner.mask].get()).assume_init_read() };
187        self.inner
188            .head
189            .0
190            .store(head.wrapping_add(1), Ordering::Release);
191        Some(value)
192    }
193
194    /// Borrow the next value without consuming it. `None` when empty.
195    ///
196    /// Safe because only the consumer advances `head`, so the slot the returned
197    /// reference points at cannot be reclaimed while the borrow is live - the
198    /// `&mut self` receiver keeps `try_pop` out of reach for the same lifetime.
199    pub fn peek(&mut self) -> Option<&T> {
200        let head = self.inner.head.0.load(Ordering::Relaxed);
201        if head == self.cached_tail {
202            self.cached_tail = self.inner.tail.0.load(Ordering::Acquire);
203            if head == self.cached_tail {
204                return None;
205            }
206        }
207        unsafe { Some((*self.inner.buf[head & self.inner.mask].get()).assume_init_ref()) }
208    }
209
210    /// Drop every buffered item and return how many went. Consumer-side only:
211    /// the producer keeps publishing throughout, so anything it appends after
212    /// the counters were read stays in the ring.
213    pub fn clear(&mut self) -> usize {
214        let mut n = 0;
215        while self.try_pop().is_some() {
216            n += 1;
217        }
218        n
219    }
220
221    /// Total slot count (power of two).
222    pub fn capacity(&self) -> usize {
223        self.inner.capacity
224    }
225
226    /// Items currently buffered. Snapshot semantics, as [`Producer::len`] - the
227    /// producer runs concurrently, so the true count can only be higher.
228    pub fn len(&self) -> usize {
229        occupancy(&self.inner)
230    }
231
232    /// True when no items are buffered. Snapshot semantics.
233    pub fn is_empty(&self) -> bool {
234        self.len() == 0
235    }
236
237    /// True when the ring holds `capacity` items. Snapshot semantics.
238    pub fn is_full(&self) -> bool {
239        self.len() == self.inner.capacity
240    }
241}
242
243#[cfg(feature = "harness")]
244pub mod recipe;
245
246// Opt-in feature catalog. Each submodule is gated on its own Cargo
247// feature; the base ring stays wait-free SPSC + zero-dep std-only.
248#[cfg(any(
249    feature = "bulk",
250    feature = "wait-strategies",
251    feature = "mpsc-fan-in",
252    feature = "mpmc-disruptor",
253    feature = "metrics",
254))]
255pub mod features;
256
257#[cfg(feature = "metrics")]
258pub use features::metrics::{InstrumentedSpsc, RingMetrics, RingMetricsSnapshot};
259#[cfg(feature = "mpmc-disruptor")]
260pub use features::mpmc_disruptor::{DisruptorConsumer, DisruptorProducer, MpmcDisruptor};
261#[cfg(feature = "mpsc-fan-in")]
262pub use features::mpsc_fan_in::{MpscFanIn, MpscFanInConsumer, MpscFanInProducer};
263#[cfg(feature = "wait-strategies")]
264pub use features::wait_strategies::{
265    BlockingSpscConsumer, BlockingSpscProducer, BusySpin, ParkStrategy, WaitStrategy, YieldStrategy,
266};
267
268#[cfg(test)]
269#[path = "lib_tests.rs"]
270mod tests;
271
272#[cfg(test)]
273#[path = "sample_app_tests.rs"]
274mod sample_app_tests;