Skip to main content

almost_enough/
sync_stopper.rs

1//! Synchronized cancellation with memory ordering guarantees.
2//!
3//! [`SyncStopper`] uses Release/Acquire ordering to ensure memory synchronization
4//! between the cancelling thread and threads that observe the cancellation.
5//!
6//! # When to Use
7//!
8//! Use `SyncStopper` when you need to ensure that writes made before `cancel()`
9//! are visible to readers after they see `should_stop() == true`.
10//!
11//! ```rust
12//! use almost_enough::{SyncStopper, Stop};
13//! use std::sync::atomic::{AtomicUsize, Ordering};
14//!
15//! static SHARED_DATA: AtomicUsize = AtomicUsize::new(0);
16//!
17//! let stop = SyncStopper::new();
18//!
19//! // Thread A: producer
20//! SHARED_DATA.store(42, Ordering::Relaxed);
21//! stop.cancel();  // Release: flushes SHARED_DATA write
22//!
23//! // Thread B: consumer (same thread here for demo)
24//! if stop.should_stop() {  // Acquire: syncs with Release
25//!     // GUARANTEED to see SHARED_DATA == 42
26//!     let value = SHARED_DATA.load(Ordering::Relaxed);
27//!     assert_eq!(value, 42);
28//! }
29//! ```
30//!
31//! # When NOT to Use
32//!
33//! [`Stopper`](crate::Stopper) gives the same guarantee, to whoever holds the
34//! `Stopper`, without an Acquire load on every check: its `cancel()` is also a
35//! Release swap, and its [`is_cancelled()`](crate::Stopper::is_cancelled) is
36//! the Acquire query to call after the work reports the cancellation. Use
37//! `SyncStopper` when the code that reads the handed-over data only sees the
38//! cancellation through `check()` or `should_stop()`.
39//!
40//! # Memory Ordering
41//!
42//! | Operation | Ordering | Effect |
43//! |-----------|----------|--------|
44//! | `cancel()` | Release | Flushes prior writes |
45//! | `is_cancelled()` | Acquire | Syncs with Release |
46//! | `should_stop()` | Acquire | Syncs with Release |
47//! | `check()` | Acquire | Syncs with Release |
48
49use alloc::sync::Arc;
50use core::sync::atomic::{AtomicBool, Ordering};
51
52use crate::{Stop, StopReason};
53
54/// Inner state for [`SyncStopper`] — implements [`Stop`] with Acquire ordering.
55pub(crate) struct SyncStopperInner {
56    cancelled: AtomicBool,
57}
58
59impl Stop for SyncStopperInner {
60    #[inline]
61    #[track_caller]
62    fn check(&self) -> Result<(), StopReason> {
63        if self.cancelled.load(Ordering::Acquire) {
64            Err(StopReason::Cancelled)
65        } else {
66            Ok(())
67        }
68    }
69
70    #[inline]
71    #[track_caller]
72    fn should_stop(&self) -> bool {
73        self.cancelled.load(Ordering::Acquire)
74    }
75}
76
77/// A cancellation primitive with Release/Acquire memory ordering.
78///
79/// Unlike [`Stopper`](crate::Stopper), whose checks are Relaxed loads,
80/// `SyncStopper` guarantees that all writes before `cancel()` are visible
81/// to any clone that subsequently observes `should_stop() == true`.
82///
83/// Converts to [`StopToken`](crate::StopToken) via `From`/`Into` with zero
84/// overhead — the existing `Arc` is reused, not double-wrapped.
85///
86/// # Performance
87///
88/// On x86-64 an Acquire load is an ordinary load, so a check costs what a
89/// [`Stopper`](crate::Stopper)'s does. On aarch64 it is `ldar` on targets
90/// without RCpc (Linux, Windows, Android, iOS) and `ldapr` with it (macOS):
91/// on a Neoverse-N1, a compute-bound loop checking every 64 bytes measured
92/// 3–10% slower than with a Relaxed check (code placement alone moved the
93/// same loop by similar amounts), and under 1% at a check per KiB or in
94/// memory-bound loops (`benchmarks/stopper-ordering-2026-10-06.md`). On
95/// ARMv6-M and ARMv7-M each check also executes a `dmb`. In a
96/// [`StopToken`](crate::StopToken) it is checked through the vtable.
97#[derive(Debug, Clone)]
98pub struct SyncStopper {
99    pub(crate) inner: Arc<SyncStopperInner>,
100}
101
102impl SyncStopper {
103    /// Create a new synchronized stopper.
104    #[inline]
105    pub fn new() -> Self {
106        Self {
107            inner: Arc::new(SyncStopperInner {
108                cancelled: AtomicBool::new(false),
109            }),
110        }
111    }
112
113    /// Create a stopper that is already cancelled.
114    #[inline]
115    pub fn cancelled() -> Self {
116        Self {
117            inner: Arc::new(SyncStopperInner {
118                cancelled: AtomicBool::new(true),
119            }),
120        }
121    }
122
123    /// Cancel with a Release swap.
124    ///
125    /// All memory writes before a `cancel()` are visible to any clone that
126    /// observes `should_stop() == true` after it, even when several threads
127    /// cancel.
128    #[inline]
129    pub fn cancel(&self) {
130        // A swap: see `Stopper::cancel`.
131        self.inner.cancelled.swap(true, Ordering::Release);
132    }
133
134    /// Check if cancelled with Acquire ordering.
135    ///
136    /// If this returns `true`, all memory writes that happened before
137    /// the corresponding `cancel()` call are guaranteed to be visible.
138    #[inline]
139    pub fn is_cancelled(&self) -> bool {
140        self.inner.cancelled.load(Ordering::Acquire)
141    }
142}
143
144impl Default for SyncStopper {
145    fn default() -> Self {
146        Self::new()
147    }
148}
149
150impl Stop for SyncStopper {
151    #[inline]
152    #[track_caller]
153    fn check(&self) -> Result<(), StopReason> {
154        self.inner.check()
155    }
156
157    #[inline]
158    #[track_caller]
159    fn should_stop(&self) -> bool {
160        self.inner.should_stop()
161    }
162}
163
164impl core::fmt::Debug for SyncStopperInner {
165    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
166        f.debug_struct("SyncStopperInner")
167            .field("cancelled", &self.cancelled.load(Ordering::Relaxed))
168            .finish()
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    #[test]
177    fn sync_stopper_basic() {
178        let stop = SyncStopper::new();
179        assert!(!stop.is_cancelled());
180        assert!(!stop.should_stop());
181        assert!(stop.check().is_ok());
182
183        stop.cancel();
184
185        assert!(stop.is_cancelled());
186        assert!(stop.should_stop());
187        assert_eq!(stop.check(), Err(StopReason::Cancelled));
188    }
189
190    #[test]
191    fn sync_stopper_cancelled_constructor() {
192        let stop = SyncStopper::cancelled();
193        assert!(stop.is_cancelled());
194        assert!(stop.should_stop());
195    }
196
197    #[test]
198    fn sync_stopper_clone_shares_state() {
199        let stop1 = SyncStopper::new();
200        let stop2 = stop1.clone();
201
202        stop2.cancel();
203
204        assert!(stop1.should_stop());
205        assert!(stop2.should_stop());
206    }
207
208    #[test]
209    fn sync_stopper_is_send_sync() {
210        fn assert_send_sync<T: Send + Sync>() {}
211        assert_send_sync::<SyncStopper>();
212    }
213
214    #[test]
215    fn sync_stopper_is_default() {
216        let stop: SyncStopper = Default::default();
217        assert!(!stop.is_cancelled());
218    }
219}