Skip to main content

ferroui_system/threading/
cancellation_token.rs

1//! A minimal cooperative cancellation primitive.
2//!
3//! The dispatcher API takes cancellation tokens in the same places the
4//! reference implementation does (run loops, queued operations, frames).
5//! Tokens are cheap handles that can be shared across threads.
6
7use std::sync::atomic::{AtomicBool, Ordering};
8use std::sync::{Arc, Mutex, PoisonError};
9
10type Callback = Box<dyn FnOnce() + Send>;
11
12struct Inner {
13    canceled: AtomicBool,
14    callbacks: Mutex<Callbacks>,
15}
16
17#[derive(Default)]
18struct Callbacks {
19    next_id: u64,
20    entries: Vec<(u64, Callback)>,
21}
22
23/// Signals to one or more [`CancellationToken`]s that they should be
24/// canceled.
25#[derive(Clone)]
26pub struct CancellationTokenSource {
27    inner: Arc<Inner>,
28}
29
30impl Default for CancellationTokenSource {
31    fn default() -> Self {
32        Self::new()
33    }
34}
35
36impl CancellationTokenSource {
37    pub fn new() -> Self {
38        Self {
39            inner: Arc::new(Inner { canceled: AtomicBool::new(false), callbacks: Mutex::new(Callbacks::default()) }),
40        }
41    }
42
43    /// The token observing this source.
44    pub fn token(&self) -> CancellationToken {
45        CancellationToken { inner: Some(self.inner.clone()) }
46    }
47
48    pub fn is_cancellation_requested(&self) -> bool {
49        self.inner.canceled.load(Ordering::SeqCst)
50    }
51
52    /// Requests cancellation and runs the registered callbacks on the
53    /// calling thread. Subsequent calls do nothing.
54    pub fn cancel(&self) {
55        if self.inner.canceled.swap(true, Ordering::SeqCst) {
56            return;
57        }
58        let entries = {
59            let mut callbacks = self.inner.callbacks.lock().unwrap_or_else(PoisonError::into_inner);
60            std::mem::take(&mut callbacks.entries)
61        };
62        for (_, callback) in entries {
63            callback();
64        }
65    }
66}
67
68/// Propagates notification that operations should be canceled.
69#[derive(Clone, Default)]
70pub struct CancellationToken {
71    inner: Option<Arc<Inner>>,
72}
73
74impl CancellationToken {
75    /// A token that can never be canceled.
76    pub const fn none() -> Self {
77        Self { inner: None }
78    }
79
80    /// Whether this token is capable of being in the canceled state.
81    pub fn can_be_canceled(&self) -> bool {
82        self.inner.is_some()
83    }
84
85    pub fn is_cancellation_requested(&self) -> bool {
86        self.inner.as_ref().is_some_and(|inner| inner.canceled.load(Ordering::SeqCst))
87    }
88
89    /// Registers a callback that runs when the token is canceled. When the
90    /// token is already canceled the callback runs immediately.
91    pub fn register(&self, callback: impl FnOnce() + Send + 'static) -> CancellationTokenRegistration {
92        let Some(inner) = &self.inner else {
93            return CancellationTokenRegistration { inner: None, id: 0 };
94        };
95        {
96            let mut callbacks = inner.callbacks.lock().unwrap_or_else(PoisonError::into_inner);
97            // Checked under the lock so that a concurrent `cancel` either
98            // sees the new entry or has already flipped the flag.
99            if !inner.canceled.load(Ordering::SeqCst) {
100                callbacks.next_id += 1;
101                let id = callbacks.next_id;
102                callbacks.entries.push((id, Box::new(callback)));
103                return CancellationTokenRegistration { inner: Some(inner.clone()), id };
104            }
105        }
106        callback();
107        CancellationTokenRegistration { inner: None, id: 0 }
108    }
109}
110
111impl CancellationToken {
112    /// Registers a callback that runs on the current thread when the token is canceled: at once
113    /// when it is canceled on this thread, or already canceled; through the poster of this thread,
114    /// or its queue of [`run_pending_wakes`](crate::threading::tasks::run_pending_wakes), when it is
115    /// canceled on another thread. The callback need not be `Send`.
116    pub fn register_local(&self, callback: impl FnOnce() + 'static) -> LocalCancellationTokenRegistration {
117        let local = super::tasks::local_executor::LocalCallback::register(Box::new(callback));
118        let handle = local.handle();
119        let registration = self.register(move || handle.run());
120        LocalCancellationTokenRegistration { registration, local }
121    }
122}
123
124/// A callback registered with [`CancellationToken::register_local`]; it belongs to the thread
125/// that registered it.
126pub struct LocalCancellationTokenRegistration {
127    registration: CancellationTokenRegistration,
128    local: super::tasks::local_executor::LocalCallback,
129}
130
131impl LocalCancellationTokenRegistration {
132    /// Removes the callback so that it no longer runs on cancellation.
133    pub fn dispose(&self) {
134        self.registration.dispose();
135        self.local.remove();
136    }
137}
138
139/// A callback registered with a [`CancellationToken`].
140pub struct CancellationTokenRegistration {
141    inner: Option<Arc<Inner>>,
142    id: u64,
143}
144
145impl CancellationTokenRegistration {
146    /// Removes the callback so that it no longer runs on cancellation.
147    pub fn dispose(&self) {
148        if let Some(inner) = &self.inner {
149            let removed = {
150                let mut callbacks = inner.callbacks.lock().unwrap_or_else(PoisonError::into_inner);
151                callbacks.entries.iter().position(|(id, _)| *id == self.id).map(|i| callbacks.entries.remove(i))
152            };
153            drop(removed);
154        }
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161    use std::sync::atomic::AtomicUsize;
162
163    #[test]
164    fn none_cannot_be_canceled() {
165        let token = CancellationToken::none();
166        assert!(!token.can_be_canceled());
167        assert!(!token.is_cancellation_requested());
168    }
169
170    #[test]
171    fn cancel_runs_callbacks_once() {
172        let count = Arc::new(AtomicUsize::new(0));
173        let source = CancellationTokenSource::new();
174        let token = source.token();
175        let c = count.clone();
176        token.register(move || {
177            c.fetch_add(1, Ordering::SeqCst);
178        });
179        let c = count.clone();
180        let removed = token.register(move || {
181            c.fetch_add(10, Ordering::SeqCst);
182        });
183        removed.dispose();
184        source.cancel();
185        source.cancel();
186        assert!(token.is_cancellation_requested());
187        assert_eq!(count.load(Ordering::SeqCst), 1);
188
189        let c = count.clone();
190        token.register(move || {
191            c.fetch_add(100, Ordering::SeqCst);
192        });
193        assert_eq!(count.load(Ordering::SeqCst), 101);
194    }
195}