Skip to main content

subms_mpsc_queue/
lib.rs

1//! Vyukov-style multi-producer single-consumer linked queue.
2//!
3//! Producers enqueue with one `swap` on the tail; the consumer drains by
4//! following `next` pointers from the head. The dangling-tail window is the
5//! load-bearing detail: between the producer's CAS-of-tail and the
6//! prev.next = new link, the consumer can see `next == null` while there is
7//! actually a publisher in flight. [`MpscQueue::try_pop`] returns
8//! [`PopResult::Inconsistent`] in that window so the caller can spin or back
9//! off rather than treating it as empty.
10//!
11//! ```
12//! use subms_mpsc_queue::{MpscQueue, PopResult};
13//! let mut q: MpscQueue<u32> = MpscQueue::new();
14//! q.push(7);
15//! q.push(8);
16//! assert!(matches!(q.try_pop(), PopResult::Some(7)));
17//! assert!(matches!(q.try_pop(), PopResult::Some(8)));
18//! ```
19//!
20//! Full writeup, design notes and measured benchmarks:
21//! <https://www.submillisecond.com/cookbook/recipes/subms-mpsc-queue>
22
23use std::cell::UnsafeCell;
24use std::ptr;
25use std::sync::atomic::{AtomicPtr, Ordering};
26
27struct Node<T> {
28    value: UnsafeCell<Option<T>>,
29    next: AtomicPtr<Node<T>>,
30}
31
32/// One-shot result of [`MpscQueue::try_pop`].
33pub enum PopResult<T> {
34    /// A value was dequeued.
35    Some(T),
36    /// Queue is truly empty.
37    Empty,
38    /// A producer is mid-publish; head was reached but `next` is not yet
39    /// linked. Callers should retry (spin or back off).
40    Inconsistent,
41}
42
43/// Multi-producer single-consumer linked queue.
44///
45/// Cloneable handles are not provided; share via `Arc<MpscQueue<T>>`.
46/// Consumer methods (`try_pop`) require `&mut self` to encode single-consumer
47/// invariant at the type level.
48pub struct MpscQueue<T> {
49    head: AtomicPtr<Node<T>>,
50    tail: UnsafeCell<*mut Node<T>>,
51    stub: Box<Node<T>>,
52}
53
54unsafe impl<T: Send> Sync for MpscQueue<T> {}
55unsafe impl<T: Send> Send for MpscQueue<T> {}
56
57impl<T> MpscQueue<T> {
58    pub fn new() -> Self {
59        let stub = Box::new(Node {
60            value: UnsafeCell::new(None),
61            next: AtomicPtr::new(ptr::null_mut()),
62        });
63        let stub_ptr = stub.as_ref() as *const Node<T> as *mut Node<T>;
64        Self {
65            head: AtomicPtr::new(stub_ptr),
66            tail: UnsafeCell::new(stub_ptr),
67            stub,
68        }
69    }
70
71    /// Multi-producer push. Wait-free for the producer once the node is
72    /// allocated.
73    pub fn push(&self, value: T) {
74        let node = Box::into_raw(Box::new(Node {
75            value: UnsafeCell::new(Some(value)),
76            next: AtomicPtr::new(ptr::null_mut()),
77        }));
78        // swap-head exchanges the publication point atomically. Producers
79        // can race here; each gets a distinct prev.
80        let prev = self.head.swap(node, Ordering::AcqRel);
81        // The dangling-tail window opens here: prev exists, but prev.next is
82        // still null until the next line. Consumer must tolerate it.
83        unsafe { (*prev).next.store(node, Ordering::Release) };
84    }
85
86    /// Publish a whole run of values with a single head swap.
87    ///
88    /// The producer links the nodes together privately before publication, so
89    /// the chain costs one `swap` and one release-store no matter how many
90    /// items it carries. Returns the number published; an empty iterator
91    /// touches no atomics at all.
92    ///
93    /// Items keep their iteration order relative to each other, and the run is
94    /// published atomically: a consumer either sees none of it or sees the
95    /// whole chain reachable from the node it links onto.
96    #[cfg(feature = "batch")]
97    pub fn push_batch<I: IntoIterator<Item = T>>(&self, values: I) -> usize {
98        let mut first: *mut Node<T> = ptr::null_mut();
99        let mut last: *mut Node<T> = ptr::null_mut();
100        let mut n = 0usize;
101        for value in values {
102            let node = Box::into_raw(Box::new(Node {
103                value: UnsafeCell::new(Some(value)),
104                next: AtomicPtr::new(ptr::null_mut()),
105            }));
106            if first.is_null() {
107                first = node;
108            } else {
109                // Relaxed is enough: the chain is thread-private until the
110                // release-store below publishes it.
111                unsafe { (*last).next.store(node, Ordering::Relaxed) };
112            }
113            last = node;
114            n += 1;
115        }
116        if n == 0 {
117            return 0;
118        }
119        let prev = self.head.swap(last, Ordering::AcqRel);
120        unsafe { (*prev).next.store(first, Ordering::Release) };
121        n
122    }
123
124    /// Consume one entry. Returns [`PopResult::Inconsistent`] if a producer
125    /// is mid-publish; callers should retry.
126    ///
127    /// Single consumer only: requires `&mut self`.
128    pub fn try_pop(&mut self) -> PopResult<T> {
129        // Safety: `tail` is consumer-private (only one consumer at a time).
130        let tail = unsafe { *self.tail.get() };
131        let next = unsafe { (*tail).next.load(Ordering::Acquire) };
132
133        // The stub trick: tail starts at the stub. Once we've drained past
134        // it, swap stub to the new tail so the head's reference is preserved.
135        let stub_ptr = self.stub.as_ref() as *const Node<T> as *mut Node<T>;
136        if tail == stub_ptr {
137            if next.is_null() {
138                // Stub is still the only node: either empty or producer
139                // mid-publish.
140                if self.head.load(Ordering::Acquire) == stub_ptr {
141                    return PopResult::Empty;
142                }
143                return PopResult::Inconsistent;
144            }
145            // Move past the stub.
146            unsafe { *self.tail.get() = next };
147            let value = unsafe { (*next).value.get().replace(None) };
148            return match value {
149                Some(v) => PopResult::Some(v),
150                None => PopResult::Inconsistent,
151            };
152        }
153
154        if !next.is_null() {
155            unsafe { *self.tail.get() = next };
156            // Drop the consumed node now that tail has advanced past it.
157            let consumed = unsafe { Box::from_raw(tail) };
158            drop(consumed);
159            let value = unsafe { (*next).value.get().replace(None) };
160            return match value {
161                Some(v) => PopResult::Some(v),
162                None => PopResult::Inconsistent,
163            };
164        }
165
166        // tail.next is null but tail is not the stub: either truly drained
167        // or a producer is racing the link write.
168        if self.head.load(Ordering::Acquire) == tail {
169            PopResult::Empty
170        } else {
171            PopResult::Inconsistent
172        }
173    }
174
175    /// Borrow the next value without consuming it.
176    ///
177    /// Returns `None` both when the queue is drained and when a producer is
178    /// mid-publish, matching JCTools' `relaxedPeek`: the caller that needs to
179    /// tell the two apart calls [`Self::is_empty`].
180    ///
181    /// Consumer-side only.
182    pub fn peek(&mut self) -> Option<&T> {
183        let tail = unsafe { *self.tail.get() };
184        let next = unsafe { (*tail).next.load(Ordering::Acquire) };
185        if next.is_null() {
186            return None;
187        }
188        unsafe { (*(*next).value.get()).as_ref() }
189    }
190
191    /// True only when the queue is genuinely drained.
192    ///
193    /// A producer inside the dangling-tail window reads as non-empty, because
194    /// its item is already committed to the chain even though the link is not
195    /// written yet.
196    ///
197    /// Consumer-side only.
198    pub fn is_empty(&mut self) -> bool {
199        let tail = unsafe { *self.tail.get() };
200        let next = unsafe { (*tail).next.load(Ordering::Acquire) };
201        next.is_null() && self.head.load(Ordering::Acquire) == tail
202    }
203
204    /// Count the linked items. O(n) in the backlog, and an estimate under a
205    /// live producer, which is the same contract JCTools' `size()` carries on
206    /// its linked queues. Use it for alarms and sizing, not on the hot path.
207    ///
208    /// Consumer-side only.
209    pub fn len(&mut self) -> usize {
210        let mut n = 0;
211        let mut node = unsafe { *self.tail.get() };
212        loop {
213            let next = unsafe { (*node).next.load(Ordering::Acquire) };
214            if next.is_null() {
215                return n;
216            }
217            n += 1;
218            node = next;
219        }
220    }
221
222    /// Drop everything the consumer can currently reach and return the count.
223    ///
224    /// Best-effort: producers keep publishing throughout, so this is not a
225    /// barrier and the queue is not guaranteed empty on return. It stops at
226    /// the first dangling-tail window rather than spinning through it.
227    ///
228    /// Consumer-side only.
229    pub fn clear(&mut self) -> usize {
230        let mut n = 0;
231        while let PopResult::Some(_) = self.try_pop() {
232            n += 1;
233        }
234        n
235    }
236}
237
238impl<T> Default for MpscQueue<T> {
239    fn default() -> Self {
240        Self::new()
241    }
242}
243
244impl<T> Drop for MpscQueue<T> {
245    fn drop(&mut self) {
246        // Drain remaining nodes so their values' Drop impls run.
247        self.clear();
248        // The stub is owned by the Box field; nothing to do for it. Any
249        // non-stub nodes were freed by try_pop as it walked past them.
250    }
251}
252
253#[cfg(feature = "harness")]
254pub mod recipe;
255
256// Opt-in feature modules. Each is independent of the base queue and
257// gated by its own Cargo feature; `cargo add subms-mpsc-queue` alone
258// keeps the base zero-dep + std-only shape.
259//
260// See README and the cookbook page for the per-feature p99 numbers
261// and composition guidance.
262#[cfg(any(
263    feature = "mpmc",
264    feature = "bounded",
265    feature = "batch",
266    feature = "metrics",
267    feature = "affinity",
268))]
269pub mod features;
270
271#[cfg(feature = "affinity")]
272pub use features::affinity::{AffinityError, set_affinity};
273#[cfg(feature = "batch")]
274pub use features::batch::BatchMpscQueue;
275#[cfg(feature = "bounded")]
276pub use features::bounded::BoundedMpscQueue;
277#[cfg(feature = "metrics")]
278pub use features::metrics::{MetricsMpscQueue, QueueMetricsSnapshot};
279#[cfg(feature = "mpmc")]
280pub use features::mpmc::MpmcQueue;
281
282#[cfg(test)]
283#[path = "mpsc_queue_tests.rs"]
284mod mpsc_queue_tests;
285#[cfg(test)]
286#[path = "sample_app_tests.rs"]
287mod sample_app_tests;