MayaFlux 1.0.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
SeqLock.hpp
Go to the documentation of this file.
1#pragma once
2
3namespace MayaFlux::Memory {
4
5/**
6 * @class Seqlock
7 * @brief Sequence lock for fixed-size data regions.
8 *
9 * Protects a data region that is written infrequently and read frequently.
10 * Readers never block writers; a reader that observes a write in progress
11 * simply retries. begin_write() requires externally serialized writers;
12 * begin_serialized_write() claims the writer sequence when callers may overlap.
13 *
14 * Sequence counter convention:
15 * even — data is stable, safe to read
16 * odd — write in progress, readers must retry
17 *
18 * Usage — writer side:
19 * @code
20 * SeqlockWriteGuard guard(lock);
21 * std::memcpy(dest, src, bytes);
22 * @endcode
23 *
24 * Usage — reader side (unbounded):
25 * @code
26 * uint32_t snap;
27 * do {
28 * snap = lock.read_begin();
29 * // copy or sample the guarded data
30 * } while (lock.read_retry(snap));
31 * @endcode
32 *
33 * Usage — reader side (bounded, preferred in frame loops):
34 * @code
35 * auto result = seqlock_read(lock, max_attempts, [&]{ return read_data(); });
36 * if (!result) {
37 // stale read accepted or skip frame
38 }
39 * @endcode
40 *
41 * Alignment to a cache line prevents false sharing when multiple Seqlock
42 * instances are stored in a contiguous array (e.g. one per texture layer).
43 */
44class alignas(64) Seqlock {
45public:
46 Seqlock() noexcept = default;
47
48 Seqlock(const Seqlock&) = delete;
49 Seqlock& operator=(const Seqlock&) = delete;
50 Seqlock(Seqlock&&) = delete;
51 Seqlock& operator=(Seqlock&&) = delete;
52 ~Seqlock() = default;
53
54 /**
55 * @brief Begin a write: advances the counter to an odd value.
56 *
57 * Must be paired with end_write(). Prefer SeqlockWriteGuard.
58 */
59 void begin_write() noexcept
60 {
61 m_seq.fetch_add(1U, std::memory_order_release);
62 }
63
64 /**
65 * @brief Claim the writer sequence after any active writer completes.
66 *
67 * Spins while the sequence is odd, then atomically advances it to odd.
68 * Pair with end_write(), or use SerializedSeqlockWriteGuard. All writers
69 * using this path must hold the claim for their entire mutation.
70 */
71 void begin_serialized_write() noexcept
72 {
73 uint32_t expected = m_seq.load(std::memory_order_relaxed);
74 for (;;) {
75 if (!(expected & 1U)
76 && m_seq.compare_exchange_weak(expected, expected + 1U,
77 std::memory_order_acquire, std::memory_order_relaxed))
78 return;
79 expected = m_seq.load(std::memory_order_relaxed);
80 }
81 }
82
83 /**
84 * @brief End a write: advances the counter back to an even value.
85 *
86 * All stores to the guarded region must be visible before this returns.
87 */
88 void end_write() noexcept
89 {
90 m_seq.fetch_add(1U, std::memory_order_release);
91 }
92
93 /**
94 * @brief Begin a read: spin until the counter is even, return the snapshot.
95 *
96 * The caller reads the guarded data after this returns, then calls
97 * read_retry() to verify no write overlapped.
98 *
99 * @return Even sequence snapshot to pass to read_retry().
100 */
101 [[nodiscard]] uint32_t read_begin() const noexcept
102 {
103 uint32_t v {};
104 do {
105 v = m_seq.load(std::memory_order_acquire);
106 } while (v & 1U);
107 return v;
108 }
109
110 /**
111 * @brief Check whether a write overlapped the read just completed.
112 *
113 * @param snapshot Value returned by the matching read_begin() call.
114 * @return true if the caller must retry the read, false if data is valid.
115 */
116 [[nodiscard]] bool read_retry(uint32_t snapshot) const noexcept
117 {
118 std::atomic_thread_fence(std::memory_order_acquire);
119 return m_seq.load(std::memory_order_relaxed) != snapshot;
120 }
121
122 /**
123 * @brief Return the raw sequence counter value.
124 *
125 * Primarily useful for diagnostics and assertions. Do not use for
126 * synchronization logic; use read_begin() / read_retry() instead.
127 */
128 [[nodiscard]] uint32_t sequence() const noexcept
129 {
130 return m_seq.load(std::memory_order_relaxed);
131 }
132
133private:
134 std::atomic<uint32_t> m_seq { 0U };
135};
136
137// =============================================================================
138// RAII write guard
139// =============================================================================
140
141/**
142 * @class SeqlockWriteGuard
143 * @brief RAII guard that brackets a Seqlock write region.
144 *
145 * Calls begin_write() on construction and end_write() on destruction.
146 * Non-copyable, non-movable: the guard is tied to a single lexical scope.
147 *
148 * @code
149 * {
150 * SeqlockWriteGuard guard(m_layer_locks[layer]);
151 * std::memcpy(dest, src, byte_count);
152 * }
153 * @endcode
154 */
156public:
157 explicit SeqlockWriteGuard(Seqlock& lock) noexcept
158 : m_lock(lock)
159 {
161 }
162
164 {
166 }
167
172
173private:
175};
176
177/**
178 * @brief Scope guard for a serialized Seqlock write.
179 *
180 * Waits for another writer to finish, claims the odd sequence for this scope,
181 * and restores an even sequence on destruction. Readers retry as with
182 * SeqlockWriteGuard. Use when multiple callers can write the guarded state.
183 */
203
204// =============================================================================
205// Bounded reader helper
206// =============================================================================
207
208/**
209 * @brief Invoke a read functor under a Seqlock with a bounded retry count.
210 *
211 * Retries the read up to @p max_attempts times if a concurrent write is
212 * detected. On each attempt the functor is called with no arguments; its
213 * return value is forwarded on success.
214 *
215 * Returns std::nullopt if @p max_attempts is exhausted without a clean
216 * read. Callers in frame loops should treat nullopt as "use previous frame"
217 * rather than blocking.
218 *
219 * The functor must be callable with no arguments and must return a value
220 * (or void — see seqlock_read_void for void functors).
221 *
222 * @tparam Fn Callable type, must satisfy std::invocable<Fn>.
223 * @param lock The Seqlock guarding the data region.
224 * @param max_attempts Maximum number of read attempts before giving up.
225 * 0 means unbounded (spin until clean).
226 * @param fn Functor that reads and returns the guarded data.
227 * @return std::optional<return type of fn>, empty on exhaustion.
228 *
229 * @code
230 * auto pixels = seqlock_read(m_layer_locks[layer], 8, [&] {
231 * return std::span<const uint8_t>(buf.data(), buf.size());
232 * });
233 * if (pixels) upload_to_gpu(*pixels);
234 * @endcode
235 */
236template <std::invocable Fn>
237[[nodiscard]] auto seqlock_read(
238 const Seqlock& lock,
239 uint32_t max_attempts,
240 Fn&& fn) -> std::optional<std::invoke_result_t<Fn>>
241{
242 uint32_t attempts = 0;
243 while (max_attempts == 0 || attempts < max_attempts) {
244 uint32_t snap = lock.read_begin();
245 auto result = std::invoke(std::forward<Fn>(fn));
246 if (!lock.read_retry(snap))
247 return result;
248 ++attempts;
249 }
250 return std::nullopt;
251}
252
253/**
254 * @brief Invoke a void read functor under a Seqlock with a bounded retry count.
255 *
256 * Same semantics as seqlock_read but for functors with no return value.
257 * Returns true if a clean read was achieved within @p max_attempts,
258 * false if the limit was exhausted.
259 *
260 * @tparam Fn Callable type with void return, must satisfy std::invocable<Fn>.
261 * @param lock The Seqlock guarding the data region.
262 * @param max_attempts Maximum attempts; 0 = unbounded.
263 * @param fn Functor that reads the guarded region (side-effect only).
264 * @return true on clean read, false on exhaustion.
265 */
266template <std::invocable Fn>
267 requires std::is_void_v<std::invoke_result_t<Fn>>
269 const Seqlock& lock,
270 uint32_t max_attempts,
271 Fn&& fn)
272{
273 uint32_t attempts = 0;
274 while (max_attempts == 0 || attempts < max_attempts) {
275 uint32_t snap = lock.read_begin();
276 std::invoke(std::forward<Fn>(fn));
277 if (!lock.read_retry(snap))
278 return true;
279 ++attempts;
280 }
281 return false;
282}
283
284// =============================================================================
285// SeqlockArray
286// =============================================================================
287
288/**
289 * @class SeqlockArray
290 * @brief Indexed collection of independent, equal-ranked Seqlock instances.
291 *
292 * Manages N independently lockable Seqlock slots with no privileged index.
293 * Suitable wherever a container holds N equal data slots (layers, channels,
294 * ring positions) that may be written and read independently and concurrently.
295 *
296 * Slots are heap-allocated individually via unique_ptr to satisfy alignas(64)
297 * without violating the non-copyable / non-movable contract of Seqlock.
298 *
299 * resize() is not safe to call concurrently with any read or write operation
300 * on this instance.
301 *
302 * @code
303 * SeqlockArray locks(num_slots);
304 *
305 * // writer
306 * {
307 * SeqlockWriteGuard g(locks[slot]);
308 * std::ranges::copy(src, dst.begin());
309 * }
310 *
311 * // reader
312 * auto result = seqlock_read(locks[slot], 8, [&]{ return read_slot(slot); });
313 * @endcode
314 */
316public:
317 SeqlockArray() = default;
318
319 explicit SeqlockArray(size_t n)
320 {
321 resize(n);
322 }
323
324 SeqlockArray(const SeqlockArray&) = delete;
328
329 ~SeqlockArray() = default;
330
331 /**
332 * @brief Replace the slot array with n default-constructed Seqlock instances.
333 *
334 * Not concurrent-safe. Call only during initialisation or reconfiguration
335 * when no readers or writers are active.
336 *
337 * @param n New slot count.
338 */
339 void resize(size_t n)
340 {
341 m_slots.clear();
342 m_slots.reserve(n);
343 for (size_t i = 0; i < n; ++i)
344 m_slots.emplace_back(std::make_unique<Seqlock>());
345 }
346
347 /**
348 * @brief Add @p n default-constructed Seqlock instances after the
349 * existing slots.
350 *
351 * Existing slots keep their address and state. Not concurrent-safe, like
352 * resize().
353 *
354 * @param n Number of slots to add.
355 */
356 void extend(size_t n)
357 {
358 m_slots.reserve(m_slots.size() + n);
359 for (size_t i = 0; i < n; ++i)
360 m_slots.emplace_back(std::make_unique<Seqlock>());
361 }
362
363 /**
364 * @brief Number of slots.
365 */
366 [[nodiscard]] size_t size() const noexcept { return m_slots.size(); }
367
368 /**
369 * @brief Access the Seqlock at index @p i.
370 * @pre i < size()
371 */
372 [[nodiscard]] Seqlock& operator[](size_t i) noexcept { return *m_slots[i]; }
373
374 /**
375 * @brief Access the Seqlock at index @p i (const overload).
376 * @pre i < size()
377 */
378 [[nodiscard]] const Seqlock& operator[](size_t i) const noexcept { return *m_slots[i]; }
379
380private:
381 std::vector<std::unique_ptr<Seqlock>> m_slots;
382};
383
384} // namespace MayaFlux::Memory
const Seqlock & operator[](size_t i) const noexcept
Access the Seqlock at index i (const overload).
Definition SeqLock.hpp:378
SeqlockArray & operator=(SeqlockArray &&)=delete
SeqlockArray(SeqlockArray &&)=delete
void resize(size_t n)
Replace the slot array with n default-constructed Seqlock instances.
Definition SeqLock.hpp:339
SeqlockArray & operator=(const SeqlockArray &)=delete
size_t size() const noexcept
Number of slots.
Definition SeqLock.hpp:366
Seqlock & operator[](size_t i) noexcept
Access the Seqlock at index i.
Definition SeqLock.hpp:372
std::vector< std::unique_ptr< Seqlock > > m_slots
Definition SeqLock.hpp:381
SeqlockArray(const SeqlockArray &)=delete
void extend(size_t n)
Add n default-constructed Seqlock instances after the existing slots.
Definition SeqLock.hpp:356
Indexed collection of independent, equal-ranked Seqlock instances.
Definition SeqLock.hpp:315
SeqlockWriteGuard(SeqlockWriteGuard &&)=delete
SeqlockWriteGuard(const SeqlockWriteGuard &)=delete
SeqlockWriteGuard & operator=(SeqlockWriteGuard &&)=delete
SeqlockWriteGuard & operator=(const SeqlockWriteGuard &)=delete
SeqlockWriteGuard(Seqlock &lock) noexcept
Definition SeqLock.hpp:157
RAII guard that brackets a Seqlock write region.
Definition SeqLock.hpp:155
void end_write() noexcept
End a write: advances the counter back to an even value.
Definition SeqLock.hpp:88
uint32_t read_begin() const noexcept
Begin a read: spin until the counter is even, return the snapshot.
Definition SeqLock.hpp:101
uint32_t sequence() const noexcept
Return the raw sequence counter value.
Definition SeqLock.hpp:128
void begin_serialized_write() noexcept
Claim the writer sequence after any active writer completes.
Definition SeqLock.hpp:71
bool read_retry(uint32_t snapshot) const noexcept
Check whether a write overlapped the read just completed.
Definition SeqLock.hpp:116
std::atomic< uint32_t > m_seq
Definition SeqLock.hpp:134
void begin_write() noexcept
Begin a write: advances the counter to an odd value.
Definition SeqLock.hpp:59
Seqlock() noexcept=default
Sequence lock for fixed-size data regions.
Definition SeqLock.hpp:44
SerializedSeqlockWriteGuard & operator=(const SerializedSeqlockWriteGuard &)=delete
SerializedSeqlockWriteGuard(Seqlock &lock) noexcept
Definition SeqLock.hpp:186
SerializedSeqlockWriteGuard(const SerializedSeqlockWriteGuard &)=delete
Scope guard for a serialized Seqlock write.
Definition SeqLock.hpp:184
auto seqlock_read(const Seqlock &lock, uint32_t max_attempts, Fn &&fn) -> std::optional< std::invoke_result_t< Fn > >
Invoke a read functor under a Seqlock with a bounded retry count.
Definition SeqLock.hpp:237
bool seqlock_read_void(const Seqlock &lock, uint32_t max_attempts, Fn &&fn)
Invoke a void read functor under a Seqlock with a bounded retry count.
Definition SeqLock.hpp:268