ion 0.12.0
Atomic concurrency support library
Loading...
Searching...
No Matches
barrier.hpp
Go to the documentation of this file.
1/**
2 * @defgroup ion_barriers Barriers
3 * Provides ion::Barrier and ion::Latch
4 *
5 * @ingroup ion
6 *
7 * @file
8 * @ingroup ion_barriers
9 *
10 * @brief ion::Barrier
11 *
12 * @copyright
13 * SPDX-FileCopyrightText: 2022-2024 European Southern Observatory (ESO)
14
15 * SPDX-License-Identifier: LGPL-3.0-only
16 */
17#ifndef ION_BARRIER_HPP
18#define ION_BARRIER_HPP
19#include <atomic>
20#include <cassert>
21#include <cstddef>
22#include <cstdint>
23#include <limits>
24
25#include <ion/detail/barrier.hpp>
26#include <ion/detail/macros.hpp>
27
28namespace ion {
29
30/**
31 * Token from `ion::Barrier::Wait()`
32 *
33 * @see ion::Barrier
34 *
35 * @thread_compatible
36 * @ingroup ion_barriers
37 */
38class ArrivalToken {
39public:
40 ArrivalToken(ArrivalToken&&) noexcept = default;
41 ArrivalToken& operator=(ArrivalToken&&) noexcept = default;
42
43protected:
44 enum Expired : std::uint8_t {
45 NotExpired,
46 AlreadyExpired,
47 };
48 /**
49 * Initalize with known last signal value and whether token has already expired.
50 */
51 inline explicit ArrivalToken(std::uint64_t last, Expired expired);
52 template <class>
53 friend class Barrier;
54 std::uint64_t m_last;
55 Expired m_expired;
56};
57
58/**
59 * Reusable thread-coordination mechanism modelled after `std::barrier`.
60 *
61 * Modelled after C++20 std::barrier but does not perform system calls or yields and as such is more
62 * suitable in a real-time environment. Notable differences:
63 * - Methods are noexcept as no mutexes are involved.
64 *
65 * A Barrier phase consist of two states:
66 * 1. *arrival*, when threads fetch and decrement the expected count
67 * 2. *departure*, when all threads have arrived and will depart. The barrier is automatically
68 * reset to the next arrival phase.
69 *
70 * @tparam CompletionFunction no-except invocable function, invoked by *one* of the threads that
71 * arrive at the barrier via `Arrive()`, `ArriveAndWait()` or `ArriveAndDrop()`.
72 *
73 * @memory_order{
74 * @synchronizes_with{All calls to `ArriveAndWait()` or `ArriveAndDrop()`,
75 * any calls to `ArriveAndWait()` or `Wait()`.}
76 *
77 * The *CompletionFunction* @synchronizes_with calls to `ArriveAndWait()` or `Wait()` after
78 * it is executed. This means that no additional synchronization is necessary in the
79 * *CompletionFunction* w.r.t. to the threads that have arrived and is waiting for departure.
80 * }
81 * @thread_safe
82 * @ingroup ion_barriers
83 * @headerfile <> <ion/barrier.hpp>
84 */
85#if !defined(DOXYGEN)
86template <class CompletionFunction = detail::NoOp>
87class Barrier : detail::BarrierBase<CompletionFunction> {
88#else
89template <class CompletionFunction = *unspecified*>
90class Barrier {
91#endif
92public:
93 /** Construct new barrier with an expected count of @a expected.
94 *
95 * @pre @a expected <= Barrier::Max()
96 *
97 * @param expected Expected number of arrivals.
98 * @param func Completion function invoked once for every barrier phase.
99 * phases *arrival* and *departure*.
100 */
101 constexpr explicit Barrier(std::ptrdiff_t expected,
102 CompletionFunction func = CompletionFunction()) noexcept;
103
104 /**
105 * @name Barrier requires stable address and is neither copyable or movable.
106 */
107 /// @{
108 Barrier(Barrier const&) = delete;
109 Barrier& operator=(Barrier const&) = delete;
110 /// @}
111
112 /**
113 * Arrives at barrier, possibly executing completion function and returns token.
114 *
115 * @pre @a n must be > 0 and <= the expected count for the current barrier phase.
116 *
117 * Constructs an ArrivalToken object associated with the phase synchronization point for the
118 * current phase. Then, decrements the expected count by n.
119 *
120 * This function executes atomically. The call to this function strongly happens-before the
121 * start of the phase completion step for the current phase.
122 *
123 * The behavior is undefined if @a n is less than or equal to @a 0 or greater than the expected
124 * count for the current barrier phase.
125 *
126 * @param n the value by which the expected count will be decreased.
127 * @return ArrivalToken associated with current phase.
128 * @sa Wait()
129 * @sa ArriveAndWait()
130 */
131 [[nodiscard]] auto Arrive(std::ptrdiff_t n = 1) noexcept -> ArrivalToken;
132
133 /**
134 * Block at the barrier synchronization point associated with @a token.
135 *
136 * If arrival is associated with the phase synchronization point for the current phase of *this,
137 * blocks at the synchronization point associated with arrival until the phase completion step
138 * of the synchronization point's phase is run.
139 *
140 * Otherwise, if arrival is associated with the phase synchronization point for the immediately
141 * preceding phase of *this, returns immediately. Otherwise, i.e. if arrival is associated with
142 * the phase synchronization point for an earlier phase of *this or any phase of a barrier
143 * object other than *this, the behavior is undefined.
144 *
145 * @param token Arrival token from previous call to `Arrive()`.
146 * @sa Arrive()
147 */
148 void Wait(ArrivalToken&& token) const noexcept;
149
150 /**
151 * Equivalent to `barrier.Wait(barrier.Arrive(n))`
152 *
153 * @param n the value by which the expected count will be decreased.
154 */
155 void ArriveAndWait(std::ptrdiff_t n = 1) noexcept;
156
157 /**
158 * Decrements both the initial expected count for subsequent phases and the expected count for
159 * current phase by one.
160 *
161 * @pre Expected count for current barrier phase must be >= 1.
162 *
163 * Unlike ArriveAndWait it will not wait.
164 */
165 void ArriveAndDrop() noexcept;
166
167 /**
168 * @return Maximum possible value for expected count.
169 */
170 static constexpr auto Max() noexcept -> std::ptrdiff_t;
171
172private:
173 ION_FORCE_INLINE inline void Signal() noexcept;
174
175 /**
176 * Barrier expected number of arrivals.
177 * Must be `acquired` when resetting m_remaining.
178 */
179 std::atomic<std::ptrdiff_t> m_expected;
180
181 /**
182 * Remaining number of arrivals in current iteration.
183 */
184 std::atomic<std::ptrdiff_t> m_remaining;
185
186 /**
187 * Signal to wait on.
188 * @note will eventually overflow and reset to 0.
189 */
190 alignas(ION_DESTRUCTIVE_INTERFERENCE_SIZE) std::atomic<std::uint64_t> m_signal;
191};
192
193/**
194 * Controls barrier completion function invocation policy.
195 *
196 * @ingroup ion_barriers
197 */
198enum class InvokePolicy {
199 /**
200 * Completion function invocation is allowed.
201 */
203
204 /**
205 * Completion function invocation is not allowed.
206 */
208};
209
210/**
211 * Reusable thread-coordination mechanism similar to ion::Barrier but allows control where the
212 * completion function is invoked.
213 *
214 * @tparam CompletionFunction no-except invocable function, invoked by *one* of the threads that
215 * arrive at the barrier via `ArriveAndWait()` and specified policy InvokePolicy::Allowed.
216 *
217 * @memory_order{
218 * @synchronizes_with{All calls to `ArriveAndWait()`, any calls to `ArriveAndWait()`.}
219 *
220 * The *CompletionFunction* @synchronizes_with calls to `ArriveAndWait()` after
221 * it is executed. This means that no additional synchronization is necessary in the
222 * *CompletionFunction* w.r.t. to the threads that have arrived and is waiting for departure.
223 * }
224 * @thread_safe
225 * @ingroup ion_barriers
226 * @headerfile <> <ion/barrier.hpp>
227 */
228template <class CompletionFunction = detail::NoOp>
229class FlexBarrier : detail::BarrierBase<CompletionFunction> {
230public:
231 /** Construct new barrier with an expected count of @a expected.
232 *
233 * @pre @a expected <= Barrier::Max()
234 *
235 * @param expected Expected number of arrivals.
236 * @param func Completion function invoked at most once for every barrier phase.
237 * phases *arrival* and *departure*.
238 */
239 constexpr explicit FlexBarrier(std::uint32_t expected,
240 CompletionFunction func = CompletionFunction()) noexcept;
241
242 /**
243 * @name FlexBarrier requires stable address and is neither copyable or movable.
244 */
245 /// @{
246 FlexBarrier(FlexBarrier const&) = delete;
247 FlexBarrier& operator=(FlexBarrier const&) = delete;
248 /// @}
249
250 /**
251 * Arrives at barrier, waits until number of arrivals reaches the expected number and depending
252 * on @a policy it may execute the completion function.
253 *
254 * @pre @a n must be > 0 and <= the expected count for the current barrier phase.
255 *
256 * Decrements the expected count by @a n and iff policy == InvokePolicy::Allowed then it may
257 * also invoke the completion function.
258 *
259 * This function executes atomically. The call to this function strongly happens-before the
260 * start of the phase completion step for the current phase.
261 *
262 * The behavior is undefined if @a n is less than or equal to @a 0 or greater than the expected
263 * count for the current barrier phase.
264 *
265 * @param policy Policy indicating whether the completion function is allowed to be invoked.
266 * @param n the value by which the expected count will be decreased.
267 * @param wait function invoked repeatedly while waiting for barrier to be lifted. The
268 * wait function is invoked without any synchronization guaranteees and may or may not be
269 * invoked at all if calling thread does not have to wait. If it is desired that all threads
270 * leave barrier at the same time, user should take special care to not introduce any latency in
271 * `WaitFunction` such that the thread may lag. Generally this feature should be avoided if
272 * possible.
273 *
274 * @tparam WaitFunction no-except invocable function with signature `void() noexcept`. Default
275 * type is a no-op. See @a wait argument for details.
276 */
277 template <class WaitFunction = detail::NoOp>
279 std::uint32_t n = 1u,
280 WaitFunction&& wait = WaitFunction()) noexcept;
281
282 /**
283 * @return Maximum possible value for expected count.
284 */
285 static constexpr auto Max() noexcept -> std::uint32_t;
286
287private:
288 static ION_FORCE_INLINE constexpr auto
289 MakeRemaining(std::uint64_t expected) noexcept -> std::uint64_t;
290 template <class WaitFunction>
291 void Wait(std::uint64_t last, WaitFunction&& wait) const noexcept;
292 inline void Signal(bool invoke) noexcept;
293 template <class WaitFunction>
294 inline void AwaitArrivals(WaitFunction&& wait) noexcept;
295
296 /**
297 * Remaining number of arrivals in current iteration.
298 *
299 * @note Low 32 bits are used to indicate remaining barrier arrival count.
300 * The high 32 bits are initially set to all ones and are subtracted once per call to Arrive*
301 * iff callbacks are allowed.
302 */
303 std::atomic<std::uint64_t> m_remaining;
304
305 /**
306 * Barrier expected number of arrivals.
307 * Must be `acquired` when resetting m_remaining.
308 */
309 std::uint32_t const m_expected;
310
311 /**
312 * Signal to wait on.
313 * @note will eventually overflow and reset to 0.
314 */
315 alignas(ION_DESTRUCTIVE_INTERFERENCE_SIZE) std::atomic<std::uint64_t> m_signal;
316};
317
318template <class CompletionFunction>
319constexpr Barrier<CompletionFunction>::Barrier(std::ptrdiff_t expected,
320 CompletionFunction func) noexcept
321 : detail::BarrierBase<CompletionFunction>(std::move(func))
322 , m_expected(expected)
323 , m_remaining(expected)
324 , m_signal(0) {
325 assert(expected >= 0 && expected <= Max());
326}
327
328template <class CompletionFunction>
329[[nodiscard]] auto Barrier<CompletionFunction>::Arrive(std::ptrdiff_t n) noexcept -> ArrivalToken {
330 // Subtract current slot with `n`
331 // Iff this thread is the one that triggers the barrier signal condition the following applies:
332 // 1. Completion function is invoked.
333 // 2. Signal is emitted (to unblock other threads).
334
335 // We must read the current signal state *before* we read m_remaining, to be able to guarantee
336 // that the ArrivalToken contains an accurate value (the following write-release prevents
337 // reordering).
338 auto signal = m_signal.load(std::memory_order_relaxed);
339
340 // We use release as it needs release-acquire synchronization with the thread that will end up
341 // triggering the barrier phase transition.
342 auto remaining_pre_sub = m_remaining.fetch_sub(n, std::memory_order_release);
343 assert(remaining_pre_sub >= n);
344 if (remaining_pre_sub == n) {
345 // This thread was the last to arrive and must trigger the transition from *arrival* to
346 // *departure* phase. This includes invoking completion function and signalling the other
347 // threads. This is done in Signal().
348 //
349 // note: synchronize release from waiting threads using acquire semantics
350 std::atomic_thread_fence(std::memory_order_acquire);
351 ION_ANNOTATE_ACQUIRE(&m_remaining);
352
353 Signal();
354 // value of signal doesn't matter in this case.
355 return ArrivalToken(signal, ArrivalToken::AlreadyExpired);
356 } else {
357 // This wasn't the thread that triggered the barrier
358 // note: We cannot read *m_signal* here as it might have already been signalled.
359 return ArrivalToken(signal, ArrivalToken::NotExpired);
360 }
361}
362
363template <class CompletionFunction>
365 if (token.m_expired == ArrivalToken::AlreadyExpired) {
366 // ArrivalToken was already recorded as expired when created in previous call to arrive.
367 return;
368 }
369 // Spinlock waiting for signal to change from current value
370 while (true) {
371 auto sig = m_signal.load(std::memory_order_relaxed);
372 if (sig != token.m_last) {
373 // Signal changed -> synchronize with completion handler and return
374 std::atomic_thread_fence(std::memory_order_acquire);
375 ION_ANNOTATE_ACQUIRE(const_cast<void*>(static_cast<void const*>(&m_signal)));
376 return;
377 }
378 ION_PAUSE();
379 }
380}
381
382template <class CompletionFunction>
383constexpr auto Barrier<CompletionFunction>::Max() noexcept -> std::ptrdiff_t {
384 return std::numeric_limits<std::ptrdiff_t>::max();
385}
386
387ArrivalToken::ArrivalToken(std::uint64_t last, Expired expired) : m_last(last), m_expired(expired) {
388}
389
390template <class CompletionFunction>
391void Barrier<CompletionFunction>::ArriveAndWait(std::ptrdiff_t n) noexcept {
392 Wait(Arrive(n));
393}
394
395template <class CompletionFunction>
397 // Subtract expected *and* remaining with `1`
398 // Iff this thread is the one that triggers the barrier signal condition the following applied:
399 // 1. Completion function is invoked.
400 // 2. Signal is emitted.
401
402 (void)m_expected.fetch_sub(1, std::memory_order_relaxed);
403 // We use release as it needs release-acquire synchronization with the thread that will end up
404 // triggering the barrier phase transition.
405 auto remaining_pre_sub = m_remaining.fetch_sub(1, std::memory_order_release);
406 assert(remaining_pre_sub >= 1);
407 if (remaining_pre_sub == 1) {
408 // This thread was the last to arrive and must trigger the transition from *arrival* to
409 // *departure* phase. This includes invoking completion function and signalling the other
410 // threads. This is done in Signal().
411 //
412 // note: synchronize release from waiting threads using acquire semantics
413 std::atomic_thread_fence(std::memory_order_acquire);
414 ION_ANNOTATE_ACQUIRE(&m_remaining);
415 Signal();
416 }
417}
418
419template <class CompletionFunction>
420void Barrier<CompletionFunction>::Signal() noexcept {
421 // *this* thread triggered condition so we must execute handler
422 if constexpr (!detail::IsNoOp<CompletionFunction>) {
423 this->Complete();
424 // note: synchronize-with waiting threads using release semantics happens with the
425 // signalling as the last thing in this function.
426 }
427
428 constexpr auto acquire_order = []() -> std::memory_order {
429 if constexpr (!detail::IsNoOp<CompletionFunction>) {
430 // As we just acquired above m_expected does not need any memory barrier at all.
431 return std::memory_order_relaxed;
432 } else {
433 return std::memory_order_consume;
434 }
435 }();
436
437 // Update value of m_remaining from m_expected using minimum synchronization
438 m_remaining.store(m_expected.load(acquire_order), std::memory_order_relaxed);
439
440 // Signal awaiters with memory release barrier for the ordering constraint
441 (void)m_signal.fetch_add(1, std::memory_order_release);
442}
443
444template <class CompletionFunction>
445constexpr FlexBarrier<CompletionFunction>::FlexBarrier(std::uint32_t expected,
446 CompletionFunction func) noexcept
447 : detail::BarrierBase<CompletionFunction>(std::move(func))
448 , m_remaining(MakeRemaining(expected))
449 , m_expected(expected)
450 , m_signal(0u) {
451 assert(expected <= Max());
452}
453
454template <class CompletionFunction>
455constexpr auto FlexBarrier<CompletionFunction>::Max() noexcept -> std::uint32_t {
456 return std::numeric_limits<std::uint32_t>::max();
457}
458
459template <class CompletionFunction>
460constexpr auto
461FlexBarrier<CompletionFunction>::MakeRemaining(std::uint64_t expected) noexcept -> std::uint64_t {
462 return 0xffff'ffff'0000'0000 | expected;
463}
464
465template <class CompletionFunction>
466template <class WaitFunction>
468 std::uint32_t n,
469 WaitFunction&& wait) noexcept {
470 // Subtract current slot with `n`
471 // Iff this thread is the one that triggers the barrier signal condition the following applies:
472 // 1. Completion function is invoked.
473 // 2. Signal is emitted (to unblock other threads).
474
475 // We must read the current signal state *before* we read m_remaining, to be able to guarantee
476 // that the ArrivalToken contains an accurate value (the following write-release prevents
477 // reordering).
478 auto signal = m_signal.load(std::memory_order_relaxed);
479
480 std::uint64_t const value = mode == InvokePolicy::Allowed
481 ? static_cast<std::uint64_t>(n) + 0x1'0000'0000
482 : static_cast<std::uint64_t>(n);
483 // We use release as it needs release-acquire synchronization with the thread that will end up
484 // triggering the barrier phase transition.
485 auto const value_pre_sub = m_remaining.fetch_sub(value, std::memory_order_release);
486 auto const remaining = (value_pre_sub & 0xffff'ffff) - n;
487 auto const any_cf = value_pre_sub >> 32 != 0xffff'ffff;
488 auto const first_cf = mode == InvokePolicy::Allowed && !any_cf;
489
490 if (remaining == 0u) {
491 // This thread was the last to arrive, the following possibilities exist:
492 //
493 // 1. This arrival was the first to allow CF to be invoked.
494 // 2. Another arrival allowed CF to be invoked.
495 // 3. No arrivals allowed CF to be invoked.
496 //
497 // Yields:
498 // 1 => barrier is expiring now => signal with CF
499 // 2 => wait for another arrival to signal.
500 // 3 => barrier is expiring now => signal without CF.
501 if (first_cf) {
502 // 1:
503 // note: synchronize release from waiting threads using acquire semantics
504 std::atomic_thread_fence(std::memory_order_acquire);
505 ION_ANNOTATE_ACQUIRE(&m_remaining);
506 Signal(true);
507 } else if (any_cf) {
508 // 2:
509 Wait(signal, std::forward<WaitFunction>(wait));
510 } else {
511 // 3:
512 Signal(false);
513 }
514 } else {
515 // This thread was not last to arrive, but if it was the first to allow CF to be invoked it
516 // will have to wait to do so.
517 //
518 // The possibilities are:
519 //
520 // 1. Arrival does not allow CF to be invoked.
521 // 2. Arrival allowes CF to be invoked but was not first.
522 // 3. Arrival allowes CF to be invoked and was first.
523 //
524 // Yields:
525 // 1 & 2 => barrier has not expired
526 // 3 => barrier is expiring *later* but signalling will be done by this thread => wait and
527 // signal.
528
529 // note: We cannot read *m_signal* here as it might have already been signalled.
530 if (!first_cf) {
531 // 1 & 2:
532 Wait(signal, std::forward<WaitFunction>(wait));
533 } else {
534 // 3: Wait for barrier arrivals to reach zero.
535 AwaitArrivals(std::forward<WaitFunction>(wait));
536 // note: synchronize release from waiting threads using acquire semantics
537 std::atomic_thread_fence(std::memory_order_acquire);
538 ION_ANNOTATE_ACQUIRE(&m_remaining);
539 Signal(true);
540 }
541 }
542}
543
544template <class CompletionFunction>
545template <class WaitFunction>
546void FlexBarrier<CompletionFunction>::Wait(std::uint64_t last,
547 WaitFunction&& wait) const noexcept {
548 // Spin waiting for signal to change from current value
549 while (true) {
550 auto sig = m_signal.load(std::memory_order_relaxed);
551 if (ION_UNLIKELY(sig != last)) {
552 // Signal changed -> synchronize with completion handler and return
553 std::atomic_thread_fence(std::memory_order_acquire);
554 ION_ANNOTATE_ACQUIRE(const_cast<void*>(static_cast<void const*>(&m_signal)));
555 return;
556 }
557 detail::Invoke(wait);
558 ION_PAUSE();
559 }
560}
561
562template <class CompletionFunction>
563void FlexBarrier<CompletionFunction>::Signal(bool invoke) noexcept {
564 // *this* thread triggered condition so we must execute handler
565 if constexpr (!detail::IsNoOp<CompletionFunction>) {
566 if (invoke) {
567 this->Complete();
568 }
569 // note: synchronize-with waiting threads using release semantics happens with the
570 // signalling as the last thing in this function.
571 }
572
573 // Update value of m_remaining from m_expected using minimum synchronization
574 m_remaining.store(MakeRemaining(m_expected), std::memory_order_relaxed);
575
576 // Signal awaiters with memory release barrier for the ordering constraint
577 (void)m_signal.fetch_add(1, std::memory_order_release);
578}
579
580template <class CompletionFunction>
581template <class WaitFunction>
582void FlexBarrier<CompletionFunction>::AwaitArrivals(WaitFunction&& wait) noexcept {
583 for (auto r = m_remaining.load(std::memory_order_relaxed); ION_LIKELY((r & 0xffff'ffff) != 0);
584 r = m_remaining.load(std::memory_order_relaxed)) {
585 detail::Invoke(wait);
586 ION_PAUSE();
587 }
588}
589
590} // namespace ion
591#endif // ION_BARRIER_HPP
Token from ion::Barrier::Wait()
Definition barrier.hpp:38
Reusable thread-coordination mechanism modelled after std::barrier.
Definition barrier.hpp:90
constexpr Barrier(std::ptrdiff_t expected, CompletionFunction func=CompletionFunction()) noexcept
Construct new barrier with an expected count of expected.
Definition barrier.hpp:319
void ArriveAndWait(std::ptrdiff_t n=1) noexcept
void Wait(ArrivalToken &&token) const noexcept
static constexpr auto Max() noexcept -> std::ptrdiff_t
auto Arrive(std::ptrdiff_t n=1) noexcept -> ArrivalToken
void ArriveAndWait(InvokePolicy policy, std::uint32_t n=1u, WaitFunction &&wait=WaitFunction()) noexcept
static constexpr auto Max() noexcept -> std::uint32_t
constexpr FlexBarrier(std::uint32_t expected, CompletionFunction func=CompletionFunction()) noexcept
Construct new barrier with an expected count of expected.
Definition barrier.hpp:445
InvokePolicy
Controls barrier completion function invocation policy.
Definition barrier.hpp:198
@ Allowed
Completion function invocation is allowed.
Definition barrier.hpp:202
@ Disallowed
Completion function invocation is not allowed.
Definition barrier.hpp:207
auto Wait(SignalToken< T > &token) noexcept -> T
Wait until signal source changes and return current value.