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;