-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdvyukov_mpmc_queue.zig
More file actions
687 lines (621 loc) · 30.9 KB
/
Copy pathdvyukov_mpmc_queue.zig
File metadata and controls
687 lines (621 loc) · 30.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
const std = @import("std");
const Allocator = std.mem.Allocator;
const Atomic = std.atomic.Value;
const Thread = std.Thread;
/// Vyukov Bounded MPMC Queue
///
/// A high-performance lock-free Multiple Producer Multiple Consumer queue
/// based on Dmitry Vyukov's bounded MPMC queue algorithm.
///
/// Reference: http://www.1024cores.net/home/lock-free-algorithms/queues/bounded-mpmc-queue
///
/// Performance characteristics:
/// - Lock-free: O(1) enqueue and dequeue (with retry on contention)
/// - Cache-efficient: proper padding to avoid false sharing
/// - High throughput: ~50-100M ops/sec on modern hardware
/// - Low latency: ~20-50ns per operation
///
/// ⚠️ PERFORMANCE WARNING - Large T Types:
/// Each Cell contains both T and a u64 sequence number, aligned to cache lines.
/// When sizeof(T) is large (>64 bytes), performance degrades significantly:
///
/// - Small T (1-64 bytes): Optimal - multiple cells per cache line or 1:1
/// - Medium T (65-256 bytes): Good - 1-4 cache lines per cell
/// - Large T (257-1024 bytes): Degraded - 4-16 cache lines per cell
/// - Huge T (>1024 bytes): Poor - consider using pointers instead
///
/// Why large T hurts performance:
/// 1. Cache line walking: Each operation touches many cache lines
/// 2. Memory bandwidth: More data transferred between CPU and RAM
/// 3. Cache pollution: Large cells evict other useful data from cache
/// 4. False sharing: Adjacent cells more likely to share cache lines
///
/// RECOMMENDATION for large types:
/// Instead of DVyukovMPMCQueue(LargeStruct, N), use DVyukovMPMCQueue(*LargeStruct, N)
/// and manage allocation separately. This keeps cells small and cache-friendly.
///
/// Thread safety:
/// - MULTIPLE producers can enqueue concurrently
/// - MULTIPLE consumers can dequeue concurrently
/// - Bounded: fixed capacity, fails when full
///
/// CRITICAL - Type Requirements for T:
/// ⚠️ T MUST be trivially copyable with NO ownership semantics.
/// ⚠️ The queue does NOT call destructors on enqueued items.
/// ⚠️ Types with deinit(), allocations, file handles, or other resources
/// will LEAK MEMORY if not manually drained before calling deinit().
///
/// Safe types: integers, floats, booleans, enums, plain structs, pointers
/// Unsafe types: ArrayList, HashMap, files, sockets, types with deinit()
///
/// If T owns resources, you MUST drain the queue before deinit():
/// ```zig
/// while (queue.dequeue()) |item| {
/// item.deinit(); // Clean up each item
/// }
/// queue.deinit(); // Now safe to destroy queue
/// ```
///
/// Usage:
/// ```zig
/// var queue = try DVyukovMPMCQueue(Task, 1024).init(allocator);
/// defer queue.deinit();
///
/// // Producer threads
/// queue.enqueue(task) catch |err| {
/// // Handle full queue
/// };
///
/// // Consumer threads
/// if (queue.dequeue()) |task| {
/// // Process task
/// }
/// ```
pub fn DVyukovMPMCQueue(comptime T: type, comptime capacity: usize) type {
// Capacity must be power of 2 for fast modulo
comptime {
if (!std.math.isPowerOfTwo(capacity)) {
@compileError("DVyukovMPMCQueue capacity must be a power of 2");
}
if (capacity < 2) {
@compileError("DVyukovMPMCQueue capacity must be at least 2 (Vyukov algorithm limitation)");
}
// Ensure cache line is large enough for padding to work correctly
// On most platforms cache_line is 64 or 128 bytes, while Atomic(u64) is 8 bytes
// This check catches exotic platforms where cache_line might equal sizeof(Atomic(u64))
if (std.atomic.cache_line < @sizeOf(std.atomic.Value(u64))) {
@compileError("Platform has cache_line < sizeof(Atomic(u64)) - padding would underflow");
}
// PERFORMANCE: Enforce small T for optimal cache behavior
const t_size = @sizeOf(T);
const cache_line = std.atomic.cache_line;
const cell_size = t_size + @sizeOf(std.atomic.Value(u64));
// HARD ERROR: T larger than cache_line is almost certainly wrong
// Each Cell would span 2+ cache lines → severe cache behavior degradation
// This is the root cause of 2-5x performance loss in multi-threaded scenarios
if (t_size > cache_line) {
@compileError(std.fmt.comptimePrint(
"\n" ++
"═══════════════════════════════════════════════════════════════════\n" ++
" DVyukovMPMCQueue PERFORMANCE ERROR: T is too large!\n" ++
"═══════════════════════════════════════════════════════════════════\n" ++
"\n" ++
" Type size: {d} bytes (limit: {d} bytes = 1 cache line)\n" ++
" Cell size: {d} bytes (spans {d}+ cache lines)\n" ++
"\n" ++
" PERFORMANCE IMPACT:\n" ++
" • Each queue operation touches {d}+ cache lines\n" ++
" • Cache miss rate increases dramatically\n" ++
" • Expected throughput: 2-5x SLOWER than optimal\n" ++
" • Under contention: 5-10x SLOWER\n" ++
"\n" ++
" WHY THIS HAPPENS:\n" ++
" Ring buffer with {d} cells = {d} KB total\n" ++
" Large cells spread across many cache lines\n" ++
" → Constant cache line ping-pong between threads\n" ++
" → Memory bandwidth saturation\n" ++
"\n" ++
" ✓ SOLUTION: Use pointers instead of large structs\n" ++
"\n" ++
" // WRONG (large T):\n" ++
" DVyukovMPMCQueue(YourLargeStruct, {d})\n" ++
"\n" ++
" // CORRECT (pointer to T):\n" ++
" DVyukovMPMCQueue(*YourLargeStruct, {d})\n" ++
"\n" ++
" Then allocate/manage YourLargeStruct separately.\n" ++
" Cell size becomes ~16 bytes (pointer + sequence) → optimal!\n" ++
"\n" ++
"═══════════════════════════════════════════════════════════════════\n",
.{
t_size,
cache_line,
cell_size,
(cell_size + cache_line - 1) / cache_line,
(cell_size + cache_line - 1) / cache_line,
capacity,
(capacity * cell_size) / 1024,
capacity,
capacity,
},
));
}
}
return struct {
const Self = @This();
/// Individual cell in the ring buffer
/// Each cell is cache-line aligned and padded to prevent false sharing
/// The entire Cell is a multiple of cache line size, ensuring no two cells
/// share a cache line even with small T types
const Cell = struct {
/// Sequence number for synchronization
/// - Even sequence = cell is available for enqueue
/// - Odd sequence = cell contains data ready for dequeue
/// Using u64 to match position counters and avoid ABA issues
sequence: Atomic(u64) align(std.atomic.cache_line),
/// The actual data (only valid when sequence is odd)
data: T,
/// Padding to ensure Cell size is a multiple of cache line
/// This prevents adjacent cells from sharing cache lines (false sharing)
/// Formula: cache_line - ((sizeof(Atomic(u64)) + sizeof(T)) % cache_line)
/// Note: May be zero-length if natural size already aligns
_padding: [blk: {
const natural_size = @sizeOf(Atomic(u64)) + @sizeOf(T);
const remainder = natural_size % std.atomic.cache_line;
break :blk if (remainder == 0) 0 else std.atomic.cache_line - remainder;
}]u8 = undefined,
};
// Compile-time verification: Cell size must be a multiple of cache line
comptime {
const cell_size = @sizeOf(Cell);
if (cell_size % std.atomic.cache_line != 0) {
@compileError(std.fmt.comptimePrint(
"CRITICAL: Cell size ({}) is not a multiple of cache_line ({}). This causes false sharing!",
.{ cell_size, std.atomic.cache_line },
));
}
}
/// Ring buffer of cells
buffer: []Cell,
/// Allocator for cleanup
allocator: Allocator,
/// Enqueue position (tail)
/// Multiple producers compete to increment this
/// Using u64 to avoid ABA issues on 32-bit systems (u32 wraps after ~4B operations)
enqueue_pos: Atomic(u64) align(std.atomic.cache_line) = Atomic(u64).init(0),
/// Padding to prevent false sharing between enqueue_pos and dequeue_pos
/// Note: On platforms where cache_line == sizeof(Atomic(u64)), this becomes
/// a zero-length array, which is valid and expected (no padding needed).
_padding1: [std.atomic.cache_line - @sizeOf(Atomic(u64))]u8 = undefined,
/// Dequeue position (head)
/// Multiple consumers compete to increment this
/// Using u64 to avoid ABA issues on 32-bit systems
dequeue_pos: Atomic(u64) align(std.atomic.cache_line) = Atomic(u64).init(0),
/// Padding to prevent false sharing with next field
/// Note: May be zero-length on exotic platforms (see _padding1 comment above).
_padding2: [std.atomic.cache_line - @sizeOf(Atomic(u64))]u8 = undefined,
/// Capacity mask for fast modulo (capacity - 1)
mask: usize,
/// Initialize the queue
pub fn init(allocator: Allocator) !Self {
// Compile-time overflow guard: prevent capacity configurations that would overflow
// when computing total allocation size (capacity * @sizeOf(Cell))
const max_safe_capacity = @divFloor(std.math.maxInt(usize), @sizeOf(Cell));
if (comptime capacity > max_safe_capacity) {
@compileError(std.fmt.comptimePrint(
\\DVyukovMPMCQueue OVERFLOW ERROR: Capacity too large!
\\ Requested capacity: {d}
\\ Maximum safe capacity: {d}
\\ Cell size: {d} bytes
\\
\\ capacity * @sizeOf(Cell) would overflow usize.
\\ Reduce capacity or use multiple smaller queues.
, .{ capacity, max_safe_capacity, @sizeOf(Cell) }));
}
// Use alignedAlloc to ensure buffer starts on cache-line boundary
// This prevents the first cell from potentially sharing a cache line with adjacent data
const cache_line_alignment: std.mem.Alignment = comptime blk: {
break :blk @enumFromInt(@ctz(@as(usize, std.atomic.cache_line)));
};
const buffer = try allocator.alignedAlloc(Cell, cache_line_alignment, capacity);
errdefer allocator.free(buffer);
// Runtime verification that allocation succeeded with correct alignment
std.debug.assert(@intFromPtr(buffer.ptr) % std.atomic.cache_line == 0);
// Initialize all cells with their sequence numbers
// Cell i starts with sequence i (even = available for enqueue)
for (buffer, 0..) |*cell, i| {
cell.* = .{
.sequence = Atomic(u64).init(@intCast(i)),
.data = undefined,
};
}
return Self{
.buffer = buffer,
.allocator = allocator,
.mask = capacity - 1,
};
}
/// Clean up resources
///
/// ⚠️ CRITICAL - Caller Responsibilities:
/// 1. ALL producer/consumer threads must be stopped BEFORE calling deinit()
/// 2. NO concurrent enqueue/dequeue operations can be in flight
/// 3. Queue must be drained of resource-owning items (see type T requirements above)
///
/// Violating these requirements causes:
/// - Use-after-free if threads still access the queue
/// - Resource leaks if T items own memory/handles
///
/// Safe shutdown pattern:
/// ```zig
/// // 1. Stop all producers (signal shutdown, join threads)
/// // 2. Drain queue of resource-owning items (REQUIRED for types with deinit/cleanup)
/// queue.drain(struct {
/// fn cleanup(item: Item) void { item.deinit(); }
/// }.cleanup);
/// // 3. Now safe to deinit the queue structure itself
/// queue.deinit();
/// ```
///
/// IMPORTANT: deinit() ONLY frees the queue's internal buffer. It does NOT:
/// - Call destructors on remaining items
/// - Free memory owned by items
/// - Close file handles or other resources
/// You MUST manually drain() before deinit() if T owns resources.
///
/// NOTE: This design prioritizes performance and explicitness. The separation of
/// drain() and deinit() makes resource ownership clear and avoids hidden cleanup
/// overhead in the destructor.
pub fn deinit(self: *Self) void {
// Regular free() works for alignedAlloc() - allocator tracks alignment internally
self.allocator.free(self.buffer);
}
/// Drain all items from the queue, optionally invoking a cleanup function on each
///
/// This is a convenience helper for the common pattern of draining before deinit().
/// Dequeues all items and invokes the optional callback on each item for cleanup.
///
/// ⚠️ IMPORTANT: Must be called AFTER all producer/consumer threads have stopped!
/// This is NOT thread-safe - concurrent access during drain causes undefined behavior.
///
/// Usage with resource cleanup:
/// ```zig
/// const Item = struct { data: []u8, allocator: Allocator,
/// fn deinit(self: @This()) void { self.allocator.free(self.data); }
/// };
///
/// // After stopping all threads:
/// queue.drain(struct {
/// fn cleanup(item: Item) void { item.deinit(); }
/// }.cleanup);
/// queue.deinit();
/// ```
///
/// Usage without cleanup (just drain):
/// ```zig
/// queue.drain(null); // or simply: while (queue.dequeue()) |_| {}
/// ```
///
/// Note on callback signature: The callback takes T by value (not *const T).
/// - For small T types (primitives, pointers): efficient, no overhead
/// - For large T types (>128 bytes): item is copied from dequeue() and to callback
/// - Trade-off: Matches dequeue() semantics and allows ownership for cleanup
/// - Recommendation: For large types, use DVyukovMPMCQueue(*LargeType, N) instead
pub fn drain(self: *Self, on_item: ?*const fn (T) void) void {
while (self.dequeue()) |item| {
if (on_item) |callback| {
callback(item);
}
}
}
/// Enqueue an item (lock-free, wait-free on success)
///
/// Returns error.QueueFull if the queue is full.
/// Multiple threads can call this concurrently.
///
/// Memory Ordering Note:
/// Position counters use .monotonic loads (not .acquire) because correctness
/// depends on the acquire/release pairing of Cell.sequence, NOT position ordering.
/// The sequence number acts as the synchronization point. This is a key optimization
/// in Vyukov's design - relaxed position loads reduce memory barriers without
/// sacrificing correctness.
///
/// Backoff Strategy:
/// Uses proper adaptive backoff from the Backoff library:
/// - Resets backoff when making progress (CAS gives new position)
/// - Escalates spin-only backoff when stuck (dif > 0, waiting for another producer)
/// - Never yields (other producers finish in nanoseconds, yielding adds 10-100μs latency)
/// This preserves throughput during progress while avoiding excessive spinning when stuck.
///
/// Performance Characteristics:
/// - Excellent: SPSC, imbalanced MPSC/SPMC (e.g., 4P/1C, 1P/4C)
/// - Good: Balanced low contention (2P/2C)
/// - Moderate: Balanced high contention (4P/4C, 8P/8C)
///
/// Under high balanced contention (4+ producers AND 4+ consumers), the two global
/// atomic counters (enqueue_pos, dequeue_pos) become throughput bottlenecks.
/// For such workloads, consider:
/// - Batching operations (claim N slots at once to reduce CAS frequency)
/// - Per-thread buffering before queue operations
/// - Alternative algorithms (e.g., distributed queues, work stealing)
pub fn enqueue(self: *Self, item: T) error{QueueFull}!void {
var pos = self.enqueue_pos.load(.monotonic);
while (true) {
// Index calculation: truncate position to usize then mask
const idx = @as(usize, @truncate(pos)) & self.mask;
const cell = &self.buffer[idx];
// Producer uses .monotonic (not .acquire) - synchronization happens via
// the acquire/release pair on sequence between producer-store and consumer-load
const seq = cell.sequence.load(.monotonic);
// Compute signed difference using 2's complement arithmetic for wraparound safety.
// This handles u64 counter overflow correctly because:
// - @bitCast reinterprets bits as i64 without changing representation
// - Signed subtraction gives correct distance even after wraparound
// - Example: seq=5, pos=2^64-3 => diff = 5 - (-3) = 8 (correct)
// - dif==0: cell available for enqueue
// - dif<0: queue full (enqueue lapped dequeue by full capacity)
// - dif>0: another thread owns this cell
const dif = @as(i64, @bitCast(seq)) - @as(i64, @bitCast(pos));
if (dif == 0) {
// Cell is available for enqueue
// Try to claim it with CAS
if (self.enqueue_pos.cmpxchgWeak(
pos,
pos + 1,
.monotonic,
.monotonic,
)) |new_pos| {
// CAS failed, retry with new position
pos = new_pos;
continue;
}
// We claimed the cell, write data
cell.data = item;
// Mark cell as full (sequence + 1 = odd number)
cell.sequence.store(pos + 1, .release);
return;
} else if (dif < 0) {
// Queue is full
return error.QueueFull;
} else {
// Another thread is in the process of enqueueing
// Reload position and retry
pos = self.enqueue_pos.load(.monotonic);
}
}
}
/// Dequeue an item (lock-free, wait-free on success)
///
/// Returns null if the queue is empty.
/// Multiple threads can call this concurrently.
///
/// Memory Ordering Note:
/// Consumer MUST use .acquire on sequence load to form synchronization pair with
/// producer's .release store. This ensures consumer sees producer's data write.
/// DO NOT weaken to .monotonic - this would break correctness!
pub fn dequeue(self: *Self) ?T {
var pos = self.dequeue_pos.load(.monotonic);
while (true) {
// Index calculation: truncate position to usize then mask
const idx = @as(usize, @truncate(pos)) & self.mask;
const cell = &self.buffer[idx];
// CRITICAL: Consumer uses .acquire to synchronize with producer's .release store
const seq = cell.sequence.load(.acquire);
// Compute signed difference for wraparound-safe comparison.
// We compare against (pos + 1) because dequeue expects odd sequence numbers.
// - dif==0: cell ready for dequeue (sequence is odd)
// - dif<0: queue empty (no items available)
// - dif>0: another thread owns this cell
const dif = @as(i64, @bitCast(seq)) - @as(i64, @bitCast(pos + 1));
if (dif == 0) {
// Cell contains data ready to dequeue
// Try to claim it with CAS
if (self.dequeue_pos.cmpxchgWeak(
pos,
pos + 1,
.monotonic,
.monotonic,
)) |new_pos| {
// CAS failed, retry with new position
pos = new_pos;
continue;
}
// We claimed the cell, read data
const item = cell.data;
// Mark cell as available for next round
// (pos + capacity) maintains the sequence progression
cell.sequence.store(pos + @as(u64, capacity), .release);
return item;
} else if (dif < 0) {
// Queue is empty
return null;
} else {
// Another thread is in the process of dequeueing
// Reload position and retry
pos = self.dequeue_pos.load(.monotonic);
}
}
}
/// Try to enqueue with retry limit
///
/// Unlike enqueue(), this limits CAS retries to avoid unbounded spinning
/// in high-contention scenarios.
///
/// Error Types:
/// - error.QueueFull: Queue is actually full (no available slots)
/// - error.Contended: Retry limit exhausted due to contention (queue may have space)
///
/// This distinction allows callers to implement appropriate back-pressure:
/// - QueueFull → shed load, apply backpressure, or use overflow queue
/// - Contended → retry later, use different shard, or fallback to enqueue()
///
/// Use Cases:
/// - Best-effort enqueue in latency-sensitive code (avoid unbounded spin)
/// - Rate limiting with different policies for "full" vs "contended"
/// - Load shedding that distinguishes capacity exhaustion from transient contention
///
/// Note: max_attempts counts retry attempts (both CAS failures and contention
/// reloads when dif > 0), limiting total spins to prevent unbounded retries.
pub fn tryEnqueue(self: *Self, item: T, max_attempts: usize) error{ QueueFull, Contended }!void {
// Fast path: if max_attempts is 0, immediately return QueueFull
if (max_attempts == 0) return error.QueueFull;
var pos = self.enqueue_pos.load(.monotonic);
var attempts: usize = 0;
while (attempts < max_attempts) {
// Index calculation: truncate position to usize then mask
const idx = @as(usize, @truncate(pos)) & self.mask;
const cell = &self.buffer[idx];
// Producer uses .monotonic (not .acquire) for sequence loads
const seq = cell.sequence.load(.monotonic);
// Wraparound-safe signed difference (see enqueue() for detailed explanation)
const dif = @as(i64, @bitCast(seq)) - @as(i64, @bitCast(pos));
if (dif == 0) {
// Cell is available, try to claim it
if (self.enqueue_pos.cmpxchgWeak(
pos,
pos + 1,
.monotonic,
.monotonic,
)) |new_pos| {
// CAS failed, retry with backoff
pos = new_pos;
attempts += 1;
std.atomic.spinLoopHint();
continue;
}
// Successfully claimed cell, write data
cell.data = item;
cell.sequence.store(pos + 1, .release);
return;
} else if (dif < 0) {
// Queue is full
return error.QueueFull;
} else {
// Another thread is enqueueing, reload and retry
pos = self.enqueue_pos.load(.monotonic);
attempts += 1;
std.atomic.spinLoopHint();
}
}
// Retry limit exhausted due to contention (queue may still have space)
return error.Contended;
}
/// Get approximate size (⚠️ RACY - DO NOT USE FOR FLOW CONTROL)
///
/// ⚠️ CRITICAL WARNING - CORRECTNESS:
/// This method is UNSAFE for decision-making in concurrent code!
/// - The value can be stale by the time you read it
/// - Multiple threads may observe DIFFERENT sizes simultaneously
/// - Observing size > 0 does NOT guarantee dequeue() will succeed
/// - Observing size < capacity does NOT guarantee enqueue() will succeed
///
/// ⚠️ PERFORMANCE WARNING:
/// Each call performs TWO atomic loads (.monotonic ordering).
/// Calling this in a tight loop creates cache line contention and hurts throughput.
/// Do NOT poll this function - use blocking patterns or check operation return values.
///
/// VALID USES ONLY:
/// - Debugging/logging (approximate queue fill level)
/// - Monitoring/telemetry (rough throughput estimation, NOT in hot path)
/// - Single-threaded testing
///
/// INVALID USES (will cause bugs or performance degradation):
/// - if (queue.size() > 0) { queue.dequeue() } // RACE: may be empty now
/// - if (!queue.isFull()) { queue.enqueue(x) } // RACE: may be full now
/// - while (queue.size() < N) { /* wait */ } // RACE + PERFORMANCE: destroys throughput
///
/// CORRECT PATTERN: Always check return values from enqueue/dequeue:
/// - dequeue() returns ?T (null if empty)
/// - enqueue() returns error.QueueFull if full
///
/// Uses wraparound-safe arithmetic to handle counter overflow.
pub fn size(self: *const Self) usize {
const enq = self.enqueue_pos.load(.monotonic);
const deq = self.dequeue_pos.load(.monotonic);
// Wraparound-safe subtraction: works because counters increment monotonically
// even when they overflow (e.g., enq=5, deq=max-2 after wraparound gives diff=7)
const diff = enq -% deq; // wrapping subtraction (u64)
// Clamp to capacity. diff > capacity occurs during:
// 1. Race condition: positions read at different times during concurrent ops
// 2. u64 wraparound (extremely rare: ~584 years at 1 billion ops/sec)
// In both cases, the queue is effectively full, so we return capacity.
if (diff > capacity) return capacity;
return @intCast(diff);
}
/// Check if queue is empty (⚠️ RACY - for debugging/monitoring only)
///
/// WARNING: This is a snapshot that may be stale immediately.
/// DO NOT use for flow control. Instead, check dequeue() return value.
///
/// WRONG: if (!queue.isEmpty()) { const item = queue.dequeue().? } // May panic!
/// RIGHT: if (queue.dequeue()) |item| { /* use item */ }
pub fn isEmpty(self: *const Self) bool {
return self.size() == 0;
}
/// Check if queue is full (⚠️ RACY - for debugging/monitoring only)
///
/// WARNING: This is a snapshot that may be stale immediately.
/// DO NOT use for flow control. Instead, check enqueue() error.
///
/// WRONG: if (!queue.isFull()) { try queue.enqueue(item) } // May still fail!
/// RIGHT: queue.enqueue(item) catch |err| { /* handle full */ }
pub fn isFull(self: *const Self) bool {
return self.size() >= capacity;
}
/// Get the queue capacity
pub fn getCapacity(_: *const Self) usize {
return capacity;
}
};
}
// =============================================================================
// Basic Tests (Single-threaded correctness)
// =============================================================================
test "basic enqueue and dequeue" {
const testing = std.testing;
var queue = try DVyukovMPMCQueue(u32, 8).init(testing.allocator);
defer queue.deinit();
try testing.expect(queue.isEmpty());
try testing.expectEqual(@as(usize, 8), queue.getCapacity());
// Enqueue some items
try queue.enqueue(1);
try queue.enqueue(2);
try queue.enqueue(3);
try testing.expectEqual(@as(usize, 3), queue.size());
// Dequeue them
try testing.expectEqual(@as(?u32, 1), queue.dequeue());
try testing.expectEqual(@as(?u32, 2), queue.dequeue());
try testing.expectEqual(@as(?u32, 3), queue.dequeue());
try testing.expect(queue.isEmpty());
try testing.expectEqual(@as(?u32, null), queue.dequeue());
}
test "queue full" {
const testing = std.testing;
var queue = try DVyukovMPMCQueue(u32, 4).init(testing.allocator);
defer queue.deinit();
// Fill the queue
try queue.enqueue(1);
try queue.enqueue(2);
try queue.enqueue(3);
try queue.enqueue(4);
try testing.expect(queue.isFull());
// Next enqueue should fail
try testing.expectError(error.QueueFull, queue.enqueue(5));
// Dequeue one item
try testing.expectEqual(@as(?u32, 1), queue.dequeue());
// Now we can enqueue again
try queue.enqueue(5);
try testing.expectEqual(@as(usize, 4), queue.size());
}
test "wraparound" {
const testing = std.testing;
var queue = try DVyukovMPMCQueue(usize, 4).init(testing.allocator);
defer queue.deinit();
// Cycle through the queue multiple times
var i: usize = 0;
while (i < 100) : (i += 1) {
try queue.enqueue(i);
try testing.expectEqual(@as(?usize, i), queue.dequeue());
}
try testing.expect(queue.isEmpty());
}