Skip to main content

subetha_cxc/
task_pool.rs

1//! `TaskPool`: a minimal bounded async executor, no external runtime.
2//!
3//! The substrate's rings are executor-agnostic (any `std::future`
4//! executor drives them). `TaskPool` is the proof that "any" includes
5//! "a tiny one we ship ourselves": a fixed pool of worker threads
6//! running an arbitrary number of suspended tasks. There is no tokio,
7//! no reactor, no per-task thread. A task that awaits a ring parks its
8//! `Waker` in the ring (see [`crate::waker_ring`]); the producer's push
9//! fires that `Waker`, which re-enqueues the task here, and a worker
10//! polls it. M threads, N tasks, with M fixed and N unbounded.
11//!
12//! The ready queue is a `Mutex<VecDeque>` + `Condvar` on purpose: the
13//! executor's own scheduling is not the thing under test, and a
14//! std-only queue keeps the dependency surface at zero.
15
16use std::collections::VecDeque;
17use std::future::Future;
18use std::pin::Pin;
19use std::sync::atomic::{AtomicBool, Ordering};
20use std::sync::{Arc, Condvar, Mutex};
21use std::task::{Context, Waker};
22use std::thread::JoinHandle;
23
24/// One scheduled unit of work. The future is `Option`-wrapped so a
25/// completed task drops its future and any later (spurious) wake that
26/// re-enqueues it is a no-op rather than a poll-after-ready contract
27/// violation.
28struct Task {
29    future: Mutex<Option<Pin<Box<dyn Future<Output = ()> + Send>>>>,
30    ready: Arc<ReadyQueue>,
31}
32
33impl std::task::Wake for Task {
34    fn wake(self: Arc<Self>) {
35        self.ready.clone().push(self);
36    }
37    fn wake_by_ref(self: &Arc<Self>) {
38        self.ready.clone().push(self.clone());
39    }
40}
41
42struct ReadyQueue {
43    queue: Mutex<VecDeque<Arc<Task>>>,
44    signal: Condvar,
45    shutdown: AtomicBool,
46}
47
48impl ReadyQueue {
49    fn push(&self, task: Arc<Task>) {
50        self.queue.lock().unwrap().push_back(task);
51        self.signal.notify_one();
52    }
53
54    /// Block until a task is available or shutdown is requested.
55    fn pop(&self) -> Option<Arc<Task>> {
56        let mut q = self.queue.lock().unwrap();
57        loop {
58            if let Some(task) = q.pop_front() {
59                return Some(task);
60            }
61            if self.shutdown.load(Ordering::Acquire) {
62                return None;
63            }
64            q = self.signal.wait(q).unwrap();
65        }
66    }
67}
68
69/// A fixed-size pool of worker threads driving an unbounded set of
70/// suspended tasks.
71pub struct TaskPool {
72    ready: Arc<ReadyQueue>,
73    workers: Vec<JoinHandle<()>>,
74}
75
76impl TaskPool {
77    /// Build a pool with `n_workers` threads (clamped to at least 1).
78    pub fn new(n_workers: usize) -> Self {
79        let n = n_workers.max(1);
80        let ready = Arc::new(ReadyQueue {
81            queue: Mutex::new(VecDeque::new()),
82            signal: Condvar::new(),
83            shutdown: AtomicBool::new(false),
84        });
85        let workers = (0..n)
86            .map(|_| {
87                let ready = Arc::clone(&ready);
88                std::thread::spawn(move || worker_loop(ready))
89            })
90            .collect();
91        Self { ready, workers }
92    }
93
94    /// Number of worker threads in the pool.
95    pub fn worker_count(&self) -> usize {
96        self.workers.len()
97    }
98
99    /// Spawn a future. It runs to completion on the pool, suspending
100    /// (off-thread) whenever it awaits, with no thread dedicated to it.
101    pub fn spawn(&self, future: impl Future<Output = ()> + Send + 'static) {
102        let task = Arc::new(Task {
103            future: Mutex::new(Some(Box::pin(future))),
104            ready: Arc::clone(&self.ready),
105        });
106        self.ready.push(task);
107    }
108
109    /// Stop the workers once the current ready queue drains. Joins all
110    /// threads. Call after the work you spawned has completed.
111    ///
112    /// The flag is set while holding the queue lock, which is what
113    /// makes a worker's check-then-park atomic against it: a worker
114    /// between the two holds that lock, so this store waits for it to
115    /// park before the notify goes out.
116    pub fn shutdown(self) {
117        {
118            let _q = self.ready.queue.lock().unwrap();
119            self.ready.shutdown.store(true, Ordering::Release);
120        }
121        self.ready.signal.notify_all();
122        for w in self.workers {
123            w.join().ok();
124        }
125    }
126}
127
128fn worker_loop(ready: Arc<ReadyQueue>) {
129    while let Some(task) = ready.pop() {
130        let mut guard = task.future.lock().unwrap();
131        if let Some(fut) = guard.as_mut() {
132            let waker = Waker::from(Arc::clone(&task));
133            let mut cx = Context::from_waker(&waker);
134            if fut.as_mut().poll(&mut cx).is_ready() {
135                // Done: drop the future so a later spurious wake that
136                // re-enqueues this task finds `None` and skips it.
137                *guard = None;
138            }
139        }
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146    use std::sync::atomic::AtomicU64;
147
148    #[test]
149    fn runs_many_tasks_on_few_threads_with_yields() {
150        // Each task yields once (returns Pending then re-wakes itself),
151        // so the pool must round-trip them through the ready queue.
152        let pool = TaskPool::new(2);
153        let done = Arc::new(AtomicU64::new(0));
154        let n = 5_000u64;
155        for _ in 0..n {
156            let done = Arc::clone(&done);
157            pool.spawn(async move {
158                YieldOnce::default().await;
159                done.fetch_add(1, Ordering::AcqRel);
160            });
161        }
162        // Spin until all complete (a real executor would join handles;
163        // this test just watches the shared counter).
164        let start = std::time::Instant::now();
165        while done.load(Ordering::Acquire) < n {
166            if start.elapsed() > std::time::Duration::from_secs(10) {
167                panic!("only {} of {n} tasks finished", done.load(Ordering::Acquire));
168            }
169            std::hint::spin_loop();
170        }
171        assert_eq!(pool.worker_count(), 2);
172        pool.shutdown();
173    }
174
175    #[derive(Default)]
176    struct YieldOnce {
177        yielded: bool,
178    }
179    impl Future for YieldOnce {
180        type Output = ();
181        fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> std::task::Poll<()> {
182            if self.yielded {
183                std::task::Poll::Ready(())
184            } else {
185                self.yielded = true;
186                cx.waker().wake_by_ref();
187                std::task::Poll::Pending
188            }
189        }
190    }
191}