Skip to main content

scirs2_core/concurrent/
queue.rs

1//! Lock-free MPMC bounded queue using a ring buffer with atomic operations.
2//!
3//! This module implements a high-performance multi-producer, multi-consumer
4//! bounded queue that avoids mutexes on the hot path.  Each slot has its own
5//! sequence number so producers and consumers can independently claim a slot
6//! without blocking one another.
7//!
8//! # Algorithm
9//!
10//! Each ring-buffer slot stores:
11//! - `sequence: AtomicUsize` — an ever-increasing stamp that encodes the slot
12//!   state (empty/ready-to-read).
13//! - `value: UnsafeCell<MaybeUninit<T>>` — the payload.
14//!
15//! A producer:
16//!   1. Atomically increments the shared `tail`.
17//!   2. Waits (spin) until `slot.sequence == tail` (the slot was last read by
18//!      `tail - capacity` ago, so it is now free).
19//!   3. Writes the value and sets `slot.sequence = tail + 1` (signals the
20//!      consumer that the slot is ready).
21//!
22//! A consumer mirrors the process using `head`.
23//!
24//! This is the classic Dmitry Vyukov MPMC queue design.
25
26use std::cell::UnsafeCell;
27use std::mem::MaybeUninit;
28use std::sync::atomic::{AtomicUsize, Ordering};
29
30/// Cache-line padding to avoid false sharing between hot atomic fields.
31#[repr(align(64))]
32struct Padded<T>(T);
33
34/// One slot in the ring buffer.
35struct Slot<T> {
36    sequence: AtomicUsize,
37    value: UnsafeCell<MaybeUninit<T>>,
38}
39
40impl<T> Slot<T> {
41    fn new(seq: usize) -> Self {
42        Slot {
43            sequence: AtomicUsize::new(seq),
44            value: UnsafeCell::new(MaybeUninit::uninit()),
45        }
46    }
47}
48
49// SAFETY: `Slot` is only accessed through carefully sequenced atomic ops.
50unsafe impl<T: Send> Send for Slot<T> {}
51unsafe impl<T: Send> Sync for Slot<T> {}
52
53/// A bounded, lock-free multi-producer / multi-consumer queue.
54///
55/// `T` must be `Send`.  All operations are wait-free from the caller's point
56/// of view: if the queue is full `push` returns `false`; if it is empty `pop`
57/// returns `None`.  There is no spinning inside the public API.
58///
59/// # Example
60///
61/// ```rust
62/// use scirs2_core::concurrent::LockFreeQueue;
63///
64/// let q: LockFreeQueue<i32> = LockFreeQueue::new(4);
65/// assert!(q.push(1));
66/// assert!(q.push(2));
67/// assert_eq!(q.pop(), Some(1));
68/// assert_eq!(q.pop(), Some(2));
69/// assert_eq!(q.pop(), None);
70/// ```
71pub struct LockFreeQueue<T> {
72    buffer: Vec<Slot<T>>,
73    capacity: usize,
74    mask: usize,
75    head: Padded<AtomicUsize>,
76    tail: Padded<AtomicUsize>,
77}
78
79// SAFETY: the internal slots are only mutated while holding the implicit
80// sequence-number "lock", so the queue is safe to share across threads.
81unsafe impl<T: Send> Send for LockFreeQueue<T> {}
82unsafe impl<T: Send> Sync for LockFreeQueue<T> {}
83
84impl<T> LockFreeQueue<T> {
85    /// Create a new queue with `capacity` rounded up to the next power of two.
86    ///
87    /// The minimum capacity is 1; if `capacity` is 0 it is treated as 1.
88    pub fn new(capacity: usize) -> Self {
89        // The Vyukov MPMC queue requires capacity >= 2 to correctly
90        // distinguish "slot free for producer" from "slot has data for consumer."
91        // With capacity 1, the sequence-number check becomes ambiguous and the
92        // queue deadlocks.
93        let cap = capacity.max(2).next_power_of_two();
94        let buffer: Vec<Slot<T>> = (0..cap).map(|i| Slot::new(i)).collect();
95        LockFreeQueue {
96            buffer,
97            capacity: cap,
98            mask: cap - 1,
99            head: Padded(AtomicUsize::new(0)),
100            tail: Padded(AtomicUsize::new(0)),
101        }
102    }
103
104    /// Attempt to push `val` onto the queue.
105    ///
106    /// Returns `false` without modifying the queue if it is full.
107    pub fn push(&self, val: T) -> bool {
108        let mut pos = self.tail.0.load(Ordering::Relaxed);
109        loop {
110            let slot = &self.buffer[pos & self.mask];
111            let seq = slot.sequence.load(Ordering::Acquire);
112            let diff = seq as isize - pos as isize;
113            match diff.cmp(&0) {
114                std::cmp::Ordering::Equal => {
115                    // Slot is free — try to claim it.
116                    match self.tail.0.compare_exchange_weak(
117                        pos,
118                        pos.wrapping_add(1),
119                        Ordering::Relaxed,
120                        Ordering::Relaxed,
121                    ) {
122                        Ok(_) => {
123                            // We own the slot; write value and publish.
124                            unsafe {
125                                (*slot.value.get()).write(val);
126                            }
127                            slot.sequence.store(pos.wrapping_add(1), Ordering::Release);
128                            return true;
129                        }
130                        Err(updated) => {
131                            pos = updated;
132                        }
133                    }
134                }
135                std::cmp::Ordering::Less => {
136                    // Queue full.
137                    return false;
138                }
139                std::cmp::Ordering::Greater => {
140                    // Another producer moved tail; reload.
141                    pos = self.tail.0.load(Ordering::Relaxed);
142                }
143            }
144        }
145    }
146
147    /// Attempt to pop a value from the queue.
148    ///
149    /// Returns `None` if the queue is currently empty.
150    pub fn pop(&self) -> Option<T> {
151        let mut pos = self.head.0.load(Ordering::Relaxed);
152        loop {
153            let slot = &self.buffer[pos & self.mask];
154            let seq = slot.sequence.load(Ordering::Acquire);
155            let diff = seq as isize - pos.wrapping_add(1) as isize;
156            match diff.cmp(&0) {
157                std::cmp::Ordering::Equal => {
158                    // Slot has data — try to claim it.
159                    match self.head.0.compare_exchange_weak(
160                        pos,
161                        pos.wrapping_add(1),
162                        Ordering::Relaxed,
163                        Ordering::Relaxed,
164                    ) {
165                        Ok(_) => {
166                            // We own the slot; read value and free.
167                            let val = unsafe { (*slot.value.get()).assume_init_read() };
168                            slot.sequence
169                                .store(pos.wrapping_add(self.capacity), Ordering::Release);
170                            return Some(val);
171                        }
172                        Err(updated) => {
173                            pos = updated;
174                        }
175                    }
176                }
177                std::cmp::Ordering::Less => {
178                    // Queue empty.
179                    return None;
180                }
181                std::cmp::Ordering::Greater => {
182                    pos = self.head.0.load(Ordering::Relaxed);
183                }
184            }
185        }
186    }
187
188    /// Return the number of items currently in the queue (approximate under
189    /// concurrent access).
190    pub fn len(&self) -> usize {
191        let tail = self.tail.0.load(Ordering::Relaxed);
192        let head = self.head.0.load(Ordering::Relaxed);
193        tail.saturating_sub(head)
194    }
195
196    /// Return `true` if the queue appears empty (approximate).
197    pub fn is_empty(&self) -> bool {
198        self.len() == 0
199    }
200
201    /// Return the maximum number of items this queue can hold.
202    pub fn capacity(&self) -> usize {
203        self.capacity
204    }
205}
206
207impl<T> Drop for LockFreeQueue<T> {
208    fn drop(&mut self) {
209        // Drain remaining items so their destructors run.
210        while self.pop().is_some() {}
211    }
212}
213
214// ---------------------------------------------------------------------------
215// Tests
216// ---------------------------------------------------------------------------
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221    use std::sync::Arc;
222    use std::thread;
223
224    #[test]
225    fn test_basic_push_pop() {
226        let q: LockFreeQueue<u32> = LockFreeQueue::new(4);
227        assert!(q.is_empty());
228        assert!(q.push(10));
229        assert!(q.push(20));
230        assert_eq!(q.len(), 2);
231        assert_eq!(q.pop(), Some(10));
232        assert_eq!(q.pop(), Some(20));
233        assert_eq!(q.pop(), None);
234    }
235
236    #[test]
237    fn test_capacity_limit() {
238        let q: LockFreeQueue<i32> = LockFreeQueue::new(4);
239        // capacity rounds up to next power-of-two = 4
240        assert_eq!(q.capacity(), 4);
241        assert!(q.push(1));
242        assert!(q.push(2));
243        assert!(q.push(3));
244        assert!(q.push(4));
245        // Fifth push must fail.
246        assert!(!q.push(5));
247        // After a pop, one push should succeed.
248        assert_eq!(q.pop(), Some(1));
249        assert!(q.push(5));
250    }
251
252    #[test]
253    fn test_fifo_order() {
254        let q: LockFreeQueue<usize> = LockFreeQueue::new(16);
255        for i in 0..10 {
256            assert!(q.push(i));
257        }
258        for i in 0..10 {
259            assert_eq!(q.pop(), Some(i));
260        }
261        assert_eq!(q.pop(), None);
262    }
263
264    #[test]
265    fn test_concurrent_mpmc() {
266        const PRODUCERS: usize = 4;
267        const ITEMS_PER_PRODUCER: usize = 1_000;
268        const CAPACITY: usize = 256;
269
270        let q = Arc::new(LockFreeQueue::<usize>::new(CAPACITY));
271        let total = PRODUCERS * ITEMS_PER_PRODUCER;
272
273        // Spawn producers.
274        let handles: Vec<_> = (0..PRODUCERS)
275            .map(|_p| {
276                let q2 = Arc::clone(&q);
277                thread::spawn(move || {
278                    let mut sent = 0usize;
279                    while sent < ITEMS_PER_PRODUCER {
280                        if q2.push(1) {
281                            sent += 1;
282                        } else {
283                            thread::yield_now();
284                        }
285                    }
286                })
287            })
288            .collect();
289
290        // Consumer on main thread.
291        let mut received = 0usize;
292        while received < total {
293            if let Some(_) = q.pop() {
294                received += 1;
295            } else {
296                thread::yield_now();
297            }
298        }
299
300        for h in handles {
301            h.join().expect("producer thread panicked");
302        }
303
304        assert_eq!(received, total);
305        assert!(q.is_empty());
306    }
307
308    #[test]
309    fn test_drop_runs_destructors() {
310        use std::sync::atomic::AtomicUsize;
311        use std::sync::Arc;
312
313        let counter = Arc::new(AtomicUsize::new(0));
314
315        struct Tracker(Arc<AtomicUsize>);
316        impl Drop for Tracker {
317            fn drop(&mut self) {
318                self.0.fetch_add(1, Ordering::Relaxed);
319            }
320        }
321
322        {
323            let q: LockFreeQueue<Tracker> = LockFreeQueue::new(8);
324            q.push(Tracker(Arc::clone(&counter)));
325            q.push(Tracker(Arc::clone(&counter)));
326            // Queue is dropped here with 2 items inside.
327        }
328
329        assert_eq!(counter.load(Ordering::Relaxed), 2);
330    }
331
332    #[test]
333    fn test_zero_capacity_becomes_one() {
334        let q: LockFreeQueue<u8> = LockFreeQueue::new(0);
335        // Minimum capacity is 2 (Vyukov MPMC requires >= 2 to avoid
336        // sequence-number ambiguity); next_power_of_two(2) == 2.
337        assert_eq!(q.capacity(), 2);
338        assert!(q.push(42));
339        assert!(q.push(43));
340        // Now full — third push must fail.
341        assert!(!q.push(44));
342        assert_eq!(q.pop(), Some(42));
343        assert_eq!(q.pop(), Some(43));
344        assert_eq!(q.pop(), None);
345    }
346}