libe3 0.2.5
C++ library for E3AP RAN vendor integration
Loading...
Searching...
No Matches
lockfree_queue.hpp
Go to the documentation of this file.
1/**
2 * @file lockfree_queue.hpp
3 * @brief Lock-free bounded blocking queue for the E3AP hot paths
4 *
5 * Wraps the lock-free MPMC ring buffer (see mpmc_queue.hpp) in a small,
6 * reusable object that adds blocking pop variants and shutdown semantics.
7 * It eliminates mutex contention on the hot publish path and reduces latency
8 * jitter in sub-millisecond control loops.
9 *
10 * libe3 uses two specialisations:
11 * - LockFreeQueue<Pdu> — outbound E3AP PDUs (publisher/outbound thread
12 * consumes; many threads produce).
13 * - LockFreeQueue<DAppReport> — inbound dApp reports handed from the RAN
14 * inbound thread to the report worker thread.
15 *
16 * SPDX-FileCopyrightText: Copyright (c) 2026 Northeastern University
17 * SPDX-License-Identifier: Apache-2.0
18 */
19
20#ifndef LIBE3_LOCKFREE_QUEUE_HPP
21#define LIBE3_LOCKFREE_QUEUE_HPP
22
23#include "types.hpp"
24#include "mpmc_queue.hpp"
25#include "logger.hpp"
26#include <atomic>
27#include <optional>
28#include <chrono>
29#include <thread>
30#include <mutex>
31#include <condition_variable>
32
33namespace libe3 {
34
35/**
36 * @brief Lock-free bounded blocking queue.
37 *
38 * Built on an MPMC lock-free ring buffer. Blocking pop variants use an
39 * adaptive wait strategy:
40 * 1. Spin with CPU pause hints (lowest latency, nanoseconds)
41 * 2. Thread yield (cooperative, microseconds)
42 * 3. Block on a condition variable, woken by push()/shutdown() (idle wait,
43 * avoids busy-loop when quiet; wake latency instead of a poll period)
44 *
45 * @tparam T Element type. Must be default-constructible and movable.
46 */
47template<typename T>
49public:
50 /**
51 * @brief Construct a LockFreeQueue.
52 * @param capacity Minimum ring buffer capacity (rounded up to next power
53 * of two, default 128).
54 */
55 explicit LockFreeQueue(size_t capacity = 128)
56 : ring_(capacity)
57 {
58 E3_LOG_DEBUG(LOG_TAG) << "Lock-free queue created, capacity="
59 << ring_.capacity();
60 }
61
62 /**
63 * @brief Destructor – signals shutdown so any blocked pop() returns.
64 */
66 shutdown();
67 E3_LOG_DEBUG(LOG_TAG) << "Lock-free queue destroyed";
68 }
69
70 // Non-copyable, non-movable
71 LockFreeQueue(const LockFreeQueue&) = delete;
75
76 /**
77 * @brief Enqueue an item (non-blocking, lock-free).
78 * @return ErrorCode::SUCCESS on success.
79 * @return ErrorCode::BUFFER_TOO_SMALL if the ring buffer is full.
80 * @return ErrorCode::NOT_INITIALIZED if shutdown() has been called.
81 */
82 ErrorCode push(T item) {
83 if (shutdown_.load(std::memory_order_relaxed)) {
84 return ErrorCode::NOT_INITIALIZED;
85 }
86
87 if (!ring_.try_push(std::move(item))) {
88 E3_LOG_WARN(LOG_TAG) << "Queue full, dropping message";
89 return ErrorCode::BUFFER_TOO_SMALL;
90 }
91
92 // Wake a consumer parked in the Phase 3 condition-variable wait (see
93 // pop()). Taking wait_mutex_ here (even with an empty critical
94 // section) is required, not just style: it is what prevents the
95 // classic missed-wakeup race against a consumer that is between its
96 // try_pop() check and its cv.wait() call. See the class-level
97 // comment on pop() for the full argument.
98 {
99 std::lock_guard<std::mutex> lock(wait_mutex_);
100 }
101 wait_cv_.notify_one();
102
103 E3_LOG_TRACE(LOG_TAG) << "Pushed item";
104 return ErrorCode::SUCCESS;
105 }
106
107 /**
108 * @brief Dequeue an item, blocking indefinitely until one is available.
109 *
110 * Returns a default-constructed T{} if shutdown() is called while waiting.
111 */
112 T pop() {
113 T item;
114
115 // Phase 1: CPU-pause spin (nanosecond latency when producer is fast)
116 for (size_t i = 0; i < SPIN_COUNT; ++i) {
117 if (ring_.try_pop(item)) return item;
118 if (shutdown_.load(std::memory_order_relaxed)) return T{};
119 cpu_relax();
120 }
121
122 // Phase 2: Cooperative yield (microsecond range)
123 for (size_t i = 0; i < YIELD_COUNT; ++i) {
124 if (ring_.try_pop(item)) return item;
125 if (shutdown_.load(std::memory_order_relaxed)) return T{};
126 std::this_thread::yield();
127 }
128
129 // Phase 3: Block on the condition variable until data arrives or
130 // shutdown is signalled. wait_mutex_ is held across the check and
131 // the wait() call (not just during the wait itself) so that a
132 // concurrent push()'s notify (which also takes wait_mutex_, see
133 // push()) can never land in the gap between "we checked and it was
134 // empty" and "we're registered as a waiter" -- that gap is exactly
135 // where a naive condvar/futex notify-on-push design loses wakeups.
136 std::unique_lock<std::mutex> lock(wait_mutex_);
137 while (true) {
138 if (ring_.try_pop(item)) return item;
139 if (shutdown_.load(std::memory_order_relaxed)) return T{};
140 wait_cv_.wait(lock);
141 }
142 }
143
144 /**
145 * @brief Dequeue an item with a maximum wait duration.
146 * @return The item on success; std::nullopt on timeout or shutdown.
147 */
148 std::optional<T> pop(std::chrono::milliseconds timeout) {
149 T item;
150 auto deadline = std::chrono::steady_clock::now() + timeout;
151
152 // Phase 1: CPU-pause spin
153 for (size_t i = 0; i < SPIN_COUNT; ++i) {
154 if (ring_.try_pop(item)) return item;
155 if (shutdown_.load(std::memory_order_relaxed)) return std::nullopt;
156 cpu_relax();
157 }
158
159 // Phase 2: Cooperative yield
160 for (size_t i = 0; i < YIELD_COUNT; ++i) {
161 if (ring_.try_pop(item)) return item;
162 if (shutdown_.load(std::memory_order_relaxed)) return std::nullopt;
163 std::this_thread::yield();
164 }
165
166 // Phase 3: Block on the condition variable until data arrives,
167 // shutdown is signalled, or the deadline passes. See the infinite
168 // pop() overload above for why wait_mutex_ must be held across the
169 // check-then-wait sequence.
170 {
171 std::unique_lock<std::mutex> lock(wait_mutex_);
172 while (std::chrono::steady_clock::now() < deadline) {
173 if (ring_.try_pop(item)) return item;
174 if (shutdown_.load(std::memory_order_relaxed)) return std::nullopt;
175 wait_cv_.wait_until(lock, deadline);
176 }
177 }
178
179 // One last attempt after deadline
180 if (ring_.try_pop(item)) return item;
181 return std::nullopt;
182 }
183
184 /**
185 * @brief Try to dequeue without blocking.
186 * @return The item if one was available; std::nullopt otherwise.
187 */
188 std::optional<T> try_pop() {
189 T item;
190 if (ring_.try_pop(item)) return item;
191 return std::nullopt;
192 }
193
194 /** @brief Return true if the queue appears empty (approximate). */
195 bool empty() const { return ring_.empty_approx(); }
196
197 /** @brief Return the approximate number of items in the queue. */
198 size_t size() const { return ring_.size_approx(); }
199
200 /** @brief Return the ring buffer capacity (always a power of two). */
201 size_t capacity() const noexcept { return ring_.capacity(); }
202
203 /** @brief Discard all items currently in the queue. */
204 void clear() {
205 T item;
206 while (ring_.try_pop(item)) {}
207 E3_LOG_DEBUG(LOG_TAG) << "Queue cleared";
208 }
209
210 /**
211 * @brief Signal shutdown so that all blocked pop() calls return promptly.
212 */
213 void shutdown() {
214 shutdown_.store(true, std::memory_order_relaxed);
215 // Wake any consumer blocked in pop()'s Phase 3 wait immediately,
216 // rather than letting it wait out its (possibly infinite) wait.
217 {
218 std::lock_guard<std::mutex> lock(wait_mutex_);
219 }
220 wait_cv_.notify_all();
221 E3_LOG_DEBUG(LOG_TAG) << "Queue shutdown signalled";
222 }
223
224 /** @brief Return true if shutdown() has been called. */
225 bool is_shutdown() const {
226 return shutdown_.load(std::memory_order_relaxed);
227 }
228
229 /**
230 * @brief Clear shutdown and discard any residual items so the queue is
231 * reusable after a shutdown(). The queue object itself is not moved, so any
232 * reference/pointer held by a consumer stays valid across a re-arm.
233 */
234 void rearm() {
235 clear();
236 shutdown_.store(false, std::memory_order_relaxed);
237 E3_LOG_DEBUG(LOG_TAG) << "Queue re-armed";
238 }
239
240private:
241 static constexpr const char* LOG_TAG = "Queue";
242
243 MpmcQueue<T> ring_;
244 std::atomic<bool> shutdown_{false};
245
246 // Phase 3 wait/notify primitives (see pop() and push()).
247 std::mutex wait_mutex_;
248 std::condition_variable wait_cv_;
249
250 // Adaptive spin-wait tuning constants
251 static constexpr size_t SPIN_COUNT = 40; ///< CPU-pause iterations
252 static constexpr size_t YIELD_COUNT = 100; ///< thread-yield iterations
253};
254
255} // namespace libe3
256
257#endif // LIBE3_LOCKFREE_QUEUE_HPP
ErrorCode push(T item)
Enqueue an item (non-blocking, lock-free).
LockFreeQueue & operator=(const LockFreeQueue &)=delete
std::optional< T > try_pop()
Try to dequeue without blocking.
std::optional< T > pop(std::chrono::milliseconds timeout)
Dequeue an item with a maximum wait duration.
LockFreeQueue(LockFreeQueue &&)=delete
LockFreeQueue(size_t capacity=128)
Construct a LockFreeQueue.
size_t size() const
Return the approximate number of items in the queue.
bool empty() const
Return true if the queue appears empty (approximate).
void shutdown()
Signal shutdown so that all blocked pop() calls return promptly.
void clear()
Discard all items currently in the queue.
size_t capacity() const noexcept
Return the ring buffer capacity (always a power of two).
~LockFreeQueue()
Destructor – signals shutdown so any blocked pop() returns.
T pop()
Dequeue an item, blocking indefinitely until one is available.
void rearm()
Clear shutdown and discard any residual items so the queue is reusable after a shutdown()....
LockFreeQueue & operator=(LockFreeQueue &&)=delete
bool is_shutdown() const
Return true if shutdown() has been called.
LockFreeQueue(const LockFreeQueue &)=delete
Lock-free bounded MPMC ring buffer.
Logging facility for libe3.
#define E3_LOG_TRACE(component)
Definition logger.hpp:229
#define E3_LOG_WARN(component)
Definition logger.hpp:226
#define E3_LOG_DEBUG(component)
Definition logger.hpp:228
Lock-free bounded Multi-Producer Multi-Consumer (MPMC) ring buffer.
ErrorCode
Error codes returned by libe3 operations Use shared error-code list (see libe3/error_codes....
Definition types.hpp:141
void cpu_relax() noexcept
Emit a CPU-level pause/yield hint inside spin-wait loops.
E3AP Protocol Types - Vendor-neutral type definitions.