Skip to main content

spawned_rt/threads/
mod.rs

1//! IO-threads based module to support shared behavior with task based version.
2
3pub mod mpsc;
4pub mod oneshot;
5
6// This cfg is only for cancellation models: instrumented tokens must be
7// constructed inside loom::model, not in ordinary actor or signal tests.
8#[cfg(spawned_loom)]
9use loom::sync::{
10    atomic::{AtomicBool, Ordering},
11    Arc, Mutex,
12};
13#[cfg(not(spawned_loom))]
14use std::sync::{
15    atomic::{AtomicBool, Ordering},
16    Arc, Mutex,
17};
18use std::sync::{mpsc as std_mpsc, OnceLock};
19pub use std::{
20    future::Future,
21    thread::{sleep, spawn, JoinHandle},
22};
23
24#[cfg(feature = "async")]
25use crate::tasks::Runtime;
26use crate::tracing::init_tracing;
27
28/// Global list of Ctrl+C subscribers
29static CTRL_C_SUBSCRIBERS: OnceLock<Mutex<Vec<std_mpsc::Sender<()>>>> = OnceLock::new();
30
31/// Initialize tracing and run the given function.
32pub fn run(f: fn()) {
33    init_tracing();
34
35    f()
36}
37
38/// Block on a future using an isolated Tokio runtime with one async worker.
39///
40/// The future is driven on the calling thread. Spawned async helpers use one
41/// additional worker, so they can progress while that thread blocks. Tokio may
42/// create further threads for `spawn_blocking`, filesystem or DNS operations.
43/// The worker count is explicit and independent of `TOKIO_WORKER_THREADS`.
44#[cfg(feature = "async")]
45pub fn block_on<F: Future>(future: F) -> F::Output {
46    #[cfg(feature = "backend-experiments")]
47    if std::env::var("SPAWNED_EXPERIMENT_RUNTIME").as_deref() == Ok("managed") {
48        // Experiment-only bounded shared pool; process lifetime, no caller dependency.
49        static POOL: OnceLock<Runtime> = OnceLock::new();
50        return POOL
51            .get_or_init(|| {
52                tokio::runtime::Builder::new_multi_thread()
53                    .worker_threads(2)
54                    .enable_all()
55                    .build()
56                    .unwrap()
57            })
58            .block_on(future);
59    }
60    #[cfg(feature = "backend-experiments")]
61    let rt = match std::env::var("SPAWNED_EXPERIMENT_RUNTIME").as_deref() {
62        Ok("current") => tokio::runtime::Builder::new_current_thread()
63            .enable_all()
64            .build()
65            .unwrap(),
66        Ok("one-worker") => tokio::runtime::Builder::new_multi_thread()
67            .worker_threads(1)
68            .enable_all()
69            .build()
70            .unwrap(),
71        _ => isolated_runtime(),
72    };
73    #[cfg(not(feature = "backend-experiments"))]
74    let rt = isolated_runtime();
75    rt.block_on(future)
76}
77
78// Keep a multithread runtime for block_in_place and independently scheduled
79// helpers, but never allocate a machine-sized worker pool for each actor.
80#[cfg(feature = "async")]
81fn isolated_runtime() -> Runtime {
82    tokio::runtime::Builder::new_multi_thread()
83        .worker_threads(1)
84        .enable_all()
85        .build()
86        .unwrap()
87}
88
89/// Spawn blocking is the same as spawn for pure threaded usage.
90pub fn spawn_blocking<F, R>(f: F) -> JoinHandle<R>
91where
92    F: FnOnce() -> R + Send + 'static,
93    R: Send + 'static,
94{
95    spawn(f)
96}
97
98type CancelCallback = Box<dyn FnOnce() + Send>;
99
100/// A token that can be used to signal cancellation.
101///
102/// Supports registering callbacks via `on_cancel()` that fire when
103/// the token is cancelled, enabling efficient waiting patterns.
104#[derive(Clone)]
105pub struct CancellationToken {
106    is_cancelled: Arc<AtomicBool>,
107    callbacks: Arc<Mutex<Vec<CancelCallback>>>,
108}
109
110impl std::fmt::Debug for CancellationToken {
111    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112        f.debug_struct("CancellationToken")
113            .field("is_cancelled", &self.is_cancelled())
114            .finish()
115    }
116}
117
118impl Default for CancellationToken {
119    fn default() -> Self {
120        Self::new()
121    }
122}
123
124impl CancellationToken {
125    pub fn new() -> Self {
126        CancellationToken {
127            is_cancelled: Arc::new(false.into()),
128            callbacks: Arc::new(Mutex::new(Vec::new())),
129        }
130    }
131
132    pub fn is_cancelled(&self) -> bool {
133        self.is_cancelled.load(Ordering::SeqCst)
134    }
135
136    pub fn cancel(&self) {
137        self.is_cancelled.store(true, Ordering::SeqCst);
138        // Fire all registered callbacks
139        let callbacks: Vec<_> = self
140            .callbacks
141            .lock()
142            .unwrap_or_else(|e| e.into_inner())
143            .drain(..)
144            .collect();
145        for cb in callbacks {
146            cb();
147        }
148    }
149
150    /// Register a callback to be invoked when this token is cancelled.
151    /// If already cancelled, the callback fires immediately.
152    ///
153    /// This method is thread-safe: the callback is guaranteed to fire exactly
154    /// once, either immediately (if already cancelled) or when `cancel()` is called.
155    pub fn on_cancel(&self, callback: CancelCallback) {
156        // Hold the lock while checking is_cancelled to avoid a race with cancel().
157        // cancel() sets the flag BEFORE acquiring the lock, so if we see
158        // is_cancelled=false while holding the lock, cancel() hasn't drained
159        // callbacks yet and will drain ours after we release the lock.
160        let mut callbacks = self.callbacks.lock().unwrap_or_else(|e| e.into_inner());
161        if self.is_cancelled() {
162            drop(callbacks);
163            callback();
164        } else {
165            callbacks.push(callback);
166        }
167    }
168}
169
170/// Returns a closure that blocks until Ctrl+C is received.
171///
172/// Multiple calls to this function are supported - each returns a closure that
173/// will be notified when Ctrl+C is pressed. This allows multiple actors to
174/// react to the same signal.
175///
176/// The signal handler is registered on the first call. Subsequent calls simply
177/// add new subscribers to the broadcast list.
178///
179/// # Example
180///
181/// ```ignore
182/// // Both actors will be notified on Ctrl+C
183/// send_message_on(actor1.clone(), rt::ctrl_c(), Msg::Shutdown);
184/// send_message_on(actor2.clone(), rt::ctrl_c(), Msg::Shutdown);
185/// ```
186pub fn ctrl_c() -> impl FnOnce() + Send + 'static {
187    // Initialize subscribers list and register handler on first call
188    let subscribers = CTRL_C_SUBSCRIBERS.get_or_init(|| {
189        ctrlc::set_handler(|| {
190            if let Some(subs) = CTRL_C_SUBSCRIBERS.get() {
191                let mut guard = subs.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
192                // Notify all subscribers and remove dead ones (where receiver was dropped)
193                guard.retain(|tx| tx.send(()).is_ok());
194            }
195        })
196        .expect("Ctrl+C handler already set. Use ctrl_c() instead of ctrlc::set_handler()");
197        Mutex::new(Vec::new())
198    });
199
200    // Create a new subscriber channel
201    let (tx, rx) = std_mpsc::channel();
202    subscribers
203        .lock()
204        .unwrap_or_else(|poisoned| poisoned.into_inner())
205        .push(tx);
206
207    move || {
208        let _ = rx.recv();
209    }
210}
211
212/// Park the caller while polling an executor-independent future. Thread-only
213/// builds have no Tokio reactor, timer driver or implicit helper workers.
214#[cfg(not(feature = "async"))]
215pub fn block_on<F: Future>(future: F) -> F::Output {
216    use std::task::{Context, Poll, Wake, Waker};
217    struct Parker {
218        notified: std::sync::Mutex<bool>,
219        signal: std::sync::Condvar,
220    }
221    impl Parker {
222        fn notify(&self) {
223            *self.notified.lock().unwrap_or_else(|p| p.into_inner()) = true;
224            self.signal.notify_one();
225        }
226        fn wait(&self) {
227            let mut notified = self.notified.lock().unwrap_or_else(|p| p.into_inner());
228            while !*notified {
229                notified = self
230                    .signal
231                    .wait(notified)
232                    .unwrap_or_else(|p| p.into_inner());
233            }
234            *notified = false;
235        }
236    }
237    impl Wake for Parker {
238        fn wake(self: std::sync::Arc<Self>) {
239            self.notify();
240        }
241        fn wake_by_ref(self: &std::sync::Arc<Self>) {
242            self.notify();
243        }
244    }
245    // Each invocation owns its wake permit. Nested blocking waits must not
246    // consume a wake intended for their caller's still-pending future.
247    let parker = std::sync::Arc::new(Parker {
248        notified: std::sync::Mutex::new(false),
249        signal: std::sync::Condvar::new(),
250    });
251    let waker = Waker::from(parker.clone());
252    let mut context = Context::from_waker(&waker);
253    let mut future = std::pin::pin!(future);
254    loop {
255        match future.as_mut().poll(&mut context) {
256            Poll::Ready(value) => return value,
257            Poll::Pending => parker.wait(),
258        }
259    }
260}
261
262#[cfg(all(test, not(feature = "async")))]
263mod parking_tests {
264    #[test]
265    fn nested_block_on_does_not_consume_the_outer_futures_wakeup() {
266        let (tx, rx) = std::sync::mpsc::channel();
267        let worker = std::thread::spawn(move || {
268            let mut outer_polled = false;
269            let value = super::block_on(std::future::poll_fn(|cx| {
270                if outer_polled {
271                    return std::task::Poll::Ready(42);
272                }
273                outer_polled = true;
274                cx.waker().wake_by_ref();
275                let mut inner_polled = false;
276                super::block_on(std::future::poll_fn(|inner| {
277                    if inner_polled {
278                        std::task::Poll::Ready(())
279                    } else {
280                        inner_polled = true;
281                        inner.waker().wake_by_ref();
282                        std::task::Poll::Pending
283                    }
284                }));
285                std::task::Poll::Pending
286            }));
287            tx.send(value).unwrap();
288        });
289        let result = rx.recv_timeout(std::time::Duration::from_secs(1));
290        if result.is_err() {
291            // Rescue the old shared-thread-token executor so a regression is a
292            // finite assertion failure rather than a hanging test process.
293            worker.thread().unpark();
294            let _ = rx.recv_timeout(std::time::Duration::from_secs(1));
295        }
296        let value = result.expect("inner executor consumed the outer wake");
297        worker.join().unwrap();
298        assert_eq!(value, 42);
299    }
300}