MayaFlux 1.0.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
PendingQueue.hpp
Go to the documentation of this file.
1#pragma once
2
3namespace MayaFlux::Memory {
4
5/**
6 * @class PendingQueue
7 * @brief Bounded multi-producer, single-consumer queue of deferred operations.
8 *
9 * Producers on any thread push an operation; the one owner of the guarded
10 * container drains the queue at a point where mutation is safe and applies each
11 * operation in the order it was claimed. Each cell carries its own sequence
12 * number, so a producer only claims a cell that is free and a consumer only
13 * reads a cell whose payload has been published. A full queue rejects the push
14 * without disturbing any other claim.
15 *
16 * Capacity must be a power of two. T must be default constructible and move
17 * assignable; the drained cell is reset so the queue never retains a payload
18 * (for example a shared_ptr) past its application.
19 *
20 * Only one thread may call drain() at a time.
21 *
22 * @code
23 * PendingQueue<Op, 1024> queue;
24 *
25 * if (!queue.push(Op { node, true })) {
26 * // full: retry after the next drain
27 * }
28 *
29 * queue.drain([&](Op&& op) { apply(std::move(op)); });
30 * @endcode
31 */
32template <typename T, size_t Capacity>
34 static_assert(Capacity >= 2 && (Capacity & (Capacity - 1)) == 0,
35 "PendingQueue capacity must be a power of two");
36 static_assert(std::is_default_constructible_v<T> && std::is_move_assignable_v<T>,
37 "PendingQueue payload must be default constructible and move assignable");
38
39public:
40 PendingQueue() noexcept
41 {
42 for (size_t i = 0; i < Capacity; ++i) {
43 m_cells[i].sequence.store(i, std::memory_order_relaxed);
44 }
45 }
46
47 PendingQueue(const PendingQueue&) = delete;
51 ~PendingQueue() = default;
52
53 /**
54 * @brief Enqueue an operation from any thread.
55 * @return false if the queue is full; the operation is left untouched.
56 */
57 [[nodiscard]] bool push(T&& value) noexcept(std::is_nothrow_move_assignable_v<T>)
58 {
59 size_t pos = m_enqueue.load(std::memory_order_relaxed);
60 Cell* cell = nullptr;
61
62 for (;;) {
63 cell = &m_cells[pos & MASK];
64 const size_t seq = cell->sequence.load(std::memory_order_acquire);
65 const auto diff = static_cast<std::intptr_t>(seq) - static_cast<std::intptr_t>(pos);
66
67 if (diff == 0) {
68 if (m_enqueue.compare_exchange_weak(pos, pos + 1, std::memory_order_relaxed)) {
69 break;
70 }
71 } else if (diff < 0) {
72 return false;
73 } else {
74 pos = m_enqueue.load(std::memory_order_relaxed);
75 }
76 }
77
78 cell->value = std::move(value);
79 cell->sequence.store(pos + 1, std::memory_order_release);
80 return true;
81 }
82
83 /**
84 * @brief Apply and remove every published operation in claim order.
85 * @param apply Callable taking T&&.
86 * @return Number of operations applied.
87 *
88 * Stops at the first cell whose producer has claimed but not yet published;
89 * that operation and everything behind it is applied by the next drain.
90 * Single consumer only.
91 */
92 template <typename Apply>
93 size_t drain(Apply&& apply)
94 {
95 size_t pos = m_dequeue.load(std::memory_order_relaxed);
96 size_t count = 0;
97
98 for (;;) {
99 Cell& cell = m_cells[pos & MASK];
100 if (cell.sequence.load(std::memory_order_acquire) != pos + 1) {
101 break;
102 }
103
104 T value = std::move(cell.value);
105 cell.value = T {};
106 cell.sequence.store(pos + Capacity, std::memory_order_release);
107 ++pos;
108 ++count;
109 m_dequeue.store(pos, std::memory_order_relaxed);
110
111 std::forward<Apply>(apply)(std::move(value));
112 }
113
114 return count;
115 }
116
117 /**
118 * @brief Cheap check for claimed operations awaiting a drain.
119 *
120 * May report true for an operation still being published; drain() then
121 * applies nothing and the operation is picked up by the next one.
122 */
123 [[nodiscard]] bool has_pending() const noexcept
124 {
125 return m_enqueue.load(std::memory_order_relaxed)
126 != m_dequeue.load(std::memory_order_relaxed);
127 }
128
129private:
130 static constexpr size_t MASK = Capacity - 1;
131
132 struct Cell {
133 std::atomic<size_t> sequence { 0 };
134 T value {};
135 };
136
137 alignas(64) std::atomic<size_t> m_enqueue { 0 };
138 alignas(64) std::atomic<size_t> m_dequeue { 0 };
139 std::array<Cell, Capacity> m_cells;
140};
141
142}
glm::vec3 value
size_t count
std::atomic< size_t > m_dequeue
PendingQueue & operator=(const PendingQueue &)=delete
bool push(T &&value) noexcept(std::is_nothrow_move_assignable_v< T >)
Enqueue an operation from any thread.
std::atomic< size_t > m_enqueue
PendingQueue(const PendingQueue &)=delete
PendingQueue & operator=(PendingQueue &&)=delete
std::array< Cell, Capacity > m_cells
static constexpr size_t MASK
PendingQueue(PendingQueue &&)=delete
bool has_pending() const noexcept
Cheap check for claimed operations awaiting a drain.
size_t drain(Apply &&apply)
Apply and remove every published operation in claim order.
Bounded multi-producer, single-consumer queue of deferred operations.