Skip to main content

scirs2_core/concurrent/
barrier.rs

1//! Barrier and latch synchronisation primitives.
2//!
3//! # Primitives
4//!
5//! | Type | Description |
6//! |------|-------------|
7//! | [`CyclicBarrier`] | Reusable barrier — all threads wait until every expected thread arrives, then all proceed; can be reused for the next phase automatically. |
8//! | [`PhaseBarrier`] | Phased barrier that tracks a monotonically-increasing phase counter; useful for iterative algorithms. |
9//! | [`CountDownLatch`] | Single-use latch that opens after `N` count-down calls. |
10//! | [`SpinBarrier`] | Spin-wait barrier with optional yield — zero OS involvement; best for very short waits across a fixed number of threads. |
11//!
12//! All types are `Send + Sync` and avoid `unwrap()`.
13
14use std::sync::atomic::{AtomicUsize, Ordering};
15use std::sync::{Arc, Condvar, Mutex};
16use std::time::{Duration, Instant};
17
18use crate::error::{CoreError, CoreResult, ErrorContext, ErrorLocation};
19
20// ── helpers ──────────────────────────────────────────────────────────────────
21
22fn lock_err(context: &'static str, e: impl std::fmt::Display) -> CoreError {
23    CoreError::MutexError(
24        ErrorContext::new(format!("{context}: mutex poisoned: {e}"))
25            .with_location(ErrorLocation::new(file!(), line!())),
26    )
27}
28
29fn wait_err(context: &'static str, e: impl std::fmt::Display) -> CoreError {
30    CoreError::MutexError(
31        ErrorContext::new(format!("{context}: condvar wait poisoned: {e}"))
32            .with_location(ErrorLocation::new(file!(), line!())),
33    )
34}
35
36// ── CyclicBarrier ─────────────────────────────────────────────────────────────
37
38/// State shared between all parties to the barrier.
39struct CyclicBarrierInner {
40    /// Number of threads that still need to arrive this cycle.
41    waiting: usize,
42    /// Total parties expected each cycle.
43    parties: usize,
44    /// Monotonically-increasing generation; incremented each time all parties arrive.
45    generation: u64,
46    /// Set to `true` after [`CyclicBarrier::break_barrier`] is called.
47    broken: bool,
48}
49
50/// A reusable barrier synchronisation aid.
51///
52/// `n` threads call [`wait`](CyclicBarrier::wait).  The call blocks until all
53/// `n` threads have arrived, at which point all calls return and the barrier
54/// automatically resets for the next cycle.  This is safe to use across
55/// multiple "rounds" without re-creating the struct.
56///
57/// # Example
58///
59/// ```rust
60/// use scirs2_core::concurrent::barrier::CyclicBarrier;
61/// use std::sync::Arc;
62///
63/// let barrier = Arc::new(CyclicBarrier::new(3));
64/// let mut handles = Vec::new();
65/// for _ in 0..3 {
66///     let b = Arc::clone(&barrier);
67///     handles.push(std::thread::spawn(move || b.wait().expect("barrier wait")));
68/// }
69/// for h in handles { h.join().expect("thread"); }
70/// ```
71pub struct CyclicBarrier {
72    inner: Mutex<CyclicBarrierInner>,
73    condvar: Condvar,
74}
75
76impl CyclicBarrier {
77    /// Create a barrier for `parties` threads.
78    ///
79    /// # Panics
80    ///
81    /// Does not panic — returns a valid barrier even for `parties == 0` (which
82    /// passes immediately).
83    pub fn new(parties: usize) -> Self {
84        Self {
85            inner: Mutex::new(CyclicBarrierInner {
86                waiting: parties,
87                parties,
88                generation: 0,
89                broken: false,
90            }),
91            condvar: Condvar::new(),
92        }
93    }
94
95    /// Wait until all parties have arrived.
96    ///
97    /// Returns `Ok(true)` for exactly one thread per cycle (the "trip" thread
98    /// that was the last to arrive).  All other threads get `Ok(false)`.
99    pub fn wait(&self) -> CoreResult<bool> {
100        let mut g = self
101            .inner
102            .lock()
103            .map_err(|e| lock_err("CyclicBarrier::wait", e))?;
104
105        if g.broken {
106            return Err(CoreError::MutexError(ErrorContext::new(
107                "CyclicBarrier: barrier is broken",
108            )));
109        }
110
111        let gen = g.generation;
112        g.waiting -= 1;
113
114        if g.waiting == 0 {
115            // Last thread: reset barrier and wake everyone.
116            g.waiting = g.parties;
117            g.generation = gen.wrapping_add(1);
118            self.condvar.notify_all();
119            return Ok(true);
120        }
121
122        // Not the last — wait for the generation to change.
123        loop {
124            g = self
125                .condvar
126                .wait(g)
127                .map_err(|e| wait_err("CyclicBarrier::wait", e))?;
128            if g.broken {
129                return Err(CoreError::MutexError(ErrorContext::new(
130                    "CyclicBarrier: barrier broken while waiting",
131                )));
132            }
133            if g.generation != gen {
134                return Ok(false);
135            }
136        }
137    }
138
139    /// Wait with a timeout.  Returns `Err` on timeout or if the barrier is broken.
140    pub fn wait_timeout(&self, timeout: Duration) -> CoreResult<bool> {
141        let deadline = Instant::now() + timeout;
142        let mut g = self
143            .inner
144            .lock()
145            .map_err(|e| lock_err("CyclicBarrier::wait_timeout", e))?;
146
147        if g.broken {
148            return Err(CoreError::MutexError(ErrorContext::new(
149                "CyclicBarrier: barrier is broken",
150            )));
151        }
152
153        let gen = g.generation;
154        g.waiting -= 1;
155
156        if g.waiting == 0 {
157            g.waiting = g.parties;
158            g.generation = gen.wrapping_add(1);
159            self.condvar.notify_all();
160            return Ok(true);
161        }
162
163        loop {
164            let remaining = deadline.saturating_duration_since(Instant::now());
165            if remaining.is_zero() {
166                // Timed out — mark barrier as broken to unblock others.
167                g.broken = true;
168                self.condvar.notify_all();
169                return Err(CoreError::TimeoutError(ErrorContext::new(
170                    "CyclicBarrier: timed out waiting for all parties",
171                )));
172            }
173            let (next_g, _timeout_result) = self
174                .condvar
175                .wait_timeout(g, remaining)
176                .map_err(|e| wait_err("CyclicBarrier::wait_timeout", e))?;
177            g = next_g;
178            if g.broken {
179                return Err(CoreError::MutexError(ErrorContext::new(
180                    "CyclicBarrier: barrier broken while waiting",
181                )));
182            }
183            if g.generation != gen {
184                return Ok(false);
185            }
186        }
187    }
188
189    /// Forcibly break the barrier.  All waiting threads receive an error.
190    pub fn break_barrier(&self) {
191        if let Ok(mut g) = self.inner.lock() {
192            g.broken = true;
193            self.condvar.notify_all();
194        }
195    }
196
197    /// Reset the barrier to its initial state.
198    pub fn reset(&self) {
199        if let Ok(mut g) = self.inner.lock() {
200            g.waiting = g.parties;
201            g.broken = false;
202            g.generation = g.generation.wrapping_add(1);
203            self.condvar.notify_all();
204        }
205    }
206
207    /// Returns `true` if the barrier is broken.
208    pub fn is_broken(&self) -> bool {
209        self.inner.lock().map(|g| g.broken).unwrap_or(true)
210    }
211
212    /// Number of parties that have not yet arrived in the current cycle.
213    pub fn waiting(&self) -> usize {
214        self.inner.lock().map(|g| g.waiting).unwrap_or(0)
215    }
216
217    /// The total number of parties.
218    pub fn parties(&self) -> usize {
219        self.inner.lock().map(|g| g.parties).unwrap_or(0)
220    }
221}
222
223// ── PhaseBarrier ─────────────────────────────────────────────────────────────
224
225/// Internal state for [`PhaseBarrier`].
226struct PhaseBarrierInner {
227    phase: u64,
228    waiting: usize,
229    parties: usize,
230}
231
232/// A phased barrier with a monotonically-advancing phase counter.
233///
234/// Conceptually identical to [`CyclicBarrier`] but exposes the *phase number*
235/// so that callers can check which phase they are currently in.  Useful for
236/// iterative parallel algorithms where each phase represents one iteration.
237///
238/// # Example
239///
240/// ```rust
241/// use scirs2_core::concurrent::barrier::PhaseBarrier;
242/// use std::sync::Arc;
243///
244/// let pb = Arc::new(PhaseBarrier::new(2));
245/// let pb2 = Arc::clone(&pb);
246/// let t = std::thread::spawn(move || pb2.arrive_and_wait().expect("wait"));
247/// pb.arrive_and_wait().expect("wait");
248/// t.join().expect("thread");
249/// assert!(pb.phase() >= 1);
250/// ```
251pub struct PhaseBarrier {
252    inner: Mutex<PhaseBarrierInner>,
253    condvar: Condvar,
254}
255
256impl PhaseBarrier {
257    /// Create a phased barrier for `parties` threads.
258    pub fn new(parties: usize) -> Self {
259        Self {
260            inner: Mutex::new(PhaseBarrierInner {
261                phase: 0,
262                waiting: parties,
263                parties,
264            }),
265            condvar: Condvar::new(),
266        }
267    }
268
269    /// Arrive at the barrier and wait for all other parties.
270    ///
271    /// Returns the phase number that just completed.
272    pub fn arrive_and_wait(&self) -> CoreResult<u64> {
273        let mut g = self
274            .inner
275            .lock()
276            .map_err(|e| lock_err("PhaseBarrier::arrive_and_wait", e))?;
277
278        let current_phase = g.phase;
279        g.waiting -= 1;
280
281        if g.waiting == 0 {
282            // Advance phase and reset counter.
283            g.phase = current_phase.wrapping_add(1);
284            g.waiting = g.parties;
285            self.condvar.notify_all();
286            return Ok(current_phase);
287        }
288
289        loop {
290            g = self
291                .condvar
292                .wait(g)
293                .map_err(|e| wait_err("PhaseBarrier::arrive_and_wait", e))?;
294            if g.phase != current_phase {
295                return Ok(current_phase);
296            }
297        }
298    }
299
300    /// Arrive but do NOT wait (signal and return immediately).
301    ///
302    /// Returns the phase number advanced to, or the current phase if other
303    /// parties are still pending.
304    pub fn arrive(&self) -> CoreResult<u64> {
305        let mut g = self
306            .inner
307            .lock()
308            .map_err(|e| lock_err("PhaseBarrier::arrive", e))?;
309
310        g.waiting -= 1;
311        if g.waiting == 0 {
312            let completed = g.phase;
313            g.phase = completed.wrapping_add(1);
314            g.waiting = g.parties;
315            self.condvar.notify_all();
316            Ok(completed)
317        } else {
318            Ok(g.phase)
319        }
320    }
321
322    /// Current phase number.
323    pub fn phase(&self) -> u64 {
324        self.inner.lock().map(|g| g.phase).unwrap_or(0)
325    }
326
327    /// Number of parties that have not yet arrived this phase.
328    pub fn waiting(&self) -> usize {
329        self.inner.lock().map(|g| g.waiting).unwrap_or(0)
330    }
331}
332
333// ── CountDownLatch ────────────────────────────────────────────────────────────
334
335/// A single-use latch that opens when its counter reaches zero.
336///
337/// Any number of threads may call [`wait`](CountDownLatch::wait) which blocks
338/// until the internal counter reaches 0.  The counter is decremented by calling
339/// [`count_down`](CountDownLatch::count_down).
340///
341/// Once opened the latch *stays open*; subsequent calls to `wait` return
342/// immediately.
343///
344/// # Example
345///
346/// ```rust
347/// use scirs2_core::concurrent::barrier::CountDownLatch;
348/// use std::sync::Arc;
349///
350/// let latch = Arc::new(CountDownLatch::new(3));
351/// let mut handles = Vec::new();
352/// for _ in 0..3 {
353///     let l = Arc::clone(&latch);
354///     handles.push(std::thread::spawn(move || l.count_down()));
355/// }
356/// latch.wait().expect("latch wait");
357/// for h in handles { h.join().expect("thread"); }
358/// ```
359pub struct CountDownLatch {
360    inner: Mutex<usize>,
361    condvar: Condvar,
362}
363
364impl CountDownLatch {
365    /// Create a latch with an initial count of `n`.
366    pub fn new(n: usize) -> Self {
367        Self {
368            inner: Mutex::new(n),
369            condvar: Condvar::new(),
370        }
371    }
372
373    /// Decrement the count by 1.  When it reaches 0 all waiting threads wake.
374    pub fn count_down(&self) {
375        if let Ok(mut g) = self.inner.lock() {
376            if *g > 0 {
377                *g -= 1;
378                if *g == 0 {
379                    self.condvar.notify_all();
380                }
381            }
382        }
383    }
384
385    /// Block until the count reaches 0.
386    pub fn wait(&self) -> CoreResult<()> {
387        let mut g = self
388            .inner
389            .lock()
390            .map_err(|e| lock_err("CountDownLatch::wait", e))?;
391        loop {
392            if *g == 0 {
393                return Ok(());
394            }
395            g = self
396                .condvar
397                .wait(g)
398                .map_err(|e| wait_err("CountDownLatch::wait", e))?;
399        }
400    }
401
402    /// Block until the count reaches 0 or `timeout` elapses.
403    ///
404    /// Returns `true` if the latch opened within the timeout.
405    pub fn wait_timeout(&self, timeout: Duration) -> CoreResult<bool> {
406        let deadline = Instant::now() + timeout;
407        let mut g = self
408            .inner
409            .lock()
410            .map_err(|e| lock_err("CountDownLatch::wait_timeout", e))?;
411        loop {
412            if *g == 0 {
413                return Ok(true);
414            }
415            let remaining = deadline.saturating_duration_since(Instant::now());
416            if remaining.is_zero() {
417                return Ok(false);
418            }
419            let (next_g, _) = self
420                .condvar
421                .wait_timeout(g, remaining)
422                .map_err(|e| wait_err("CountDownLatch::wait_timeout", e))?;
423            g = next_g;
424        }
425    }
426
427    /// Current count (informational, not synchronisation-safe on its own).
428    pub fn count(&self) -> usize {
429        self.inner.lock().map(|g| *g).unwrap_or(0)
430    }
431
432    /// Returns `true` if the latch has already opened.
433    pub fn is_open(&self) -> bool {
434        self.count() == 0
435    }
436}
437
438// ── SpinBarrier ──────────────────────────────────────────────────────────────
439
440/// A spin-wait barrier backed by a single atomic counter.
441///
442/// No OS involvement — threads busy-spin (with optional `yield_now`) until all
443/// arrive.  Appropriate only for very short synchronisation gaps (e.g., < 1 µs
444/// expected wait).  For longer waits prefer [`CyclicBarrier`].
445///
446/// # Example
447///
448/// ```rust
449/// use scirs2_core::concurrent::barrier::SpinBarrier;
450/// use std::sync::Arc;
451///
452/// let b = Arc::new(SpinBarrier::new(2));
453/// let b2 = Arc::clone(&b);
454/// let t = std::thread::spawn(move || b2.wait());
455/// b.wait();
456/// t.join().expect("thread");
457/// ```
458pub struct SpinBarrier {
459    /// Number of parties still to arrive.
460    arrived: AtomicUsize,
461    /// Monotonically-increasing epoch; each full round increments it.
462    epoch: AtomicUsize,
463    parties: usize,
464}
465
466impl SpinBarrier {
467    /// Create a spin barrier for `parties` threads.
468    pub fn new(parties: usize) -> Self {
469        let parties = parties.max(1);
470        Self {
471            arrived: AtomicUsize::new(0),
472            epoch: AtomicUsize::new(0),
473            parties,
474        }
475    }
476
477    /// Spin-wait until all parties have called `wait`.
478    ///
479    /// Uses `std::hint::spin_loop` for low-latency busy waiting.  On each
480    /// unsuccessful poll the thread yields via [`std::thread::yield_now`].
481    pub fn wait(&self) {
482        let current_epoch = self.epoch.load(Ordering::Acquire);
483        let prev = self.arrived.fetch_add(1, Ordering::AcqRel);
484        let new_count = prev + 1;
485
486        if new_count == self.parties {
487            // Last to arrive — reset counter and advance epoch.
488            self.arrived.store(0, Ordering::Release);
489            self.epoch.fetch_add(1, Ordering::Release);
490        } else {
491            // Spin until epoch advances.
492            let mut spins = 0usize;
493            loop {
494                let e = self.epoch.load(Ordering::Acquire);
495                if e != current_epoch {
496                    break;
497                }
498                if spins < 32 {
499                    std::hint::spin_loop();
500                } else {
501                    std::thread::yield_now();
502                }
503                spins = spins.saturating_add(1);
504            }
505        }
506    }
507
508    /// Number of parties.
509    pub fn parties(&self) -> usize {
510        self.parties
511    }
512
513    /// Current epoch (completes per round-trip).
514    pub fn epoch(&self) -> usize {
515        self.epoch.load(Ordering::Relaxed)
516    }
517}
518
519// ── Tests ─────────────────────────────────────────────────────────────────────
520
521#[cfg(test)]
522mod tests {
523    use super::*;
524    use std::sync::atomic::{AtomicU64, Ordering as AO};
525    use std::thread;
526
527    // ── CyclicBarrier ──
528
529    #[test]
530    fn cyclic_barrier_all_proceed() {
531        const N: usize = 5;
532        let barrier = Arc::new(CyclicBarrier::new(N));
533        let counter = Arc::new(AtomicU64::new(0));
534        let mut handles = Vec::new();
535
536        for _ in 0..N {
537            let b = Arc::clone(&barrier);
538            let c = Arc::clone(&counter);
539            handles.push(thread::spawn(move || {
540                c.fetch_add(1, AO::Relaxed);
541                b.wait().expect("barrier wait");
542            }));
543        }
544        for h in handles {
545            h.join().expect("thread");
546        }
547        assert_eq!(counter.load(AO::Relaxed), N as u64);
548    }
549
550    #[test]
551    fn cyclic_barrier_trip_thread_count() {
552        const N: usize = 4;
553        let barrier = Arc::new(CyclicBarrier::new(N));
554        let trips = Arc::new(AtomicU64::new(0));
555        let mut handles = Vec::new();
556
557        for _ in 0..N {
558            let b = Arc::clone(&barrier);
559            let t = Arc::clone(&trips);
560            handles.push(thread::spawn(move || {
561                let trip = b.wait().expect("wait");
562                if trip {
563                    t.fetch_add(1, AO::Relaxed);
564                }
565            }));
566        }
567        for h in handles {
568            h.join().expect("thread");
569        }
570        assert_eq!(trips.load(AO::Relaxed), 1, "exactly one trip thread");
571    }
572
573    #[test]
574    fn cyclic_barrier_two_cycles() {
575        const N: usize = 3;
576        let barrier = Arc::new(CyclicBarrier::new(N));
577        let phase_counter = Arc::new(AtomicU64::new(0));
578        let mut handles = Vec::new();
579
580        for _ in 0..N {
581            let b = Arc::clone(&barrier);
582            let p = Arc::clone(&phase_counter);
583            handles.push(thread::spawn(move || {
584                // Phase 1
585                b.wait().expect("phase 1 wait");
586                p.fetch_add(1, AO::Relaxed);
587                // Phase 2
588                b.wait().expect("phase 2 wait");
589                p.fetch_add(1, AO::Relaxed);
590            }));
591        }
592        for h in handles {
593            h.join().expect("thread");
594        }
595        assert_eq!(phase_counter.load(AO::Relaxed), (N * 2) as u64);
596    }
597
598    // ── PhaseBarrier ──
599
600    #[test]
601    fn phase_barrier_advances_phase() {
602        const N: usize = 4;
603        let pb = Arc::new(PhaseBarrier::new(N));
604        let mut handles = Vec::new();
605        for _ in 0..N {
606            let p = Arc::clone(&pb);
607            handles.push(thread::spawn(move || {
608                p.arrive_and_wait().expect("arrive phase 1");
609                p.arrive_and_wait().expect("arrive phase 2");
610            }));
611        }
612        for h in handles {
613            h.join().expect("thread");
614        }
615        assert_eq!(pb.phase(), 2);
616    }
617
618    // ── CountDownLatch ──
619
620    #[test]
621    fn countdown_latch_basic() {
622        const N: usize = 5;
623        let latch = Arc::new(CountDownLatch::new(N));
624        let counter = Arc::new(AtomicU64::new(0));
625        let mut handles = Vec::new();
626
627        for _ in 0..N {
628            let l = Arc::clone(&latch);
629            let c = Arc::clone(&counter);
630            handles.push(thread::spawn(move || {
631                c.fetch_add(1, AO::Relaxed);
632                l.count_down();
633            }));
634        }
635
636        latch.wait().expect("latch wait");
637        assert!(latch.is_open());
638        assert_eq!(counter.load(AO::Relaxed), N as u64);
639
640        for h in handles {
641            h.join().expect("thread");
642        }
643    }
644
645    #[test]
646    fn countdown_latch_already_open() {
647        let latch = CountDownLatch::new(0);
648        assert!(latch.is_open());
649        latch.wait().expect("already open wait");
650    }
651
652    #[test]
653    fn countdown_latch_timeout_opens() {
654        let latch = Arc::new(CountDownLatch::new(1));
655        let l2 = Arc::clone(&latch);
656        thread::spawn(move || {
657            thread::sleep(Duration::from_millis(20));
658            l2.count_down();
659        });
660        let opened = latch
661            .wait_timeout(Duration::from_secs(5))
662            .expect("wait_timeout");
663        assert!(opened);
664    }
665
666    #[test]
667    fn countdown_latch_timeout_expires() {
668        let latch = CountDownLatch::new(1); // never count down
669        let opened = latch
670            .wait_timeout(Duration::from_millis(10))
671            .expect("wait_timeout");
672        assert!(!opened);
673    }
674
675    // ── SpinBarrier ──
676
677    #[test]
678    fn spin_barrier_basic() {
679        const N: usize = 4;
680        let b = Arc::new(SpinBarrier::new(N));
681        let counter = Arc::new(AtomicU64::new(0));
682        let mut handles = Vec::new();
683
684        for _ in 0..N {
685            let bar = Arc::clone(&b);
686            let c = Arc::clone(&counter);
687            handles.push(thread::spawn(move || {
688                bar.wait();
689                c.fetch_add(1, AO::Relaxed);
690            }));
691        }
692        for h in handles {
693            h.join().expect("thread");
694        }
695        assert_eq!(counter.load(AO::Relaxed), N as u64);
696        assert_eq!(b.epoch(), 1);
697    }
698
699    #[test]
700    fn spin_barrier_multiple_epochs() {
701        const N: usize = 3;
702        let b = Arc::new(SpinBarrier::new(N));
703        let mut handles = Vec::new();
704
705        for _ in 0..N {
706            let bar = Arc::clone(&b);
707            handles.push(thread::spawn(move || {
708                bar.wait(); // epoch 0 → 1
709                bar.wait(); // epoch 1 → 2
710                bar.wait(); // epoch 2 → 3
711            }));
712        }
713        for h in handles {
714            h.join().expect("thread");
715        }
716        assert_eq!(b.epoch(), 3);
717    }
718}