MayaFlux 1.0.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
SweepList.hpp
Go to the documentation of this file.
1#pragma once
2
3namespace MayaFlux::Memory {
4
5/**
6 * @class SweepList
7 * @brief Lock-free, unbounded, multi-producer collection of in-flight items.
8 *
9 * Holds items whose readiness is checked out of order and reclaimed out of
10 * order, such as `std::future<T>` handles for asynchronous work. Unlike
11 * `RingBuffer`'s lock-free policies, which require a fixed power-of-two
12 * capacity, SweepList grows without bound: every push() allocates one link,
13 * and every reclaimed item frees its link. There is no capacity to exhaust
14 * and no size policy to choose.
15 *
16 * The concurrency shape is "any thread may push, any thread may sweep."
17 * There is no reserved single-consumer role: sweep() and drain() both take
18 * the entire current chain atomically via a single exchange(), so once a
19 * thread holds that chain no other thread can observe or mutate any link in
20 * it. This sidesteps the classic hazard of lock-free list removal, where
21 * unlinking one link out of the middle of a shared list races against
22 * concurrent traversal, by never unlinking a single link. A sweep always
23 * takes everything, decides per item, and republishes the survivors as one
24 * new chain.
25 *
26 * push() is wait-free under the same CAS-retry bound as any Treiber stack.
27 * sweep() and drain() are lock-free: no thread blocks on another thread's
28 * mutex, though a CAS loop may retry under contention from concurrent
29 * pushes.
30 *
31 * Ownership of each link is a std::unique_ptr everywhere except the single
32 * word actually under CAS. That word, m_head, is necessarily a raw pointer:
33 * no atomic smart pointer gives genuine lock-free compare-exchange. The
34 * standard library's own std::atomic<std::shared_ptr<T>> is typically
35 * implemented with an internal lock on every major implementation, since a
36 * shared_ptr CAS would need to update an object pointer and a control-block
37 * pointer as one indivisible step, which no hardware CAS instruction does.
38 * Using it here would silently reintroduce a lock behind an API that no
39 * longer looks like one, defeating the reason this type exists. m_head is
40 * therefore raw for the same reason LiveArena's backing storage is raw: a
41 * narrowly scoped exception at the exact point the type system cannot help,
42 * never exposed past this class's own implementation.
43 *
44 * approx_size() is named approx deliberately. Concurrent pushes and sweeps
45 * can make it transiently stale, the same caveat RingBuffer's MPSCPolicy
46 * documents for its own size().
47 *
48 * @tparam T Element type. Move-only types such as std::future<T> are fully
49 * supported; SweepList never copies an element.
50 *
51 * @code{.cpp}
52 * Memory::SweepList<std::future<bool>> tasks;
53 *
54 * tasks.push(std::async(std::launch::async, [] { return do_work(); }));
55 *
56 * // Opportunistic, non-blocking prune of completed work.
57 * tasks.sweep(
58 * [](std::future<bool>& f) {
59 * return f.wait_for(std::chrono::seconds(0)) == std::future_status::ready;
60 * },
61 * [](std::future<bool>& f) { (void)f.get(); });
62 *
63 * // Blocking drain of everything still in flight.
64 * tasks.drain([](std::future<bool>& f) { (void)f.get(); });
65 * @endcode
66 */
67template <typename T>
68class SweepList {
69public:
70 SweepList() noexcept = default;
71
72 SweepList(const SweepList&) = delete;
73 SweepList& operator=(const SweepList&) = delete;
74 SweepList(SweepList&&) = delete;
75 SweepList& operator=(SweepList&&) = delete;
76
77 /**
78 * @brief Drains and discards any remaining items without inspecting them.
79 *
80 * Callers that need every item finalized, exceptions observed, results
81 * consumed, must call drain() with a finish functor before destruction.
82 * The destructor exists only to free links a caller genuinely abandoned.
83 */
85 {
86 drain([](T&) noexcept { });
87 }
88
89 /**
90 * @brief Publishes @p value for later sweep() or drain().
91 *
92 * Safe to call from any number of concurrent threads. Allocates one
93 * link; the link is freed when the item is reclaimed by sweep() or
94 * drain().
95 *
96 * @param value Item to retain. Moved into internal storage.
97 */
98 void push(T value) noexcept
99 {
100 std::unique_ptr<Link> owned = std::make_unique<Link>(std::move(value), nullptr);
101
102 // The atomic word alone decides ownership from here: once the CAS
103 // below succeeds, m_head is the link's only owner. release() marks
104 // that handoff explicitly rather than leaving a raw pointer implied.
105 Link* node = owned.release();
106
107 node->next = m_head.load(std::memory_order_relaxed);
108 while (!m_head.compare_exchange_weak(
109 node->next, node,
110 std::memory_order_release, std::memory_order_relaxed)) {
111 }
112
113 m_count.fetch_add(1, std::memory_order_relaxed);
114 }
115
116 /**
117 * @brief Non-blocking pass over every currently held item.
118 *
119 * Atomically takes the entire chain, then for each item calls
120 * @p ready(item). If it returns true, calls @p finish(item) and frees
121 * the link. If false, the item is republished so a later sweep() or
122 * drain() will see it again.
123 *
124 * Items pushed by other threads while this sweep is running are not
125 * part of the taken chain and are left untouched; they surface on a
126 * later sweep().
127 *
128 * @tparam Pred Callable (T&) -> bool. True means the item is done
129 * and safe to finalize now.
130 * @tparam Finish Callable (T&) -> void. Invoked exactly once per item
131 * for which @p ready returned true, before the link is
132 * freed.
133 * @param ready Readiness check.
134 * @param finish Finalizer, invoked once per reclaimed item.
135 */
136 template <typename Pred, typename Finish>
137 requires std::invocable<Pred, T&> && std::invocable<Finish, T&>
138 void sweep(Pred ready, Finish finish)
139 {
140 Link* chain = m_head.exchange(nullptr, std::memory_order_acquire);
141
142 Link* survivors = nullptr;
143 size_t reclaimed = 0;
144
145 while (chain) {
146 Link* next = chain->next;
147
148 if (std::invoke(ready, chain->value)) {
149 std::unique_ptr<Link> owned(chain);
150 std::invoke(finish, owned->value);
151 ++reclaimed;
152 } else {
153 chain->next = survivors;
154 survivors = chain;
155 }
156
157 chain = next;
158 }
159
160 if (survivors) {
161 reattach(survivors);
162 }
163
164 m_count.fetch_sub(reclaimed, std::memory_order_relaxed);
165 }
166
167 /**
168 * @brief Blocking pass that finalizes every item currently held.
169 *
170 * Atomically takes the entire chain and calls @p finish(item) on every
171 * one of them unconditionally, freeing each link as it goes. Intended
172 * for teardown paths, destructors, and explicit "wait for everything"
173 * calls, where @p finish itself performs the blocking wait (for example
174 * `future.get()`).
175 *
176 * Items pushed by other threads after the chain is taken are not
177 * included and remain for a later call.
178 *
179 * @tparam Finish Callable (T&) -> void.
180 * @param finish Finalizer, invoked once per item, in unspecified order.
181 */
182 template <typename Finish>
183 requires std::invocable<Finish, T&>
184 void drain(Finish finish)
185 {
186 Link* chain = m_head.exchange(nullptr, std::memory_order_acquire);
187
188 while (chain) {
189 Link* next = chain->next;
190 std::unique_ptr<Link> owned(chain);
191 std::invoke(finish, owned->value);
192 chain = next;
193 }
194
195 m_count.store(0, std::memory_order_relaxed);
196 }
197
198 /**
199 * @brief Approximate number of items currently held.
200 *
201 * May be transiently stale under concurrent push()/sweep()/drain().
202 * Use for diagnostics, not for synchronization decisions.
203 */
204 [[nodiscard]] size_t approx_size() const noexcept
205 {
206 return m_count.load(std::memory_order_relaxed);
207 }
208
209 /**
210 * @brief Whether the list currently holds no items.
211 *
212 * Same staleness caveat as approx_size().
213 */
214 [[nodiscard]] bool empty() const noexcept
215 {
216 return m_head.load(std::memory_order_acquire) == nullptr;
217 }
218
219private:
220 struct Link {
223 };
224
225 void reattach(Link* survivors) noexcept
226 {
227 Link* tail = survivors;
228 while (tail->next)
229 tail = tail->next;
230
231 tail->next = m_head.load(std::memory_order_relaxed);
232 while (!m_head.compare_exchange_weak(
233 tail->next, survivors,
234 std::memory_order_release, std::memory_order_relaxed)) {
235 }
236 }
237
238 alignas(64) std::atomic<Link*> m_head { nullptr };
239 alignas(64) std::atomic<size_t> m_count { 0 };
240};
241
242} // namespace MayaFlux::Memory
BufferProcessingChain * chain
glm::vec3 value
void reattach(Link *survivors) noexcept
void push(T value) noexcept
Publishes value for later sweep() or drain().
Definition SweepList.hpp:98
void drain(Finish finish)
Blocking pass that finalizes every item currently held.
bool empty() const noexcept
Whether the list currently holds no items.
void sweep(Pred ready, Finish finish)
Non-blocking pass over every currently held item.
SweepList() noexcept=default
std::atomic< Link * > m_head
std::atomic< size_t > m_count
size_t approx_size() const noexcept
Approximate number of items currently held.
Lock-free, unbounded, multi-producer collection of in-flight items.
Definition SweepList.hpp:68