Skip to main content

bevy_tasks/
single_threaded_task_pool.rs

1use alloc::{string::String, vec::Vec};
2use bevy_platform::sync::Arc;
3use core::{cell::{RefCell, Cell}, future::Future, marker::PhantomData, mem};
4
5use crate::executor::LocalExecutor;
6use crate::{block_on, Task};
7
8crate::cfg::std! {
9    if {
10        use std::thread_local;
11
12        use crate::executor::LocalExecutor as Executor;
13
14        thread_local! {
15            static LOCAL_EXECUTOR: Executor<'static> = const { Executor::new() };
16        }
17    } else {
18
19        // Because we do not have thread-locals without std, we cannot use LocalExecutor here.
20        use crate::executor::Executor;
21
22        static LOCAL_EXECUTOR: Executor<'static> = const { Executor::new() };
23    }
24}
25
26/// Used to create a [`TaskPool`].
27#[derive(Debug, Default, Clone)]
28pub struct TaskPoolBuilder {}
29
30/// This is a dummy struct for wasm support to provide the same api as with the multithreaded
31/// task pool. In the case of the multithreaded task pool this struct is used to spawn
32/// tasks on a specific thread. But the wasm task pool just calls
33/// `wasm_bindgen_futures::spawn_local` for spawning which just runs tasks on the main thread
34/// and so the [`ThreadExecutor`] does nothing.
35#[derive(Default)]
36pub struct ThreadExecutor<'a>(PhantomData<&'a ()>);
37impl<'a> ThreadExecutor<'a> {
38    /// Creates a new `ThreadExecutor`
39    pub fn new() -> Self {
40        Self::default()
41    }
42}
43
44impl TaskPoolBuilder {
45    /// Creates a new `TaskPoolBuilder` instance
46    pub fn new() -> Self {
47        Self::default()
48    }
49
50    /// No op on the single threaded task pool
51    pub fn num_threads(self, _num_threads: usize) -> Self {
52        self
53    }
54
55    /// No op on the single threaded task pool
56    pub fn stack_size(self, _stack_size: usize) -> Self {
57        self
58    }
59
60    /// No op on the single threaded task pool
61    pub fn thread_name(self, _thread_name: String) -> Self {
62        self
63    }
64
65    /// No op on the single threaded task pool
66    pub fn on_thread_spawn(self, _f: impl Fn() + Send + Sync + 'static) -> Self {
67        self
68    }
69
70    /// No op on the single threaded task pool
71    pub fn on_thread_destroy(self, _f: impl Fn() + Send + Sync + 'static) -> Self {
72        self
73    }
74
75    /// Creates a new [`TaskPool`]
76    pub fn build(self) -> TaskPool {
77        TaskPool::new_internal()
78    }
79}
80
81/// A thread pool for executing tasks. Tasks are futures that are being automatically driven by
82/// the pool on threads owned by the pool. In this case - main thread only.
83#[derive(Debug, Default, Clone)]
84pub struct TaskPool {}
85
86impl TaskPool {
87    /// Just create a new `ThreadExecutor` for wasm
88    pub fn get_thread_executor() -> Arc<ThreadExecutor<'static>> {
89        Arc::new(ThreadExecutor::new())
90    }
91
92    /// Create a `TaskPool` with the default configuration.
93    pub fn new() -> Self {
94        TaskPoolBuilder::new().build()
95    }
96
97    fn new_internal() -> Self {
98        Self {}
99    }
100
101    /// Return the number of threads owned by the task pool
102    pub fn thread_num(&self) -> usize {
103        1
104    }
105
106    /// Allows spawning non-`'static` futures on the thread pool. The function takes a callback,
107    /// passing a scope object into it. The scope object provided to the callback can be used
108    /// to spawn tasks. This function will await the completion of all tasks before returning.
109    ///
110    /// This is similar to `rayon::scope` and `crossbeam::scope`
111    pub fn scope<'env, F, T>(&self, f: F) -> Vec<T>
112    where
113        F: for<'scope> FnOnce(&'scope mut Scope<'scope, 'env, T>),
114        T: Send + 'static,
115    {
116        self.scope_with_executor(false, None, f)
117    }
118
119    /// Allows spawning non-`'static` futures on the thread pool. The function takes a callback,
120    /// passing a scope object into it. The scope object provided to the callback can be used
121    /// to spawn tasks. This function will await the completion of all tasks before returning.
122    ///
123    /// This is similar to `rayon::scope` and `crossbeam::scope`
124    #[expect(unsafe_code, reason = "Required to transmute lifetimes.")]
125    pub fn scope_with_executor<'env, F, T>(
126        &self,
127        _tick_task_pool_executor: bool,
128        _thread_executor: Option<&ThreadExecutor>,
129        f: F,
130    ) -> Vec<T>
131    where
132        F: for<'scope> FnOnce(&'scope mut Scope<'scope, 'env, T>),
133        T: Send + 'static,
134    {
135        // SAFETY: This safety comment applies to all references transmuted to 'env.
136        // Any futures spawned with these references need to return before this function completes.
137        // This is guaranteed because we drive all the futures spawned onto the Scope
138        // to completion in this function. However, rust has no way of knowing this so we
139        // transmute the lifetimes to 'env here to appease the compiler as it is unable to validate safety.
140        // Any usages of the references passed into `Scope` must be accessed through
141        // the transmuted reference for the rest of this function.
142
143        let executor = LocalExecutor::new();
144        // SAFETY: As above, all futures must complete in this function so we can change the lifetime
145        let executor_ref: &'env LocalExecutor<'env> = unsafe { mem::transmute(&executor) };
146
147        let results: RefCell<Vec<Option<T>>> = RefCell::new(Vec::new());
148        // SAFETY: As above, all futures must complete in this function so we can change the lifetime
149        let results_ref: &'env RefCell<Vec<Option<T>>> = unsafe { mem::transmute(&results) };
150
151        let pending_tasks: Cell<usize> = Cell::new(0);
152        // SAFETY: As above, all futures must complete in this function so we can change the lifetime
153        let pending_tasks: &'env Cell<usize> = unsafe { mem::transmute(&pending_tasks) };
154
155        let mut scope = Scope {
156            executor_ref,
157            pending_tasks,
158            results_ref,
159            scope: PhantomData,
160            env: PhantomData,
161        };
162
163        // SAFETY: As above, all futures must complete in this function so we can change the lifetime
164        let scope_ref: &'env mut Scope<'_, 'env, T> = unsafe { mem::transmute(&mut scope) };
165
166        f(scope_ref);
167
168        // Wait until the scope is complete
169        block_on(executor.run(async {
170            while pending_tasks.get() != 0 {
171                futures_lite::future::yield_now().await;
172            }
173        }));
174
175        results
176            .take()
177            .into_iter()
178            .map(|result| result.unwrap())
179            .collect()
180    }
181
182    /// Spawns a static future onto the thread pool. The returned Task is a future, which can be polled
183    /// to retrieve the output of the original future. Dropping the task will attempt to cancel it.
184    /// It can also be "detached", allowing it to continue running without having to be polled by the
185    /// end-user.
186    ///
187    /// If the provided future is non-`Send`, [`TaskPool::spawn_local`] should be used instead.
188    pub fn spawn<T>(
189        &self,
190        future: impl Future<Output = T> + 'static + MaybeSend + MaybeSync,
191    ) -> Task<T>
192    where
193        T: 'static + MaybeSend + MaybeSync,
194    {
195        crate::cfg::switch! {{
196            crate::cfg::web => {
197                web_task::spawn_local(future)
198            }
199            crate::cfg::std => {
200                LOCAL_EXECUTOR.with(|executor| {
201                    let task = executor.spawn(future);
202                    // Loop until all tasks are done
203                    while executor.try_tick() {}
204                    task
205                })
206            }
207            _ => {
208                let task = LOCAL_EXECUTOR.spawn(future);
209                // Loop until all tasks are done
210                while LOCAL_EXECUTOR.try_tick() {}
211                task
212            }
213        }}
214    }
215
216    /// Spawns a static future on the JS event loop. This is exactly the same as [`TaskPool::spawn`].
217    pub fn spawn_local<T>(
218        &self,
219        future: impl Future<Output = T> + 'static + MaybeSend + MaybeSync,
220    ) -> Task<T>
221    where
222        T: 'static + MaybeSend + MaybeSync,
223    {
224        self.spawn(future)
225    }
226
227    /// Runs a function with the local executor. Typically used to tick
228    /// the local executor on the main thread as it needs to share time with
229    /// other things.
230    ///
231    /// ```
232    /// use bevy_tasks::TaskPool;
233    ///
234    /// TaskPool::new().with_local_executor(|local_executor| {
235    ///     local_executor.try_tick();
236    /// });
237    /// ```
238    pub fn with_local_executor<F, R>(&self, f: F) -> R
239    where
240        F: FnOnce(&Executor) -> R,
241    {
242        crate::cfg::switch! {{
243            crate::cfg::std => {
244                LOCAL_EXECUTOR.with(f)
245            }
246            _ => {
247                f(&LOCAL_EXECUTOR)
248            }
249        }}
250    }
251}
252
253/// A `TaskPool` scope for running one or more non-`'static` futures.
254///
255/// For more information, see [`TaskPool::scope`].
256#[derive(Debug)]
257pub struct Scope<'scope, 'env: 'scope, T> {
258    executor_ref: &'scope LocalExecutor<'scope>,
259    // The number of pending tasks spawned on the scope
260    pending_tasks: &'scope Cell<usize>,
261    // Vector to gather results of all futures spawned during scope run
262    results_ref: &'env RefCell<Vec<Option<T>>>,
263
264    // make `Scope` invariant over 'scope and 'env
265    scope: PhantomData<&'scope mut &'scope ()>,
266    env: PhantomData<&'env mut &'env ()>,
267}
268
269impl<'scope, 'env, T: Send + 'env> Scope<'scope, 'env, T> {
270    /// Spawns a scoped future onto the executor. The scope *must* outlive
271    /// the provided future. The results of the future will be returned as a part of
272    /// [`TaskPool::scope`]'s return value.
273    ///
274    /// On the single threaded task pool, it just calls [`Scope::spawn_on_scope`].
275    ///
276    /// For more information, see [`TaskPool::scope`].
277    pub fn spawn<Fut: Future<Output = T> + 'scope + MaybeSend>(&self, f: Fut) {
278        self.spawn_on_scope(f);
279    }
280
281    /// Spawns a scoped future onto the executor. The scope *must* outlive
282    /// the provided future. The results of the future will be returned as a part of
283    /// [`TaskPool::scope`]'s return value.
284    ///
285    /// On the single threaded task pool, it just calls [`Scope::spawn_on_scope`].
286    ///
287    /// For more information, see [`TaskPool::scope`].
288    pub fn spawn_on_external<Fut: Future<Output = T> + 'scope + MaybeSend>(&self, f: Fut) {
289        self.spawn_on_scope(f);
290    }
291
292    /// Spawns a scoped future that runs on the thread the scope called from. The
293    /// scope *must* outlive the provided future. The results of the future will be
294    /// returned as a part of [`TaskPool::scope`]'s return value.
295    ///
296    /// For more information, see [`TaskPool::scope`].
297    pub fn spawn_on_scope<Fut: Future<Output = T> + 'scope + MaybeSend>(&self, f: Fut) {
298        // increment the number of pending tasks
299        let pending_tasks = self.pending_tasks;
300        pending_tasks.update(|i| i + 1);
301
302        // add a spot to keep the result, and record the index
303        let results_ref = self.results_ref;
304        let mut results = results_ref.borrow_mut();
305        let task_number = results.len();
306        results.push(None);
307        drop(results);
308
309        // create the job closure
310        let f = async move {
311            let result = f.await;
312
313            // store the result in the allocated slot
314            let mut results = results_ref.borrow_mut();
315            results[task_number] = Some(result);
316            drop(results);
317
318            // decrement the pending tasks count
319            pending_tasks.update(|i| i - 1);
320        };
321
322        // spawn the job itself
323        self.executor_ref.spawn(f).detach();
324    }
325}
326
327crate::cfg::std! {
328    if {
329        pub trait MaybeSend {}
330        impl<T> MaybeSend for T {}
331
332        pub trait MaybeSync {}
333        impl<T> MaybeSync for T {}
334    } else {
335        pub trait MaybeSend: Send {}
336        impl<T: Send> MaybeSend for T {}
337
338        pub trait MaybeSync: Sync {}
339        impl<T: Sync> MaybeSync for T {}
340    }
341}
342
343#[cfg(test)]
344mod test {
345    use std::{time, thread};
346
347    use super::*;
348
349    /// This test creates a scope with a single task that goes to sleep for a
350    /// nontrivial amount of time. At one point, the scope would (incorrectly)
351    /// return early under these conditions, causing a crash.
352    ///
353    /// The correct behavior is for the scope to block until the receiver is
354    /// woken by the external thread.
355    #[test]
356    fn scoped_spawn() {
357        let (sender, receiver) = async_channel::unbounded();
358        let task_pool = TaskPool {};
359        let thread = thread::spawn(move || {
360            let duration = time::Duration::from_millis(50);
361            thread::sleep(duration);
362            let _ = sender.send(0);
363        });
364        task_pool.scope(|scope| {
365            scope.spawn(async {
366                receiver.recv().await
367            });
368        });
369    }
370}