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}