low_latency_data_structures/spsc/
queue.rs1use crossbeam_utils::CachePadded;
2
3use crate::mem::{Allocation, Allocator};
4use crate::shim::sync::atomic::AtomicUsize;
5use crate::shim::sync::{Arc, atomic};
6use crate::spsc::builder::Options;
7use crate::spsc::consumer::Consumer;
8use crate::spsc::producer::Producer;
9
10pub fn new<T, const CAPACITY: usize, A>(
48 options: Options<A>,
49) -> (Producer<T, CAPACITY, A>, Consumer<T, CAPACITY, A>)
50where
51 A: Allocator,
52{
53 const {
54 assert!(
55 CAPACITY.is_power_of_two(),
56 "Given capacity is not a power of two!"
57 );
58 };
59 let slots_allocation = A::allocate(CAPACITY, options.alloc);
60 let q = Arc::new(Queue {
61 head: AtomicUsize::new(0).into(),
62 tail: AtomicUsize::new(0).into(),
63 slots_allocation,
64 });
65 let producer = Producer::new(q.clone());
66 let consumer = Consumer::new(q);
67 (producer, consumer)
68}
69
70pub(super) struct Queue<T, const CAPACITY: usize, A>
71where
72 A: Allocator,
73{
74 pub(super) slots_allocation: A::Allocation<T>,
75 pub(super) head: CachePadded<AtomicUsize>,
76 pub(super) tail: CachePadded<AtomicUsize>,
77}
78
79impl<T, const CAPACITY: usize, A> Drop for Queue<T, CAPACITY, A>
80where
81 A: Allocator,
82{
83 fn drop(&mut self) {
84 let head = self.head.load(atomic::Ordering::Relaxed);
85 let tail = self.tail.load(atomic::Ordering::Relaxed);
86 let count = tail.wrapping_sub(head);
87 for k in 0..count {
88 let i = head.wrapping_add(k);
89 let slot_ptr = self.slots_allocation.ptr().wrapping_add(i & (CAPACITY - 1));
90 unsafe {
94 slot_ptr.as_ref_unchecked().with_mut(|ptr| {
95 ptr.as_mut_unchecked().assume_init_drop();
96 })
97 }
98 }
99 }
100}
101
102#[cfg(test)]
103#[cfg(feature = "tests_basic")]
104mod tests_basic {
105 use std::cell::Cell;
106 use std::rc::Rc;
107 use std::thread;
108
109 use seq_macro::seq;
110
111 use super::new;
112 #[cfg(not(feature = "tests_hugepage"))]
113 use crate::mem::global::{GlobalAllocator, GlobalAllocatorOptions};
114 #[cfg(feature = "tests_hugepage")]
115 use crate::mem::hugepages::{HugepageAllocator, HugepageAllocatorOptions, HugepageSize};
116 use crate::spsc::builder::Options;
117
118 #[cfg(not(feature = "tests_hugepage"))]
119 fn alloc_options() -> GlobalAllocatorOptions {
120 GlobalAllocatorOptions::builder().mlock(true).build()
121 }
122
123 #[cfg(not(feature = "tests_hugepage"))]
124 type Alloc = GlobalAllocator;
125 #[cfg(feature = "tests_hugepage")]
126 type Alloc = HugepageAllocator;
127
128 #[cfg(feature = "tests_hugepage")]
129 fn alloc_options() -> HugepageAllocatorOptions {
130 HugepageAllocatorOptions::builder()
131 .mlock(true)
132 .hugepage_size(HugepageSize::H2MB)
133 .build()
134 }
135
136 fn spsc_options() -> Options<Alloc> {
137 Options::builder().alloc(alloc_options()).build()
138 }
139
140 #[test]
141 fn move_producer_consumer_to_threads() {
142 let (mut producer, mut consumer) = new::<_, 2, Alloc>(spsc_options());
143 thread::spawn(move || {
144 let _ = producer.push(123);
145 })
146 .join()
147 .unwrap();
148 thread::spawn(move || {
149 assert_eq!(consumer.pop(), Some(123));
150 })
151 .join()
152 .unwrap();
153 }
154
155 #[test]
156 fn handoff_one_value() {
157 let (mut producer, mut consumer) = new::<_, 2, Alloc>(spsc_options());
158 assert_eq!(producer.push(123), None);
159 assert_eq!(consumer.pop(), Some(123));
160 }
161
162 #[test]
163 fn allows_queues_with_powers_of_two_capacity() {
164 #[cfg(feature = "tests_hugepage")]
165 seq!(N in 0..6 {
166 {
167 const POWER: usize = 2usize.pow(N);
168 new::<u32, POWER, Alloc>(spsc_options());
169 }
170 });
171 #[cfg(not(feature = "tests_hugepage"))]
172 seq!(N in 0..20 {
173 {
174 const POWER: usize = 2usize.pow(N);
175 new::<u32, POWER, Alloc>(spsc_options());
176 }
177 });
178 }
179
180 #[test]
181 fn drops_unread_items() {
182 let counter = Rc::new(Cell::new(0));
183 #[derive(Debug, PartialEq)]
184 struct Droppable {
185 counter: Rc<Cell<usize>>,
186 }
187 impl Drop for Droppable {
188 fn drop(&mut self) {
189 let cnt = self.counter.get();
190 self.counter.set(cnt + 1);
191 }
192 }
193 const CAPACITY: usize = 64;
194 let (mut producer, mut consumer) = new::<_, CAPACITY, Alloc>(spsc_options());
195 for _ in 0..CAPACITY {
196 let counter = counter.clone();
197 assert_eq!(producer.push(Droppable { counter }), None);
198 }
199 let read = CAPACITY / 2;
200 for _ in 0..read {
201 assert!(matches!(consumer.pop(), Some(_)));
202 }
203 assert_eq!(read, counter.get());
204 drop(producer);
205 drop(consumer);
206 assert_eq!(CAPACITY, counter.get());
207 }
208
209 #[test]
210 fn empty_returns_none() {
211 let (_, mut consumer) = new::<i32, 4, Alloc>(spsc_options());
212 assert_eq!(consumer.pop(), None);
213 assert_eq!(consumer.pop(), None);
214 }
215
216 #[test]
217 fn full_returns_item_back() {
218 let (mut producer, _) = new::<i32, 2, Alloc>(spsc_options());
219 assert_eq!(producer.push(1), None);
220 assert_eq!(producer.push(2), None);
221 assert_eq!(producer.push(3), Some(3));
223 assert_eq!(producer.push(4), Some(4));
224 }
225
226 #[test]
227 fn fifo_ordering() -> anyhow::Result<()> {
228 let (mut producer, mut consumer) = new::<_, 8, Alloc>(spsc_options());
229 for i in 0..8 {
230 assert_eq!(producer.push(i), None);
231 }
232 for i in 0..8 {
233 assert_eq!(consumer.pop(), Some(i));
234 }
235 Ok(())
236 }
237
238 #[test]
239 fn wraparound_n_laps() {
240 const CAPACITY: usize = 4;
241 let laps = 100;
242 let (mut producer, mut consumer) = new::<_, CAPACITY, Alloc>(spsc_options());
243 for lap in 0..laps {
244 for i in 0..CAPACITY {
245 let val = lap * CAPACITY + i;
246 assert_eq!(producer.push(val), None, "push failed at lap {lap}, i {i}");
247 }
248 assert_eq!(producer.push(9999), Some(9999));
250 for i in 0..CAPACITY {
251 let val = lap * CAPACITY + i;
252 assert_eq!(consumer.pop(), Some(val), "wrong value at lap {lap}, i {i}");
253 }
254 assert_eq!(consumer.pop(), None);
256 }
257 }
258
259 #[test]
260 fn interleaved_push_pop() {
261 let (mut producer, mut consumer) = new::<_, 2, Alloc>(spsc_options());
262 for i in 0..1000 {
264 assert_eq!(producer.push(i), None);
265 assert_eq!(consumer.pop(), Some(i));
266 }
267 }
268
269 #[test]
270 fn capacity_one() {
271 let (mut producer, mut consumer) = new::<_, 1, Alloc>(spsc_options());
272 assert_eq!(consumer.pop(), None);
273 assert_eq!(producer.push(42), None);
274 assert_eq!(producer.push(43), Some(43)); assert_eq!(consumer.pop(), Some(42));
276 assert_eq!(consumer.pop(), None); }
278
279 #[test]
280 fn move_only_type() {
281 let (mut producer, mut consumer) = new::<_, 4, Alloc>(spsc_options());
283 let s = String::from("hello");
284 assert_eq!(producer.push(s), None);
285 let got = consumer.pop().unwrap();
286 assert_eq!(got, "hello");
287 }
288}
289
290#[cfg(test)]
291#[cfg(feature = "tests_loom")]
292mod tests_loom {
293 use super::*;
294 use crate::mem::loom::{LoomVecAllocator, LoomVecAllocatorOptions};
295
296 static_assertions::assert_cfg!(
297 not(feature = "tests_hugepage"),
298 "tests_loom incompatible with tests_hugepage as loom uses custom buffer allocator",
299 );
300
301 fn spsc_options() -> Options<LoomVecAllocator> {
302 Options::builder().alloc(LoomVecAllocatorOptions).build()
303 }
304
305 #[test]
306 fn concurrent_push_pop() {
307 loom::model(|| {
308 let (mut producer, mut consumer) = new::<i32, 4, _>(spsc_options());
309
310 let t1 = loom::thread::spawn(move || {
311 let _ = producer.push(1);
312 let _ = producer.push(2);
313 let _ = producer.push(3);
314 });
315
316 let t2 = loom::thread::spawn(move || {
317 let mut collected = Vec::new();
318 while collected.len() < 3 {
319 if let Some(v) = consumer.pop() {
320 collected.push(v);
321 } else {
322 loom::thread::yield_now();
323 }
324 }
325 collected
326 });
327
328 t1.join().unwrap();
329 let values = t2.join().unwrap();
330 assert_eq!(values, vec![1, 2, 3]);
332 });
333 }
334
335 #[test]
337 fn concurrent_with_full_queue() {
338 loom::model(|| {
339 let (mut producer, mut consumer) = new::<i32, 1, _>(spsc_options());
340
341 let t1 = loom::thread::spawn(move || {
342 for i in 0..3 {
343 while producer.push(i).is_some() {
344 loom::thread::yield_now();
345 }
346 }
347 });
348
349 let t2 = loom::thread::spawn(move || {
350 let mut collected = Vec::new();
351 while collected.len() < 3 {
352 if let Some(v) = consumer.pop() {
353 collected.push(v);
354 } else {
355 loom::thread::yield_now();
356 }
357 }
358 collected
359 });
360
361 t1.join().unwrap();
362 let values = t2.join().unwrap();
363 assert_eq!(values, vec![0, 1, 2]);
364 });
365 }
366
367 #[test]
368 fn concurrent_no_values_lost() {
369 loom::model(|| {
370 let (mut producer, mut consumer) = new::<i32, 2, _>(spsc_options());
371
372 let t1 = loom::thread::spawn(move || {
373 for i in 0..3 {
374 while producer.push(i).is_some() {
375 loom::thread::yield_now();
376 }
377 }
378 });
379
380 let t2 = loom::thread::spawn(move || {
381 let mut sum = 0;
382 let mut count = 0;
383 while count < 3 {
384 if let Some(v) = consumer.pop() {
385 sum += v;
386 count += 1;
387 } else {
388 loom::thread::yield_now();
389 }
390 }
391 (count, sum)
392 });
393
394 t1.join().unwrap();
395 let (count, sum) = t2.join().unwrap();
396 assert_eq!(count, 3);
397 assert_eq!(sum, 0 + 1 + 2);
398 });
399 }
400}
401
402#[cfg(test)]
403#[cfg(feature = "tests_dhat")]
404mod tests_dhat {
405 use super::*;
406 use crate::mem::global::GlobalAllocator;
407
408 #[test]
409 fn hot_path_zero_allocations() {
410 let _profiler = dhat::Profiler::builder().testing().build();
411 let (mut producer, mut consumer) =
412 new::<u64, 1024, GlobalAllocator>(Options::global_mlocked());
413
414 for i in 0..1024u64 {
420 let _ = producer.push(i);
421 }
422 for _ in 0..1024 {
423 let _ = consumer.pop();
424 }
425 let _ = dhat::HeapStats::get();
426
427 let before = dhat::HeapStats::get();
428
429 for i in 0..1024u64 {
430 let _ = producer.push(i);
431 }
432 for _ in 0..1024 {
433 let _ = consumer.pop();
434 }
435
436 let after = dhat::HeapStats::get();
437 let allocs = after.total_blocks - before.total_blocks;
438 dhat::assert!(
439 allocs == 0,
440 "hot path allocated {} blocks; a real regression would be O(iters)",
441 allocs,
442 );
443 }
444}