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}