moirai_utils/queue.rs
1//! Lock-free queues for high-performance data structures.
2//!
3//! This module provides an efficient bounded multi-producer multi-consumer
4//! queue built on per-slot sequence numbers (the Vyukov algorithm).
5
6use core::cell::UnsafeCell;
7use core::sync::atomic::{AtomicUsize, Ordering};
8
9use crate::cache::CacheAligned;
10
11#[cfg(feature = "std")]
12use std::boxed::Box;
13
14#[cfg(not(feature = "std"))]
15use alloc::boxed::Box;
16
17/// Default capacity for [`LockFreeQueue`]. Large enough to avoid backpressure
18/// under normal scheduling load while bounding memory under adversarial
19/// producer rates per the bounded-resource policy.
20const DEFAULT_QUEUE_CAPACITY: usize = 65536;
21
22/// A single slot in the bounded MPMC queue.
23struct Slot<T> {
24 /// Monotonic sequence number that distinguishes empty, full, and stale
25 /// states without an ABA hazard.
26 sequence: AtomicUsize,
27 /// The slot's data. `None` when empty, `Some(item)` when filled.
28 data: UnsafeCell<Option<T>>,
29}
30
31/// A bounded, genuinely lock-free multi-producer multi-consumer queue.
32///
33/// This is an array-based MPMC queue using per-slot sequence numbers (the
34/// Vyukov algorithm). Producers and consumers operate through independent
35/// atomic head/tail cursors and never acquire a mutex or spinlock. The
36/// sequence-number protocol eliminates the ABA problem without tagged
37/// pointers or epoch-based reclamation: slots are reused in place, so no
38/// node allocation or deallocation occurs during enqueue/dequeue.
39///
40/// # Capacity
41///
42/// The queue is bounded. [`LockFreeQueue::new`] creates a queue with
43/// `DEFAULT_QUEUE_CAPACITY` slots. [`LockFreeQueue::with_capacity`] allows
44/// a custom power-of-two capacity. When the queue is full, [`enqueue`]
45/// retries with exponential backoff (preserving the unblocked-sender
46/// contract of the previous API), while [`try_enqueue`] returns `Err(item)`
47/// immediately for callers that prefer explicit backpressure.
48///
49/// # Memory safety
50///
51/// Each slot's `Option<T>` is written by the producer (replacing `None` with
52/// `Some(item)`) and taken by the consumer (replacing `Some(item)` with
53/// `None`). The sequence-number protocol guarantees that only one thread
54/// accesses a slot's data at a time: the producer writes between `sequence ==
55/// pos` and `sequence == pos+1`, the consumer reads between `sequence ==
56/// pos+1` and `sequence == pos+capacity`.
57///
58/// [`enqueue`]: LockFreeQueue::enqueue
59/// [`try_enqueue`]: LockFreeQueue::try_enqueue
60#[repr(align(64))]
61pub struct LockFreeQueue<T> {
62 buffer: Box<[Slot<T>]>,
63 mask: usize,
64 capacity: usize,
65 head: CacheAligned<AtomicUsize>,
66 tail: CacheAligned<AtomicUsize>,
67}
68
69// Safety: The sequence-number protocol ensures that each slot's data is
70// accessed by at most one thread at a time: a producer writes between
71// sequence == pos and sequence == pos+1; a consumer takes between
72// sequence == pos+1 and sequence == pos+capacity. The head and tail atomics
73// are independently advanced via CAS, so no global lock is needed. T: Send
74// is sufficient because ownership of the value transfers between threads
75// through the slot, never shared concurrently.
76unsafe impl<T: Send> Send for LockFreeQueue<T> {}
77unsafe impl<T: Send> Sync for LockFreeQueue<T> {}
78
79impl<T> LockFreeQueue<T> {
80 /// Create a new queue with the default capacity.
81 pub fn new() -> Self {
82 Self::with_capacity(DEFAULT_QUEUE_CAPACITY)
83 }
84
85 /// Create a new queue with a custom capacity (must be a power of 2).
86 ///
87 /// # Panics
88 ///
89 /// Panics if `capacity` is 0 or not a power of 2.
90 pub fn with_capacity(capacity: usize) -> Self {
91 assert!(capacity > 0, "Capacity must be greater than 0");
92 assert!(capacity.is_power_of_two(), "Capacity must be a power of 2");
93
94 #[cfg(feature = "std")]
95 let buffer: Box<[Slot<T>]> = (0..capacity)
96 .map(|i| Slot {
97 sequence: AtomicUsize::new(i),
98 data: UnsafeCell::new(None),
99 })
100 .collect::<std::vec::Vec<_>>()
101 .into_boxed_slice();
102
103 #[cfg(not(feature = "std"))]
104 let buffer: Box<[Slot<T>]> = (0..capacity)
105 .map(|i| Slot {
106 sequence: AtomicUsize::new(i),
107 data: UnsafeCell::new(None),
108 })
109 .collect::<alloc::vec::Vec<_>>()
110 .into_boxed_slice();
111
112 Self {
113 buffer,
114 mask: capacity - 1,
115 capacity,
116 head: CacheAligned::new(AtomicUsize::new(0)),
117 tail: CacheAligned::new(AtomicUsize::new(0)),
118 }
119 }
120
121 /// Try to enqueue an item without blocking.
122 ///
123 /// Returns `Ok(())` if the item was enqueued, or `Err(item)` if the
124 /// queue is full. This is the lock-free fast path: no spinlock, no
125 /// mutex, no retry loop.
126 #[inline]
127 pub fn try_enqueue(&self, item: T) -> Result<(), T> {
128 let mut pos = self.tail.load(Ordering::Relaxed);
129 loop {
130 let slot = &self.buffer[pos & self.mask];
131 let seq = slot.sequence.load(Ordering::Acquire);
132 let diff = seq.wrapping_sub(pos) as isize;
133
134 if diff == 0 {
135 // Slot is empty: try to claim it by advancing tail.
136 match self.tail.compare_exchange_weak(
137 pos,
138 pos.wrapping_add(1),
139 Ordering::Relaxed,
140 Ordering::Relaxed,
141 ) {
142 Ok(_) => {
143 // SAFETY: we successfully claimed this slot via CAS on
144 // tail. No other producer can claim the same slot
145 // position because tail has advanced past it. The
146 // sequence == pos invariant guarantees no consumer is
147 // reading this slot.
148 unsafe {
149 *slot.data.get() = Some(item);
150 }
151 slot.sequence.store(pos.wrapping_add(1), Ordering::Release);
152 return Ok(());
153 }
154 Err(actual) => pos = actual,
155 }
156 } else if diff < 0 {
157 // Queue is full: sequence lags behind tail, meaning all slots
158 // between head and tail are occupied.
159 return Err(item);
160 } else {
161 // Another producer advanced tail before us: reload and retry.
162 pos = self.tail.load(Ordering::Relaxed);
163 }
164 }
165 }
166
167 /// Enqueue an item, retrying with exponential backoff if the queue is full.
168 ///
169 /// This preserves the unblocked-sender contract of the previous API: the
170 /// call always eventually succeeds (assuming consumers make progress).
171 /// The backoff path uses `core::hint::spin_loop` and, on std targets,
172 /// `std::thread::yield_now` after heavy contention, but never acquires a
173 /// global lock, so multiple producers can enqueue concurrently.
174 #[inline]
175 pub fn enqueue(&self, item: T) {
176 let mut backoff: usize = 1;
177 let mut item = Some(item);
178 loop {
179 match self.try_enqueue(item.take().expect("invariant: item present")) {
180 Ok(()) => return,
181 Err(returned) => {
182 item = Some(returned);
183 for _ in 0..backoff {
184 core::hint::spin_loop();
185 }
186 if backoff < 64 {
187 backoff = backoff.saturating_mul(2);
188 } else {
189 #[cfg(feature = "std")]
190 {
191 std::thread::yield_now();
192 }
193 backoff = 1;
194 }
195 }
196 }
197 }
198 }
199
200 /// Try to dequeue an item from the front of the queue.
201 /// Returns `None` if the queue is empty.
202 ///
203 /// This is the lock-free fast path: no spinlock, no mutex.
204 #[inline]
205 pub fn try_dequeue(&self) -> Option<T> {
206 let mut pos = self.head.load(Ordering::Relaxed);
207 loop {
208 let slot = &self.buffer[pos & self.mask];
209 let seq = slot.sequence.load(Ordering::Acquire);
210 let diff = seq.wrapping_sub(pos.wrapping_add(1)) as isize;
211
212 if diff == 0 {
213 // Slot has data: try to claim it by advancing head.
214 match self.head.compare_exchange_weak(
215 pos,
216 pos.wrapping_add(1),
217 Ordering::Relaxed,
218 Ordering::Relaxed,
219 ) {
220 Ok(_) => {
221 // SAFETY: we successfully claimed this slot via CAS on
222 // head. No other consumer can claim the same slot
223 // because head has advanced past it. The
224 // sequence == pos+1 invariant guarantees the producer
225 // has finished writing and no producer will write
226 // again until we set sequence to pos+capacity.
227 let item = unsafe { (*slot.data.get()).take() };
228 slot.sequence
229 .store(pos.wrapping_add(self.capacity), Ordering::Release);
230 return item;
231 }
232 Err(actual) => pos = actual,
233 }
234 } else if diff < 0 {
235 // Queue is empty: sequence has not advanced past pos+1.
236 return None;
237 } else {
238 // Another consumer advanced head before us: reload and retry.
239 pos = self.head.load(Ordering::Relaxed);
240 }
241 }
242 }
243
244 /// Check if the queue is empty.
245 ///
246 /// This is a best-effort check: the queue may have items added or removed
247 /// between this call and the next operation. It is safe to call
248 /// concurrently with enqueue/dequeue.
249 pub fn is_empty(&self) -> bool {
250 let head = self.head.load(Ordering::Relaxed);
251 let tail = self.tail.load(Ordering::Relaxed);
252 head == tail
253 }
254
255 /// Returns the queue capacity.
256 pub const fn capacity(&self) -> usize {
257 self.capacity
258 }
259}
260
261impl<T> Default for LockFreeQueue<T> {
262 fn default() -> Self {
263 Self::new()
264 }
265}
266
267impl<T> Drop for LockFreeQueue<T> {
268 fn drop(&mut self) {
269 // Drain remaining items so their destructors run.
270 while self.try_dequeue().is_some() {}
271 }
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277 use core::sync::atomic::AtomicUsize;
278
279 #[cfg(feature = "std")]
280 use std::sync::Arc;
281
282 #[cfg(not(feature = "std"))]
283 use alloc::sync::Arc;
284
285 #[test]
286 fn test_lock_free_queue_basic() {
287 let queue = LockFreeQueue::<i32>::with_capacity(4);
288 assert!(queue.is_empty());
289
290 queue.enqueue(1);
291 queue.enqueue(2);
292 assert!(!queue.is_empty());
293
294 assert_eq!(queue.try_dequeue(), Some(1));
295 assert_eq!(queue.try_dequeue(), Some(2));
296 assert_eq!(queue.try_dequeue(), None);
297 assert!(queue.is_empty());
298 }
299
300 #[test]
301 fn test_lock_free_queue_wrap_around() {
302 // Fill and drain multiple times to exercise slot reuse.
303 let queue = LockFreeQueue::<i32>::with_capacity(4);
304 for round in 0..16 {
305 for i in 0..3 {
306 queue.enqueue(round * 3 + i);
307 }
308 for i in 0..3 {
309 assert_eq!(
310 queue.try_dequeue(),
311 Some(round * 3 + i),
312 "round {round}, item {i}"
313 );
314 }
315 assert!(queue.try_dequeue().is_none(), "round {round} not empty");
316 }
317 }
318
319 #[test]
320 fn test_lock_free_queue_full_try_enqueue() {
321 let queue = LockFreeQueue::<i32>::with_capacity(4);
322 for i in 0..4 {
323 queue.try_enqueue(i).unwrap();
324 }
325 // Now full.
326 assert!(queue.try_enqueue(99).is_err());
327 assert_eq!(queue.try_dequeue(), Some(0));
328 // One slot freed.
329 queue.try_enqueue(99).unwrap();
330 }
331
332 #[test]
333 fn test_lock_free_queue_drop_runs_destructors() {
334 struct DropCounter {
335 counter: Arc<AtomicUsize>,
336 }
337 impl Drop for DropCounter {
338 fn drop(&mut self) {
339 self.counter.fetch_add(1, Ordering::Relaxed);
340 }
341 }
342
343 let counter = Arc::new(AtomicUsize::new(0));
344 {
345 let queue = LockFreeQueue::<DropCounter>::with_capacity(4);
346 for _ in 0..3 {
347 queue.enqueue(DropCounter {
348 counter: Arc::clone(&counter),
349 });
350 }
351 // Drop the queue without draining: destructors must run.
352 }
353 assert_eq!(counter.load(Ordering::Relaxed), 3);
354 }
355
356 #[cfg(feature = "std")]
357 #[test]
358 fn test_lock_free_queue_concurrent_mpmc() {
359 use std::thread;
360
361 let queue = Arc::new(LockFreeQueue::<i32>::with_capacity(1024));
362 let num_producers = 4;
363 let num_consumers = 4;
364 let items_per_producer = 1000;
365 let total_items = num_producers * items_per_producer;
366
367 let mut handles = Vec::new();
368
369 for p in 0..num_producers {
370 let q = Arc::clone(&queue);
371 handles.push(thread::spawn(move || {
372 for i in 0..items_per_producer {
373 q.enqueue((p * items_per_producer + i) as i32);
374 }
375 }));
376 }
377
378 let consumed = Arc::new(AtomicUsize::new(0));
379 for _ in 0..num_consumers {
380 let q = Arc::clone(&queue);
381 let c = Arc::clone(&consumed);
382 handles.push(thread::spawn(move || {
383 while c.load(Ordering::Relaxed) < total_items {
384 if q.try_dequeue().is_some() {
385 c.fetch_add(1, Ordering::Relaxed);
386 } else {
387 std::thread::yield_now();
388 }
389 }
390 }));
391 }
392
393 for h in handles {
394 h.join().unwrap();
395 }
396
397 assert_eq!(consumed.load(Ordering::Relaxed), total_items);
398 }
399}