Skip to main content

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}