Skip to main content

cuckoo_runtime/
lib.rs

1//! A work-stealing runtime to run async CPU-heavy rust workloads inside multi-threaded applications.
2//!
3//! The main idea behind Cuckoo is that some systems already have opinionated threading models, and spinning up additional threads
4//! just to run some async code might cause increased core contention and hurt overall performance.
5//!
6//! Cuckoo is mostly intended to run async code in a block fashion (within `block_on` blocks), and while a thread is waiting on
7//! some future, it can take work from other threads and make some progress on them.
8//!
9//! ## Current Status
10//!
11//! Cuckoo is completely experimental, it might deadlock, cause unexpected behavior or is probably not very fast.
12
13use std::{
14    sync::Arc,
15    thread::{self, ThreadId},
16};
17
18use async_task::Runnable;
19use crossbeam::deque::{Injector, Stealer, Worker};
20use dashmap::DashMap;
21
22pub mod futures;
23mod join_handle;
24
25pub use join_handle::*;
26
27thread_local! {
28    static WORK_QUEUE: Worker<Runnable> = Worker::new_fifo();
29}
30
31/// The main runtime instance, holds all global state and thread-specific handles are created by it.
32pub struct Runtime {
33    injector: Arc<Injector<Runnable>>,
34    stealers: Arc<DashMap<ThreadId, Stealer<Runnable>>>,
35}
36
37/// A handle to the runtime that can be cloned and sent across threads.
38///
39/// Right now the underlying work queue is thread-local, overtime there might also be a thread-local variant of the handel that can't be sent to other threads.
40#[derive(Clone)]
41pub struct Handle {
42    injector: Arc<Injector<Runnable>>,
43    stealers: Arc<DashMap<ThreadId, Stealer<Runnable>>>,
44}
45
46impl Runtime {
47    /// Creates a new runtime with a fixed number of background threads.
48    ///
49    /// Background threads execute tasks that are spawned in the background, allowing foreground threads to keep doing other work.
50    pub fn new(background_threads: usize) -> Self {
51        let injector: Arc<Injector<Runnable>> = Arc::new(Injector::new());
52        for idx in 0..background_threads {
53            thread::Builder::new()
54                .name(format!("cuckoo-background-thread-{idx}"))
55                .spawn({
56                    let injector = Arc::clone(&injector);
57                    move || {
58                        loop {
59                            if let Some(task) = injector.steal().success() {
60                                task.run();
61                            }
62                        }
63                    }
64                })
65                .expect("Failed to spawn background thread");
66        }
67        Self {
68            injector,
69            stealers: Arc::new(DashMap::new()),
70        }
71    }
72
73    pub fn handle(&self) -> Handle {
74        Handle {
75            injector: Arc::clone(&self.injector),
76            stealers: Arc::clone(&self.stealers),
77        }
78    }
79
80    /// Spawn a future to run in the background. The future can make progress by any participating thread.
81    ///
82    /// If the runtime doesn't have any background threads, the [`JoinHandle`] should be polled/awaited explicitly.
83    pub fn spawn<F>(&self, future: F) -> JoinHandle<F::Output>
84    where
85        F: Future + Send + 'static,
86        F::Output: Send + Sync,
87    {
88        let injector = Arc::clone(&self.injector);
89        let schedule = move |runnable| injector.push(runnable);
90        let (runnable, task) = async_task::spawn(future, schedule);
91        runnable.schedule();
92        JoinHandle { task: Some(task) }
93    }
94
95    /// Run a future, blocking the current thread until its done.
96    ///
97    /// The future might end up running on a different thread, and this thread might make progress on other futures before returning.
98    pub fn block_on<F>(&self, future: F) -> F::Output
99    where
100        F: Future + Send + 'static,
101        F::Output: Send + Sync,
102    {
103        self.handle().block_on(future)
104    }
105}
106
107impl Handle {
108    /// Run a future, blocking the current thread until its done.
109    ///
110    /// The future might end up running on a different thread, and this thread might make progress on other futures before returning.
111    pub fn block_on<F>(&self, future: F) -> F::Output
112    where
113        F: Future + Send + 'static,
114        F::Output: Send + Sync,
115    {
116        let schedule = {
117            let stealers = Arc::clone(&self.stealers);
118            move |runnable| {
119                WORK_QUEUE.with(|q| {
120                    let current_thread_id = thread::current().id();
121                    if !stealers.contains_key(&current_thread_id) {
122                        // Make sure this thread's stealer is populated
123                        stealers.entry(current_thread_id).or_insert(q.stealer());
124                    }
125
126                    q.push(runnable);
127                })
128            }
129        };
130        let (runnable, task) = async_task::spawn(future, schedule);
131        // I think we want to call `run` here to prioritize the existing task before we start stealing work from other threads.
132        runnable.run();
133
134        while !task.is_finished() {
135            if let Some(stolen_task) = self.find_task() {
136                stolen_task.run();
137            }
138        }
139
140        ::futures::executor::block_on(task)
141    }
142
143    /// Spawn a future to run in the background. The future can make progress by any participating thread.
144    ///
145    /// If the runtime doesn't have any background threads, the [`JoinHandle`] should be polled/awaited explicitly.
146    pub fn spawn<F>(&self, future: F) -> JoinHandle<F::Output>
147    where
148        F: Future + Send + 'static,
149        F::Output: Send + Sync,
150    {
151        let injector = Arc::clone(&self.injector);
152        let schedule = move |runnable| injector.push(runnable);
153        let (runnable, task) = async_task::spawn(future, schedule);
154        runnable.schedule();
155        JoinHandle { task: Some(task) }
156    }
157
158    fn find_task(&self) -> Option<Runnable> {
159        WORK_QUEUE.with(|local| {
160            // Pop a task from the local queue, if not empty.
161            local.pop().or_else(|| {
162                std::iter::repeat_with(|| {
163                    self.injector
164                        .steal_batch_with_limit_and_pop(local, 1)
165                        .or_else(|| self.stealers.iter().map(|s| s.steal()).collect())
166                })
167                .find(|s| !s.is_retry())
168                .and_then(|s| s.success())
169            })
170        })
171    }
172}
173
174#[cfg(test)]
175mod tests {
176
177    use rstest::rstest;
178
179    use super::*;
180
181    #[rstest]
182    #[case(0)]
183    #[case(1)]
184    fn test_basic(#[case] thread_count: usize) {
185        let rt = Runtime::new(thread_count);
186        let jh: JoinHandle<()> = rt.spawn(async move { println!("spawned task!") });
187        let handle = rt.handle();
188        let h2 = handle.clone();
189
190        handle.block_on(async move {
191            _ = h2.spawn(async move {
192                if thread_count == 0 {
193                    panic!("This should never run");
194                }
195            });
196            println!("blocking here!");
197            println!("Awaited the second future");
198        });
199        println!("Finished block_on block");
200        rt.block_on(jh);
201    }
202}