MayaFlux 0.5.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 Single-writer multiple-reader 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. Writers are serialized externally (e.g. by the container's
12 * existing state machine or a higher-level mutex on the write path only).
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 End a write: advances the counter back to an even value.
66 *
67 * All stores to the guarded region must be visible before this returns.
68 */
69 void end_write() noexcept
70 {
71 m_seq.fetch_add(1U, std::memory_order_release);
72 }
73
74 /**
75 * @brief Begin a read: spin until the counter is even, return the snapshot.
76 *
77 * The caller reads the guarded data after this returns, then calls
78 * read_retry() to verify no write overlapped.
79 *
80 * @return Even sequence snapshot to pass to read_retry().
81 */
82 [[nodiscard]] uint32_t read_begin() const noexcept
83 {
84 uint32_t v {};
85 do {
86 v = m_seq.load(std::memory_order_acquire);
87 } while (v & 1U);
88 return v;
89 }
90
91 /**
92 * @brief Check whether a write overlapped the read just completed.
93 *
94 * @param snapshot Value returned by the matching read_begin() call.
95 * @return true if the caller must retry the read, false if data is valid.
96 */
97 [[nodiscard]] bool read_retry(uint32_t snapshot) const noexcept
98 {
99 std::atomic_thread_fence(std::memory_order_acquire);
100 return m_seq.load(std::memory_order_relaxed) != snapshot;
101 }
102
103 /**
104 * @brief Return the raw sequence counter value.
105 *
106 * Primarily useful for diagnostics and assertions. Do not use for
107 * synchronization logic; use read_begin() / read_retry() instead.
108 */
109 [[nodiscard]] uint32_t sequence() const noexcept
110 {
111 return m_seq.load(std::memory_order_relaxed);
112 }
113
114private:
115 std::atomic<uint32_t> m_seq { 0U };
116};
117
118// =============================================================================
119// RAII write guard
120// =============================================================================
121
122/**
123 * @class SeqlockWriteGuard
124 * @brief RAII guard that brackets a Seqlock write region.
125 *
126 * Calls begin_write() on construction and end_write() on destruction.
127 * Non-copyable, non-movable: the guard is tied to a single lexical scope.
128 *
129 * @code
130 * {
131 * SeqlockWriteGuard guard(m_layer_locks[layer]);
132 * std::memcpy(dest, src, byte_count);
133 * }
134 * @endcode
135 */
137public:
138 explicit SeqlockWriteGuard(Seqlock& lock) noexcept
139 : m_lock(lock)
140 {
142 }
143
145 {
147 }
148
153
154private:
156};
157
158// =============================================================================
159// Bounded reader helper
160// =============================================================================
161
162/**
163 * @brief Invoke a read functor under a Seqlock with a bounded retry count.
164 *
165 * Retries the read up to @p max_attempts times if a concurrent write is
166 * detected. On each attempt the functor is called with no arguments; its
167 * return value is forwarded on success.
168 *
169 * Returns std::nullopt if @p max_attempts is exhausted without a clean
170 * read. Callers in frame loops should treat nullopt as "use previous frame"
171 * rather than blocking.
172 *
173 * The functor must be callable with no arguments and must return a value
174 * (or void — see seqlock_read_void for void functors).
175 *
176 * @tparam Fn Callable type, must satisfy std::invocable<Fn>.
177 * @param lock The Seqlock guarding the data region.
178 * @param max_attempts Maximum number of read attempts before giving up.
179 * 0 means unbounded (spin until clean).
180 * @param fn Functor that reads and returns the guarded data.
181 * @return std::optional<return type of fn>, empty on exhaustion.
182 *
183 * @code
184 * auto pixels = seqlock_read(m_layer_locks[layer], 8, [&] {
185 * return std::span<const uint8_t>(buf.data(), buf.size());
186 * });
187 * if (pixels) upload_to_gpu(*pixels);
188 * @endcode
189 */
190template <std::invocable Fn>
191[[nodiscard]] auto seqlock_read(
192 const Seqlock& lock,
193 uint32_t max_attempts,
194 Fn&& fn) -> std::optional<std::invoke_result_t<Fn>>
195{
196 uint32_t attempts = 0;
197 while (max_attempts == 0 || attempts < max_attempts) {
198 uint32_t snap = lock.read_begin();
199 auto result = std::invoke(std::forward<Fn>(fn));
200 if (!lock.read_retry(snap))
201 return result;
202 ++attempts;
203 }
204 return std::nullopt;
205}
206
207/**
208 * @brief Invoke a void read functor under a Seqlock with a bounded retry count.
209 *
210 * Same semantics as seqlock_read but for functors with no return value.
211 * Returns true if a clean read was achieved within @p max_attempts,
212 * false if the limit was exhausted.
213 *
214 * @tparam Fn Callable type with void return, must satisfy std::invocable<Fn>.
215 * @param lock The Seqlock guarding the data region.
216 * @param max_attempts Maximum attempts; 0 = unbounded.
217 * @param fn Functor that reads the guarded region (side-effect only).
218 * @return true on clean read, false on exhaustion.
219 */
220template <std::invocable Fn>
221 requires std::is_void_v<std::invoke_result_t<Fn>>
223 const Seqlock& lock,
224 uint32_t max_attempts,
225 Fn&& fn)
226{
227 uint32_t attempts = 0;
228 while (max_attempts == 0 || attempts < max_attempts) {
229 uint32_t snap = lock.read_begin();
230 std::invoke(std::forward<Fn>(fn));
231 if (!lock.read_retry(snap))
232 return true;
233 ++attempts;
234 }
235 return false;
236}
237
238// =============================================================================
239// SeqlockArray
240// =============================================================================
241
242/**
243 * @class SeqlockArray
244 * @brief Indexed collection of independent, equal-ranked Seqlock instances.
245 *
246 * Manages N independently lockable Seqlock slots with no privileged index.
247 * Suitable wherever a container holds N equal data slots (layers, channels,
248 * ring positions) that may be written and read independently and concurrently.
249 *
250 * Slots are heap-allocated individually via unique_ptr to satisfy alignas(64)
251 * without violating the non-copyable / non-movable contract of Seqlock.
252 *
253 * resize() is not safe to call concurrently with any read or write operation
254 * on this instance.
255 *
256 * @code
257 * SeqlockArray locks(num_slots);
258 *
259 * // writer
260 * {
261 * SeqlockWriteGuard g(locks[slot]);
262 * std::ranges::copy(src, dst.begin());
263 * }
264 *
265 * // reader
266 * auto result = seqlock_read(locks[slot], 8, [&]{ return read_slot(slot); });
267 * @endcode
268 */
270public:
271 SeqlockArray() = default;
272
273 explicit SeqlockArray(size_t n)
274 {
275 resize(n);
276 }
277
278 SeqlockArray(const SeqlockArray&) = delete;
282
283 ~SeqlockArray() = default;
284
285 /**
286 * @brief Replace the slot array with n default-constructed Seqlock instances.
287 *
288 * Not concurrent-safe. Call only during initialisation or reconfiguration
289 * when no readers or writers are active.
290 *
291 * @param n New slot count.
292 */
293 void resize(size_t n)
294 {
295 m_slots.clear();
296 m_slots.reserve(n);
297 for (size_t i = 0; i < n; ++i)
298 m_slots.emplace_back(std::make_unique<Seqlock>());
299 }
300
301 /**
302 * @brief Number of slots.
303 */
304 [[nodiscard]] size_t size() const noexcept { return m_slots.size(); }
305
306 /**
307 * @brief Access the Seqlock at index @p i.
308 * @pre i < size()
309 */
310 [[nodiscard]] Seqlock& operator[](size_t i) noexcept { return *m_slots[i]; }
311
312 /**
313 * @brief Access the Seqlock at index @p i (const overload).
314 * @pre i < size()
315 */
316 [[nodiscard]] const Seqlock& operator[](size_t i) const noexcept { return *m_slots[i]; }
317
318private:
319 std::vector<std::unique_ptr<Seqlock>> m_slots;
320};
321
322} // namespace MayaFlux::Memory
const Seqlock & operator[](size_t i) const noexcept
Access the Seqlock at index i (const overload).
Definition SeqLock.hpp:316
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:293
SeqlockArray & operator=(const SeqlockArray &)=delete
size_t size() const noexcept
Number of slots.
Definition SeqLock.hpp:304
Seqlock & operator[](size_t i) noexcept
Access the Seqlock at index i.
Definition SeqLock.hpp:310
std::vector< std::unique_ptr< Seqlock > > m_slots
Definition SeqLock.hpp:319
SeqlockArray(const SeqlockArray &)=delete
Indexed collection of independent, equal-ranked Seqlock instances.
Definition SeqLock.hpp:269
SeqlockWriteGuard(SeqlockWriteGuard &&)=delete
SeqlockWriteGuard(const SeqlockWriteGuard &)=delete
SeqlockWriteGuard & operator=(SeqlockWriteGuard &&)=delete
SeqlockWriteGuard & operator=(const SeqlockWriteGuard &)=delete
SeqlockWriteGuard(Seqlock &lock) noexcept
Definition SeqLock.hpp:138
RAII guard that brackets a Seqlock write region.
Definition SeqLock.hpp:136
void end_write() noexcept
End a write: advances the counter back to an even value.
Definition SeqLock.hpp:69
uint32_t read_begin() const noexcept
Begin a read: spin until the counter is even, return the snapshot.
Definition SeqLock.hpp:82
uint32_t sequence() const noexcept
Return the raw sequence counter value.
Definition SeqLock.hpp:109
bool read_retry(uint32_t snapshot) const noexcept
Check whether a write overlapped the read just completed.
Definition SeqLock.hpp:97
std::atomic< uint32_t > m_seq
Definition SeqLock.hpp:115
void begin_write() noexcept
Begin a write: advances the counter to an odd value.
Definition SeqLock.hpp:59
Seqlock() noexcept=default
Single-writer multiple-reader sequence lock for fixed-size data regions.
Definition SeqLock.hpp:44
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:191
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:222