1- //! Experimental drop-in replacement for [`std::thread::scope`] for WebAssembly .
1+ //! Experimental replacement for [`std::thread::scope`] using a fixed worker pool .
22//!
3- //! Uses a pool of web worker threads spawned by the host JS environment to run scoped tasks.
3+ //! *Scoped tasks* are similar to *scoped threads* but run on an existing thread pool instead of
4+ //! spawning dedicated threads.
5+ //!
6+ //! # WebAssembly support
7+ //!
8+ //! This module was originally designed for WebAssembly, where it can use a pool of
9+ //! [web worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) threads spawned
10+ //! by the host JS environment to run scoped tasks.
411//!
512//! Requires the `atomics`, `bulk-memory` and `mutable-globals` target features to be enabled, and
613//! for all threads to be using web workers as `memory.atomic.wait` doesn't work on the main thread.
714//!
815//! Catching unwinding panics should be supported, but at the time of writing, the Rust standard
9- //! library doesn't support panic=unwind on WebAssembly.
16+ //! library doesn't support ` panic=unwind` on WebAssembly.
1017//!
1118//! # Examples
1219//!
1320//! ```
14- //! // Setup pool of workers. In WebAssembly, this would be done by spawning more web workers which
15- //! // then call the exported worker function.
21+ //! # use std::num::NonZero;
22+ //! # use std::sync::atomic::AtomicU32;
23+ //! # use std::sync::atomic::Ordering;
24+ //! # use utils::multithreading::scoped_tasks;
25+ //! // Setup pool of workers. In WebAssembly, where std::thread::spawn is not available, this would
26+ //! // be implemented by spawning more web workers which then call the exported worker function.
1627//! for _ in 0..std::thread::available_parallelism().map_or(4, NonZero::get) {
17- //! std::thread::spawn(scoped ::worker);
28+ //! std::thread::spawn(scoped_tasks ::worker);
1829//! }
1930//!
2031//! let data = vec![1, 2, 3];
2132//! let mut data2 = vec![10, 100, 1000];
2233//!
2334//! // Start scoped tasks which may run on other threads
24- //! scoped ::scope(|s| {
35+ //! scoped_tasks ::scope(|s| {
2536//! s.spawn(|| {
2637//! println!("[task 1] data={:?}", data);
2738//! });
4253//!
4354//! // Start another set of scoped tasks
4455//! let counter = AtomicU32::new(0);
45- //! scoped ::scope(|s| {
56+ //! scoped_tasks ::scope(|s| {
4657//! let counter = &counter;
4758//! for t in 0..4 {
4859//! s.spawn(move || {
@@ -61,17 +72,14 @@ use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind};
6172use std:: sync:: mpsc:: { SyncSender , TrySendError } ;
6273use std:: sync:: { Arc , Condvar , Mutex } ;
6374
64- #[ cfg( not( all(
65- target_feature = "atomics" ,
66- target_feature = "bulk-memory" ,
67- target_feature = "mutable-globals" ,
68- ) ) ) ]
69- compile_error ! ( "Required target features not enabled" ) ;
70-
7175/// Create a scope for spawning scoped tasks.
7276///
7377/// Scoped tasks may borrow non-`static` data, and may run in parallel depending on thread pool
7478/// worker availability.
79+ ///
80+ /// All scoped tasks are automatically joined before this function returns.
81+ ///
82+ /// Designed to match the [`std::thread::scope`] API.
7583#[ inline( never) ]
7684pub fn scope < ' env , F , T > ( f : F ) -> T
7785where
@@ -103,6 +111,8 @@ where
103111
104112/// Scope to spawn tasks in.
105113///
114+ /// Designed to match the [`std::thread::Scope`] API.
115+ ///
106116/// # Lifetimes
107117///
108118/// The `'scope` lifetime represents the lifetime of the scope itself, starting when the closure
@@ -111,19 +121,36 @@ where
111121/// The `'env` lifetime represents the lifetime of the data borrowed by the scoped tasks, and must
112122/// outlive `'scope`.
113123#[ derive( Debug ) ]
124+ #[ expect( clippy:: struct_field_names) ]
114125pub struct Scope < ' scope , ' env : ' scope > {
115126 data : Arc < ScopeData > ,
116127 // &'scope mut &'scope is needed to prevent lifetimes from shrinking
117128 _scope : PhantomData < & ' scope mut & ' scope ( ) > ,
118129 _env : PhantomData < & ' env mut & ' env ( ) > ,
119130}
120131
121- impl < ' scope , ' env > Scope < ' scope , ' env > {
132+ impl < ' scope > Scope < ' scope , ' _ > {
122133 /// Spawn a new task within the scope.
123134 ///
124135 /// If no workers within the thread pool are available, the task will be executed on the current
125136 /// thread.
126137 pub fn spawn < F , T > ( & ' scope self , f : F ) -> ScopedJoinHandle < ' scope , T >
138+ where
139+ F : FnOnce ( ) -> T + Send + ' scope ,
140+ T : Send + ' scope ,
141+ {
142+ let ( closure, handle) = self . create_closure ( f) ;
143+ if let Err ( closure) = try_queue_task ( closure) {
144+ // Fall back to running the closure on this thread
145+ closure ( ) ;
146+ }
147+ handle
148+ }
149+
150+ fn create_closure < F , T > (
151+ & ' scope self ,
152+ f : F ,
153+ ) -> ( Box < dyn FnOnce ( ) + Send > , ScopedJoinHandle < ' scope , T > )
127154 where
128155 F : FnOnce ( ) -> T + Send + ' scope ,
129156 T : Send + ' scope ,
@@ -162,19 +189,15 @@ impl<'scope, 'env> Scope<'scope, 'env> {
162189 } ;
163190
164191 let scope_data = self . data . clone ( ) ;
165- scoped_task ( Box :: new (
166- #[ inline( never) ]
167- move || {
168- // Use a second closure to ensure that the closure which borrows from 'scope is
169- // dropped before `ScopeData::task_end` is called. This prevents `scope` from
170- // returning too soon, while the closures still exist, which causes UB as detected
171- // by Miri.
172- let panicked = closure ( ) ;
173- scope_data. task_end ( panicked) ;
174- } ,
175- ) ) ;
176-
177- handle
192+ let task_closure = Box :: new ( move || {
193+ // Use a second closure to ensure that the closure which borrows from 'scope is dropped
194+ // before `ScopeData::task_end` is called. This prevents `scope()` from returning while
195+ // the inner closure still exists, which causes UB as detected by Miri.
196+ let panicked = closure ( ) ;
197+ scope_data. task_end ( panicked) ;
198+ } ) ;
199+
200+ ( task_closure, handle)
178201 }
179202}
180203
@@ -210,6 +233,10 @@ impl ScopeData {
210233}
211234
212235/// Handle to block on a task's termination.
236+ ///
237+ /// Designed to match the [`std::thread::ScopedJoinHandle`] API, except
238+ /// [`std::thread::ScopedJoinHandle::thread`] is not supported as tasks are not run on dedicated
239+ /// threads.
213240#[ derive( Debug ) ]
214241pub struct ScopedJoinHandle < ' scope , T > {
215242 data : Arc < HandleData < T > > ,
@@ -222,10 +249,7 @@ struct HandleData<T> {
222249 condvar : Condvar ,
223250}
224251
225- impl < ' scope , T > ScopedJoinHandle < ' scope , T > {
226- // Unsupported
227- // pub fn thread(&self) -> &Thread {}
228-
252+ impl < T > ScopedJoinHandle < ' _ , T > {
229253 /// Wait for the task to finish.
230254 pub fn join ( self ) -> Result < T , Box < dyn Any + Send + ' static > > {
231255 let HandleData { mutex, condvar } = self . data . as_ref ( ) ;
@@ -246,7 +270,7 @@ impl<'scope, T> ScopedJoinHandle<'scope, T> {
246270#[ expect( clippy:: type_complexity) ]
247271static WORKERS : Mutex < VecDeque < SyncSender < Box < dyn FnOnce ( ) + Send > > > > = Mutex :: new ( VecDeque :: new ( ) ) ;
248272
249- fn scoped_task ( mut closure : Box < dyn FnOnce ( ) + Send > ) {
273+ fn try_queue_task ( mut closure : Box < dyn FnOnce ( ) + Send > ) -> Result < ( ) , Box < dyn FnOnce ( ) + Send > > {
250274 let mut guard = WORKERS . lock ( ) . unwrap ( ) ;
251275 let queue = & mut * guard;
252276
@@ -258,7 +282,7 @@ fn scoped_task(mut closure: Box<dyn FnOnce() + Send>) {
258282 match sender. try_send ( closure) {
259283 Ok ( ( ) ) => {
260284 queue. push_back ( sender) ;
261- return ;
285+ return Ok ( ( ) ) ;
262286 }
263287 Err ( TrySendError :: Full ( v) ) => {
264288 closure = v;
@@ -272,11 +296,12 @@ fn scoped_task(mut closure: Box<dyn FnOnce() + Send>) {
272296 }
273297 drop ( guard) ;
274298
275- // Fall back to run the closure on this thread
276- closure ( ) ;
299+ Err ( closure)
277300}
278301
279302/// Use this thread as a worker in the thread pool for scoped tasks.
303+ ///
304+ /// This function never returns.
280305pub fn worker ( ) {
281306 let ( tx, rx) = std:: sync:: mpsc:: sync_channel ( 0 ) ;
282307
0 commit comments