libe3 0.2.5
C++ library for E3AP RAN vendor integration
Loading...
Searching...
No Matches
mpmc_queue.hpp
Go to the documentation of this file.
1/**
2 * @file mpmc_queue.hpp
3 * @brief Lock-free bounded Multi-Producer Multi-Consumer (MPMC) ring buffer
4 *
5 * Implements Dmitry Vyukov's MPMC bounded queue algorithm. Each slot carries
6 * an atomic sequence counter that acts as a generation tag, eliminating the
7 * need for CAS on the data itself and making the queue safe for any number of
8 * concurrent producers and consumers with very low overhead.
9 *
10 * Design highlights:
11 * - Zero locks / zero condition variables in the fast path
12 * - head_ and tail_ are on separate cache lines to eliminate false sharing
13 * - Capacity is rounded up to the nearest power of two for cheap masking
14 * - Provides try_push / try_pop (non-blocking) used by LockFreeQueue
15 *
16 * SPDX-FileCopyrightText: Copyright (c) 2026 Northeastern University
17 * SPDX-License-Identifier: Apache-2.0
18 */
19
20#ifndef LIBE3_MPMC_QUEUE_HPP
21#define LIBE3_MPMC_QUEUE_HPP
22
23#include <atomic>
24#include <cstddef>
25#include <new>
26
27namespace libe3 {
28
29/** @brief Assumed cache-line size (bytes) used for alignment padding. */
30static constexpr size_t CACHE_LINE_SIZE = 64;
31
32/**
33 * @brief Lock-free bounded MPMC ring buffer.
34 *
35 * @tparam T Element type. Must be default-constructible and movable.
36 *
37 * Usage:
38 * @code
39 * MpmcQueue<MyMsg> q(256);
40 * // producer:
41 * if (!q.try_push(std::move(msg))) { // queue full }
42 * // consumer:
43 * MyMsg out;
44 * if (q.try_pop(out)) { // use out }
45 * @endcode
46 */
47template<typename T>
48class MpmcQueue {
49public:
50 /**
51 * @brief Construct a queue with at least @p min_capacity slots.
52 *
53 * The actual capacity is rounded up to the next power of two (minimum 2).
54 * @param min_capacity Minimum number of slots (0 is treated as 1).
55 */
56 explicit MpmcQueue(size_t min_capacity) {
57 // Round up to next power of two (minimum 2 to make the mask sensible)
58 capacity_ = 2;
59 while (capacity_ < min_capacity) capacity_ <<= 1;
60 mask_ = capacity_ - 1;
61
62 // Allocate cache-line aligned buffer to prevent false sharing between
63 // adjacent slots' sequence counters on small-element queues.
64 buffer_ = static_cast<Slot*>(
65 ::operator new(sizeof(Slot) * capacity_,
66 std::align_val_t{CACHE_LINE_SIZE})
67 );
68
69 for (size_t i = 0; i < capacity_; ++i) {
70 new (&buffer_[i]) Slot();
71 // Sequence == index means "slot is free for the next producer
72 // whose enqueue position is i".
73 buffer_[i].sequence.store(i, std::memory_order_relaxed);
74 }
75 }
76
78 for (size_t i = 0; i < capacity_; ++i) {
79 buffer_[i].~Slot();
80 }
81 ::operator delete(buffer_, std::align_val_t{CACHE_LINE_SIZE});
82 }
83
84 // Non-copyable, non-movable
85 MpmcQueue(const MpmcQueue&) = delete;
86 MpmcQueue& operator=(const MpmcQueue&) = delete;
87 MpmcQueue(MpmcQueue&&) = delete;
89
90 /**
91 * @brief Try to enqueue an item (non-blocking, move semantics).
92 * @return true on success; false if the queue is full.
93 */
94 bool try_push(T&& data) {
95 size_t pos = head_.load(std::memory_order_relaxed);
96 for (;;) {
97 Slot& slot = buffer_[pos & mask_];
98 size_t seq = slot.sequence.load(std::memory_order_acquire);
99 intptr_t diff = static_cast<intptr_t>(seq)
100 - static_cast<intptr_t>(pos);
101
102 if (diff == 0) {
103 // Slot is ready for this producer; claim it.
104 if (head_.compare_exchange_weak(pos, pos + 1,
105 std::memory_order_relaxed)) {
106 slot.data = std::move(data);
107 // Mark slot as containing valid data for consumer at pos+1.
108 slot.sequence.store(pos + 1, std::memory_order_release);
109 return true;
110 }
111 // Another producer claimed this slot; reload and retry.
112 } else if (diff < 0) {
113 return false; // Queue is full
114 } else {
115 // Another producer advanced head past us; reload.
116 pos = head_.load(std::memory_order_relaxed);
117 }
118 }
119 }
120
121 /**
122 * @brief Try to enqueue an item (copy overload).
123 */
124 bool try_push(const T& data) {
125 T copy = data;
126 return try_push(std::move(copy));
127 }
128
129 /**
130 * @brief Try to dequeue an item (non-blocking).
131 * @param[out] data Receives the dequeued item on success.
132 * @return true on success; false if the queue is empty.
133 */
134 bool try_pop(T& data) {
135 size_t pos = tail_.load(std::memory_order_relaxed);
136 for (;;) {
137 Slot& slot = buffer_[pos & mask_];
138 size_t seq = slot.sequence.load(std::memory_order_acquire);
139 intptr_t diff = static_cast<intptr_t>(seq)
140 - static_cast<intptr_t>(pos + 1);
141
142 if (diff == 0) {
143 // Slot holds data for this consumer; claim it.
144 if (tail_.compare_exchange_weak(pos, pos + 1,
145 std::memory_order_relaxed)) {
146 data = std::move(slot.data);
147 // Recycle slot for the producer that wraps around.
148 slot.sequence.store(pos + capacity_,
149 std::memory_order_release);
150 return true;
151 }
152 // Another consumer claimed this slot; reload and retry.
153 } else if (diff < 0) {
154 return false; // Queue is empty
155 } else {
156 // Another consumer advanced tail past us; reload.
157 pos = tail_.load(std::memory_order_relaxed);
158 }
159 }
160 }
161
162 /**
163 * @brief Approximate number of items currently in the queue.
164 *
165 * Due to concurrent modifications this value may be slightly stale,
166 * but is always in [0, capacity()].
167 */
168 size_t size_approx() const noexcept {
169 size_t h = head_.load(std::memory_order_relaxed);
170 size_t t = tail_.load(std::memory_order_relaxed);
171 return h >= t ? h - t : 0;
172 }
173
174 /** @brief Return true if the queue appears empty (approximate). */
175 bool empty_approx() const noexcept { return size_approx() == 0; }
176
177 /** @brief Actual slot capacity (always a power of two). */
178 size_t capacity() const noexcept { return capacity_; }
179
180private:
181 struct Slot {
182 std::atomic<size_t> sequence{0};
183 T data;
184 };
185
186 Slot* buffer_{nullptr};
187 size_t capacity_{0};
188 size_t mask_{0};
189
190 // Separate cache lines for head and tail to prevent producer/consumer
191 // false sharing.
192 alignas(CACHE_LINE_SIZE) std::atomic<size_t> head_{0};
193 alignas(CACHE_LINE_SIZE) std::atomic<size_t> tail_{0};
194};
195
196/**
197 * @brief Emit a CPU-level pause/yield hint inside spin-wait loops.
198 *
199 * Reduces power consumption and inter-core bus traffic while spinning.
200 * On architectures without a dedicated pause instruction this is a no-op.
201 */
202inline void cpu_relax() noexcept {
203#if defined(__x86_64__) || defined(__i386__)
204 __asm__ volatile("pause" ::: "memory");
205#elif defined(__aarch64__) || defined(__arm__)
206 __asm__ volatile("yield" ::: "memory");
207#endif
208}
209
210} // namespace libe3
211
212#endif // LIBE3_MPMC_QUEUE_HPP
MpmcQueue & operator=(MpmcQueue &&)=delete
bool empty_approx() const noexcept
Return true if the queue appears empty (approximate).
MpmcQueue & operator=(const MpmcQueue &)=delete
MpmcQueue(const MpmcQueue &)=delete
bool try_pop(T &data)
Try to dequeue an item (non-blocking).
size_t size_approx() const noexcept
Approximate number of items currently in the queue.
bool try_push(const T &data)
Try to enqueue an item (copy overload).
bool try_push(T &&data)
Try to enqueue an item (non-blocking, move semantics).
size_t capacity() const noexcept
Actual slot capacity (always a power of two).
MpmcQueue(size_t min_capacity)
Construct a queue with at least min_capacity slots.
MpmcQueue(MpmcQueue &&)=delete
void cpu_relax() noexcept
Emit a CPU-level pause/yield hint inside spin-wait loops.
static constexpr size_t CACHE_LINE_SIZE
Assumed cache-line size (bytes) used for alignment padding.