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
18use std::cell::UnsafeCell;
19use std::mem::MaybeUninit;
20use std::sync::Arc;
21use std::sync::atomic::{AtomicUsize, Ordering};
22
23/// 128 bytes is the right padding for Apple Silicon + recent x86 (some
24/// prefetchers operate on pairs of cache lines). Wastes ~96 bytes on 64-byte
25/// line machines; the cost is negligible against the false-sharing tax.
26#[repr(align(128))]
27pub(crate) struct Padded(pub(crate) AtomicUsize);
28
29pub(crate) struct Inner<T> {
30    /// Next slot the consumer will read. Written by the consumer only.
31    pub(crate) head: Padded,
32    /// Next slot the producer will write. Written by the producer only.
33    pub(crate) tail: Padded,
34    /// Power-of-two slot count.
35    pub(crate) capacity: usize,
36    /// `capacity - 1`; lets `idx & mask` replace `idx % capacity`.
37    pub(crate) mask: usize,
38    /// Slot storage. Each cell is touched by exactly one of producer/consumer
39    /// at any instant; `UnsafeCell` makes that explicit to the compiler.
40    pub(crate) buf: Box<[UnsafeCell<MaybeUninit<T>>]>,
41}
42
43// Producer/Consumer split is the type-level enforcement of SPSC. Inner itself
44// is `Sync` because the two sides touch disjoint state.
45unsafe impl<T: Send> Sync for Inner<T> {}
46unsafe impl<T: Send> Send for Inner<T> {}
47
48impl<T> Drop for Inner<T> {
49    fn drop(&mut self) {
50        // Drain remaining entries so their Drop impls fire.
51        let head = self.head.0.load(Ordering::Relaxed);
52        let tail = self.tail.0.load(Ordering::Relaxed);
53        for i in head..tail {
54            unsafe {
55                (*self.buf[i & self.mask].get()).assume_init_drop();
56            }
57        }
58    }
59}
60
61/// Producer handle. Move it to the producing thread; the type system enforces
62/// the SPSC invariant.
63pub struct Producer<T> {
64    pub(crate) inner: Arc<Inner<T>>,
65    /// Last-known consumer head; re-read on under-run only.
66    pub(crate) cached_head: usize,
67}
68
69unsafe impl<T: Send> Send for Producer<T> {}
70
71/// Consumer handle. Move it to the consuming thread.
72pub struct Consumer<T> {
73    pub(crate) inner: Arc<Inner<T>>,
74    /// Last-known producer tail; re-read on under-run only.
75    pub(crate) cached_tail: usize,
76}
77
78unsafe impl<T: Send> Send for Consumer<T> {}
79
80/// Constructor namespace. Use [`SpscRingBuffer::with_capacity`].
81pub struct SpscRingBuffer;
82
83impl SpscRingBuffer {
84    /// Build a (producer, consumer) pair backed by a buffer of at least
85    /// `requested_capacity` slots, rounded up to the next power of two.
86    /// A floor of 2 is enforced.
87    pub fn with_capacity<T>(requested_capacity: usize) -> (Producer<T>, Consumer<T>) {
88        let cap = requested_capacity.max(2).next_power_of_two();
89        let mut buf = Vec::with_capacity(cap);
90        for _ in 0..cap {
91            buf.push(UnsafeCell::new(MaybeUninit::<T>::uninit()));
92        }
93        let inner = Arc::new(Inner {
94            head: Padded(AtomicUsize::new(0)),
95            tail: Padded(AtomicUsize::new(0)),
96            capacity: cap,
97            mask: cap - 1,
98            buf: buf.into_boxed_slice(),
99        });
100        (
101            Producer {
102                inner: inner.clone(),
103                cached_head: 0,
104            },
105            Consumer {
106                inner,
107                cached_tail: 0,
108            },
109        )
110    }
111}
112
113impl<T> Producer<T> {
114    /// Push a value. Returns the input back as `Err(value)` if the buffer is
115    /// full. Wait-free: at most one atomic load + one atomic store.
116    pub fn try_push(&mut self, value: T) -> Result<(), T> {
117        let tail = self.inner.tail.0.load(Ordering::Relaxed);
118
119        // First check against the cached head - no atomic traffic if it's stale-but-not-full.
120        if tail.wrapping_sub(self.cached_head) == self.inner.capacity {
121            // Cache says full; re-read the real consumer head.
122            self.cached_head = self.inner.head.0.load(Ordering::Acquire);
123            if tail.wrapping_sub(self.cached_head) == self.inner.capacity {
124                return Err(value);
125            }
126        }
127
128        // Slot is ours; write the value, then publish the new tail.
129        unsafe {
130            (*self.inner.buf[tail & self.inner.mask].get()).write(value);
131        }
132        self.inner
133            .tail
134            .0
135            .store(tail.wrapping_add(1), Ordering::Release);
136        Ok(())
137    }
138
139    /// Total slot count (power of two; not the requested capacity).
140    pub fn capacity(&self) -> usize {
141        self.inner.capacity
142    }
143}
144
145impl<T> Consumer<T> {
146    /// Pop a value. Returns `None` if the buffer is empty. Wait-free.
147    pub fn try_pop(&mut self) -> Option<T> {
148        let head = self.inner.head.0.load(Ordering::Relaxed);
149
150        if head == self.cached_tail {
151            self.cached_tail = self.inner.tail.0.load(Ordering::Acquire);
152            if head == self.cached_tail {
153                return None;
154            }
155        }
156
157        let value = unsafe { (*self.inner.buf[head & self.inner.mask].get()).assume_init_read() };
158        self.inner
159            .head
160            .0
161            .store(head.wrapping_add(1), Ordering::Release);
162        Some(value)
163    }
164
165    /// Total slot count (power of two).
166    pub fn capacity(&self) -> usize {
167        self.inner.capacity
168    }
169}
170
171#[cfg(feature = "harness")]
172pub mod recipe;
173
174// Opt-in feature catalog. Each submodule is gated on its own Cargo
175// feature; the base ring stays wait-free SPSC + zero-dep std-only.
176#[cfg(any(
177    feature = "bulk",
178    feature = "wait-strategies",
179    feature = "mpsc-fan-in",
180    feature = "mpmc-disruptor",
181    feature = "metrics",
182))]
183pub mod features;
184
185#[cfg(feature = "metrics")]
186pub use features::metrics::{InstrumentedSpsc, RingMetrics, RingMetricsSnapshot};
187#[cfg(feature = "mpmc-disruptor")]
188pub use features::mpmc_disruptor::{DisruptorConsumer, DisruptorProducer, MpmcDisruptor};
189#[cfg(feature = "mpsc-fan-in")]
190pub use features::mpsc_fan_in::{MpscFanIn, MpscFanInConsumer, MpscFanInProducer};
191#[cfg(feature = "wait-strategies")]
192pub use features::wait_strategies::{
193    BlockingSpscConsumer, BlockingSpscProducer, BusySpin, ParkStrategy, WaitStrategy, YieldStrategy,
194};