From 9e62075db8e6094e840a2572d443880e3a4c1060 Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Wed, 15 Feb 2023 15:10:09 -0800 Subject: [PATCH 1/6] Add a fallback when threading is unsupported --- rayon-core/src/lib.rs | 4 ++ rayon-core/src/registry.rs | 76 +++++++++++++++++++++++++++++--------- 2 files changed, 63 insertions(+), 17 deletions(-) diff --git a/rayon-core/src/lib.rs b/rayon-core/src/lib.rs index b31a2d7e0..d0d2ea406 100644 --- a/rayon-core/src/lib.rs +++ b/rayon-core/src/lib.rs @@ -707,6 +707,10 @@ impl ThreadPoolBuildError { fn new(kind: ErrorKind) -> ThreadPoolBuildError { ThreadPoolBuildError { kind } } + + fn is_unsupported(&self) -> bool { + matches!(&self.kind, ErrorKind::IOError(e) if e.kind() == io::ErrorKind::Unsupported) + } } const GLOBAL_POOL_ALREADY_INITIALIZED: &str = diff --git a/rayon-core/src/registry.rs b/rayon-core/src/registry.rs index 24c0855c6..cd021230f 100644 --- a/rayon-core/src/registry.rs +++ b/rayon-core/src/registry.rs @@ -50,7 +50,7 @@ impl ThreadBuilder { /// Executes the main loop for this thread. This will not return until the /// thread pool is dropped. pub fn run(self) { - unsafe { main_loop(self.worker, self.stealer, self.registry, self.index) } + unsafe { main_loop(self) } } } @@ -164,7 +164,7 @@ static THE_REGISTRY_SET: Once = Once::new(); /// initialization has not already occurred, use the default /// configuration. pub(super) fn global_registry() -> &'static Arc { - set_global_registry(|| Registry::new(ThreadPoolBuilder::new())) + set_global_registry(default_global_registry) .or_else(|err| unsafe { THE_REGISTRY.as_ref().ok_or(err) }) .expect("The global thread pool has not been initialized.") } @@ -198,6 +198,46 @@ where result } +fn default_global_registry() -> Result, ThreadPoolBuildError> { + let result = Registry::new(ThreadPoolBuilder::new()); + + // If we're running in an environment that doesn't support threads at all, we can fall back to + // using the current thread alone. This is crude, and probably won't work for non-blocking + // calls like `spawn` or `broadcast_spawn`, but a lot of stuff does work fine. + // + // Notably, this allows current WebAssembly targets to work even though their threading support + // is stubbed out, and we won't have to change anything if they do add real threading. + let unsupported = matches!(&result, Err(e) if e.is_unsupported()); + if unsupported && WorkerThread::current().is_null() { + let builder = ThreadPoolBuilder::new() + .num_threads(1) + .spawn_handler(|thread| { + // Rather than starting a new thread, we're just taking over the current thread + // *without* running the main loop, so we can still return from here. + // The WorkerThread is leaked, but we never shutdown the global pool anyway. + let worker_thread = Box::leak(Box::new(WorkerThread::from(thread))); + let registry = &*worker_thread.registry; + let index = worker_thread.index; + + unsafe { + WorkerThread::set_current(worker_thread); + + // let registry know we are ready to do work + Latch::set(®istry.thread_infos[index].primed); + } + + Ok(()) + }); + + let fallback_result = Registry::new(builder); + if fallback_result.is_ok() { + return fallback_result; + } + } + + result +} + struct Terminator<'a>(&'a Arc); impl<'a> Drop for Terminator<'a> { @@ -655,6 +695,19 @@ thread_local! { static WORKER_THREAD_STATE: Cell<*const WorkerThread> = Cell::new(ptr::null()); } +impl From for WorkerThread { + fn from(thread: ThreadBuilder) -> Self { + Self { + worker: thread.worker, + stealer: thread.stealer, + fifo: JobFifo::new(), + index: thread.index, + rng: XorShift64Star::new(), + registry: thread.registry, + } + } +} + impl Drop for WorkerThread { fn drop(&mut self) { // Undo `set_current` @@ -851,22 +904,11 @@ impl WorkerThread { /// //////////////////////////////////////////////////////////////////////// -unsafe fn main_loop( - worker: Worker, - stealer: Stealer, - registry: Arc, - index: usize, -) { - let worker_thread = &WorkerThread { - worker, - stealer, - fifo: JobFifo::new(), - index, - rng: XorShift64Star::new(), - registry, - }; +unsafe fn main_loop(thread: ThreadBuilder) { + let worker_thread = &WorkerThread::from(thread); WorkerThread::set_current(worker_thread); let registry = &*worker_thread.registry; + let index = worker_thread.index; // let registry know we are ready to do work Latch::set(®istry.thread_infos[index].primed); @@ -924,7 +966,7 @@ where // invalidated until we return. op(&*owner_thread, false) } else { - global_registry().in_worker_cold(op) + global_registry().in_worker(op) } } } From 289883035dff786622cb828766a16ae02eb083f0 Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Fri, 17 Feb 2023 10:54:01 -0800 Subject: [PATCH 2/6] Filter tests that need panic unwinding --- rayon-core/src/broadcast/test.rs | 4 ++++ rayon-core/src/join/test.rs | 1 + rayon-core/src/scope/test.rs | 4 ++++ rayon-core/src/spawn/test.rs | 3 +++ rayon-core/src/thread_pool/test.rs | 1 + src/iter/collect/test.rs | 2 ++ tests/collect.rs | 2 ++ tests/iter_panic.rs | 1 + 8 files changed, 18 insertions(+) diff --git a/rayon-core/src/broadcast/test.rs b/rayon-core/src/broadcast/test.rs index a765cb034..eb4754c32 100644 --- a/rayon-core/src/broadcast/test.rs +++ b/rayon-core/src/broadcast/test.rs @@ -130,6 +130,7 @@ fn spawn_broadcast_mutual_sleepy() { } #[test] +#[cfg_attr(not(panic = "unwind"), ignore)] fn broadcast_panic_one() { let count = AtomicUsize::new(0); let pool = ThreadPoolBuilder::new().num_threads(7).build().unwrap(); @@ -146,6 +147,7 @@ fn broadcast_panic_one() { } #[test] +#[cfg_attr(not(panic = "unwind"), ignore)] fn spawn_broadcast_panic_one() { let (tx, rx) = crossbeam_channel::unbounded(); let (panic_tx, panic_rx) = crossbeam_channel::unbounded(); @@ -166,6 +168,7 @@ fn spawn_broadcast_panic_one() { } #[test] +#[cfg_attr(not(panic = "unwind"), ignore)] fn broadcast_panic_many() { let count = AtomicUsize::new(0); let pool = ThreadPoolBuilder::new().num_threads(7).build().unwrap(); @@ -182,6 +185,7 @@ fn broadcast_panic_many() { } #[test] +#[cfg_attr(not(panic = "unwind"), ignore)] fn spawn_broadcast_panic_many() { let (tx, rx) = crossbeam_channel::unbounded(); let (panic_tx, panic_rx) = crossbeam_channel::unbounded(); diff --git a/rayon-core/src/join/test.rs b/rayon-core/src/join/test.rs index e7f287f6f..e01378b92 100644 --- a/rayon-core/src/join/test.rs +++ b/rayon-core/src/join/test.rs @@ -77,6 +77,7 @@ fn panic_propagate_both() { } #[test] +#[cfg_attr(not(panic = "unwind"), ignore)] fn panic_b_still_executes() { let mut x = false; match unwind::halt_unwinding(|| join(|| panic!("Hello, world!"), || x = true)) { diff --git a/rayon-core/src/scope/test.rs b/rayon-core/src/scope/test.rs index 00dd18c92..5d4fa7a12 100644 --- a/rayon-core/src/scope/test.rs +++ b/rayon-core/src/scope/test.rs @@ -213,6 +213,7 @@ fn panic_propagate_nested_scope_spawn() { } #[test] +#[cfg_attr(not(panic = "unwind"), ignore)] fn panic_propagate_still_execute_1() { let mut x = false; match unwind::halt_unwinding(|| { @@ -227,6 +228,7 @@ fn panic_propagate_still_execute_1() { } #[test] +#[cfg_attr(not(panic = "unwind"), ignore)] fn panic_propagate_still_execute_2() { let mut x = false; match unwind::halt_unwinding(|| { @@ -241,6 +243,7 @@ fn panic_propagate_still_execute_2() { } #[test] +#[cfg_attr(not(panic = "unwind"), ignore)] fn panic_propagate_still_execute_3() { let mut x = false; match unwind::halt_unwinding(|| { @@ -255,6 +258,7 @@ fn panic_propagate_still_execute_3() { } #[test] +#[cfg_attr(not(panic = "unwind"), ignore)] fn panic_propagate_still_execute_4() { let mut x = false; match unwind::halt_unwinding(|| { diff --git a/rayon-core/src/spawn/test.rs b/rayon-core/src/spawn/test.rs index 761fafc77..8c453a0db 100644 --- a/rayon-core/src/spawn/test.rs +++ b/rayon-core/src/spawn/test.rs @@ -23,6 +23,7 @@ fn spawn_then_join_outside_worker() { } #[test] +#[cfg_attr(not(panic = "unwind"), ignore)] fn panic_fwd() { let (tx, rx) = channel(); @@ -80,6 +81,7 @@ fn termination_while_things_are_executing() { } #[test] +#[cfg_attr(not(panic = "unwind"), ignore)] fn custom_panic_handler_and_spawn() { let (tx, rx) = channel(); @@ -107,6 +109,7 @@ fn custom_panic_handler_and_spawn() { } #[test] +#[cfg_attr(not(panic = "unwind"), ignore)] fn custom_panic_handler_and_nested_spawn() { let (tx, rx) = channel(); diff --git a/rayon-core/src/thread_pool/test.rs b/rayon-core/src/thread_pool/test.rs index ac750a6dc..142ba6b79 100644 --- a/rayon-core/src/thread_pool/test.rs +++ b/rayon-core/src/thread_pool/test.rs @@ -115,6 +115,7 @@ fn failed_thread_stack() { } #[test] +#[cfg_attr(not(panic = "unwind"), ignore)] fn panic_thread_name() { let (start_count, start_handler) = count_handler(); let (exit_count, exit_handler) = count_handler(); diff --git a/src/iter/collect/test.rs b/src/iter/collect/test.rs index b5f676f5d..97bec3f4e 100644 --- a/src/iter/collect/test.rs +++ b/src/iter/collect/test.rs @@ -76,6 +76,7 @@ fn right_produces_items_with_no_complete() { // Complete is not called by the consumer. Hence,the collection vector is not fully initialized. #[test] +#[cfg_attr(not(panic = "unwind"), ignore)] fn produces_items_with_no_complete() { let counter = DropCounter::default(); let mut v = vec![]; @@ -273,6 +274,7 @@ fn right_panics() { // The left consumer produces fewer items while the right // consumer produces correct number; check that created elements are dropped #[test] +#[cfg_attr(not(panic = "unwind"), ignore)] fn left_produces_fewer_items_drops() { let counter = DropCounter::default(); let mut v = vec![]; diff --git a/tests/collect.rs b/tests/collect.rs index 48b80f699..bfb080ceb 100644 --- a/tests/collect.rs +++ b/tests/collect.rs @@ -6,6 +6,7 @@ use std::sync::atomic::Ordering; use std::sync::Mutex; #[test] +#[cfg_attr(not(panic = "unwind"), ignore)] fn collect_drop_on_unwind() { struct Recorddrop<'a>(i64, &'a Mutex>); @@ -61,6 +62,7 @@ fn collect_drop_on_unwind() { } #[test] +#[cfg_attr(not(panic = "unwind"), ignore)] fn collect_drop_on_unwind_zst() { static INSERTS: AtomicUsize = AtomicUsize::new(0); static DROPS: AtomicUsize = AtomicUsize::new(0); diff --git a/tests/iter_panic.rs b/tests/iter_panic.rs index 37d4d6a3a..c62a8dbda 100644 --- a/tests/iter_panic.rs +++ b/tests/iter_panic.rs @@ -20,6 +20,7 @@ fn iter_panic() { } #[test] +#[cfg_attr(not(panic = "unwind"), ignore)] fn iter_panic_fuse() { // We only use a single thread in order to make the behavior // of 'panic_fuse' deterministic From ae79f0e62e954082214ae0c4bce04b39b2bffd53 Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Fri, 17 Feb 2023 10:55:21 -0800 Subject: [PATCH 3/6] Filter tests that need true threading --- rayon-core/src/broadcast/test.rs | 10 ++++++++ rayon-core/src/join/test.rs | 5 ++++ rayon-core/src/scope/test.rs | 14 +++++++++++ rayon-core/src/spawn/test.rs | 9 +++++++ rayon-core/src/test.rs | 8 ++++++ rayon-core/src/thread_pool/test.rs | 15 +++++++++++ rayon-core/tests/double_init_fail.rs | 1 + rayon-core/tests/init_zero_threads.rs | 1 + rayon-core/tests/scoped_threadpool.rs | 3 +++ rayon-core/tests/stack_overflow_crash.rs | 2 ++ src/iter/test.rs | 3 +++ tests/cross-pool.rs | 1 + tests/named-threads.rs | 1 + tests/octillion.rs | 32 +++++++++++++++++++++--- tests/par_bridge_recursion.rs | 1 + tests/sort-panic-safe.rs | 1 + 16 files changed, 104 insertions(+), 3 deletions(-) diff --git a/rayon-core/src/broadcast/test.rs b/rayon-core/src/broadcast/test.rs index eb4754c32..a2b3acdd8 100644 --- a/rayon-core/src/broadcast/test.rs +++ b/rayon-core/src/broadcast/test.rs @@ -12,6 +12,7 @@ fn broadcast_global() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn spawn_broadcast_global() { let (tx, rx) = crossbeam_channel::unbounded(); crate::spawn_broadcast(move |ctx| tx.send(ctx.index()).unwrap()); @@ -22,6 +23,7 @@ fn spawn_broadcast_global() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn broadcast_pool() { let pool = ThreadPoolBuilder::new().num_threads(7).build().unwrap(); let v = pool.broadcast(|ctx| ctx.index()); @@ -29,6 +31,7 @@ fn broadcast_pool() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn spawn_broadcast_pool() { let (tx, rx) = crossbeam_channel::unbounded(); let pool = ThreadPoolBuilder::new().num_threads(7).build().unwrap(); @@ -40,6 +43,7 @@ fn spawn_broadcast_pool() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn broadcast_self() { let pool = ThreadPoolBuilder::new().num_threads(7).build().unwrap(); let v = pool.install(|| crate::broadcast(|ctx| ctx.index())); @@ -47,6 +51,7 @@ fn broadcast_self() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn spawn_broadcast_self() { let (tx, rx) = crossbeam_channel::unbounded(); let pool = ThreadPoolBuilder::new().num_threads(7).build().unwrap(); @@ -58,6 +63,7 @@ fn spawn_broadcast_self() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn broadcast_mutual() { let count = AtomicUsize::new(0); let pool1 = ThreadPoolBuilder::new().num_threads(3).build().unwrap(); @@ -73,6 +79,7 @@ fn broadcast_mutual() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn spawn_broadcast_mutual() { let (tx, rx) = crossbeam_channel::unbounded(); let pool1 = Arc::new(ThreadPoolBuilder::new().num_threads(3).build().unwrap()); @@ -90,6 +97,7 @@ fn spawn_broadcast_mutual() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn broadcast_mutual_sleepy() { let count = AtomicUsize::new(0); let pool1 = ThreadPoolBuilder::new().num_threads(3).build().unwrap(); @@ -108,6 +116,7 @@ fn broadcast_mutual_sleepy() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn spawn_broadcast_mutual_sleepy() { let (tx, rx) = crossbeam_channel::unbounded(); let pool1 = Arc::new(ThreadPoolBuilder::new().num_threads(3).build().unwrap()); @@ -206,6 +215,7 @@ fn spawn_broadcast_panic_many() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn broadcast_sleep_race() { let test_duration = time::Duration::from_secs(1); let pool = ThreadPoolBuilder::new().num_threads(7).build().unwrap(); diff --git a/rayon-core/src/join/test.rs b/rayon-core/src/join/test.rs index e01378b92..b303dbc81 100644 --- a/rayon-core/src/join/test.rs +++ b/rayon-core/src/join/test.rs @@ -47,6 +47,7 @@ fn sort() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn sort_in_pool() { let rng = seeded_rng(); let mut data: Vec = rng.sample_iter(&Standard).take(12 * 1024).collect(); @@ -87,6 +88,7 @@ fn panic_b_still_executes() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn join_context_both() { // If we're not in a pool, both should be marked stolen as they're injected. let (a_migrated, b_migrated) = join_context(|a| a.migrated(), |b| b.migrated()); @@ -95,6 +97,7 @@ fn join_context_both() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn join_context_neither() { // If we're already in a 1-thread pool, neither job should be stolen. let pool = ThreadPoolBuilder::new().num_threads(1).build().unwrap(); @@ -105,6 +108,7 @@ fn join_context_neither() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn join_context_second() { use std::sync::Barrier; @@ -128,6 +132,7 @@ fn join_context_second() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn join_counter_overflow() { const MAX: u32 = 500_000; diff --git a/rayon-core/src/scope/test.rs b/rayon-core/src/scope/test.rs index 5d4fa7a12..ad8c4af0b 100644 --- a/rayon-core/src/scope/test.rs +++ b/rayon-core/src/scope/test.rs @@ -148,6 +148,7 @@ fn update_tree() { /// linearly with N. We test this by some unsafe hackery and /// permitting an approx 10% change with a 10x input change. #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn linear_stack_growth() { let builder = ThreadPoolBuilder::new().num_threads(1); let pool = builder.build().unwrap(); @@ -296,6 +297,7 @@ macro_rules! test_order { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn lifo_order() { // In the absence of stealing, `scope()` runs its `spawn()` jobs in LIFO order. let vec = test_order!(scope => spawn); @@ -304,6 +306,7 @@ fn lifo_order() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn fifo_order() { // In the absence of stealing, `scope_fifo()` runs its `spawn_fifo()` jobs in FIFO order. let vec = test_order!(scope_fifo => spawn_fifo); @@ -338,6 +341,7 @@ macro_rules! test_nested_order { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn nested_lifo_order() { // In the absence of stealing, `scope()` runs its `spawn()` jobs in LIFO order. let vec = test_nested_order!(scope => spawn, scope => spawn); @@ -346,6 +350,7 @@ fn nested_lifo_order() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn nested_fifo_order() { // In the absence of stealing, `scope_fifo()` runs its `spawn_fifo()` jobs in FIFO order. let vec = test_nested_order!(scope_fifo => spawn_fifo, scope_fifo => spawn_fifo); @@ -354,6 +359,7 @@ fn nested_fifo_order() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn nested_lifo_fifo_order() { // LIFO on the outside, FIFO on the inside let vec = test_nested_order!(scope => spawn, scope_fifo => spawn_fifo); @@ -365,6 +371,7 @@ fn nested_lifo_fifo_order() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn nested_fifo_lifo_order() { // FIFO on the outside, LIFO on the inside let vec = test_nested_order!(scope_fifo => spawn_fifo, scope => spawn); @@ -407,6 +414,7 @@ macro_rules! test_mixed_order { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn mixed_lifo_order() { // NB: the end of the inner scope makes us execute some of the outer scope // before they've all been spawned, so they're not perfectly LIFO. @@ -416,6 +424,7 @@ fn mixed_lifo_order() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn mixed_fifo_order() { let vec = test_mixed_order!(scope_fifo => spawn_fifo, scope_fifo => spawn_fifo); let expected = vec![-1, 0, -2, 1, -3, 2, 3]; @@ -423,6 +432,7 @@ fn mixed_fifo_order() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn mixed_lifo_fifo_order() { // NB: the end of the inner scope makes us execute some of the outer scope // before they've all been spawned, so they're not perfectly LIFO. @@ -432,6 +442,7 @@ fn mixed_lifo_fifo_order() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn mixed_fifo_lifo_order() { let vec = test_mixed_order!(scope_fifo => spawn_fifo, scope => spawn); let expected = vec![-3, 0, -2, 1, -1, 2, 3]; @@ -557,6 +568,7 @@ fn scope_spawn_broadcast_nested() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn scope_spawn_broadcast_barrier() { let barrier = Barrier::new(8); let pool = ThreadPoolBuilder::new().num_threads(7).build().unwrap(); @@ -569,6 +581,7 @@ fn scope_spawn_broadcast_barrier() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn scope_spawn_broadcast_panic_one() { let count = AtomicUsize::new(0); let pool = ThreadPoolBuilder::new().num_threads(7).build().unwrap(); @@ -587,6 +600,7 @@ fn scope_spawn_broadcast_panic_one() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn scope_spawn_broadcast_panic_many() { let count = AtomicUsize::new(0); let pool = ThreadPoolBuilder::new().num_threads(7).build().unwrap(); diff --git a/rayon-core/src/spawn/test.rs b/rayon-core/src/spawn/test.rs index 8c453a0db..b7a0535aa 100644 --- a/rayon-core/src/spawn/test.rs +++ b/rayon-core/src/spawn/test.rs @@ -7,6 +7,7 @@ use super::{spawn, spawn_fifo}; use crate::ThreadPoolBuilder; #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn spawn_then_join_in_worker() { let (tx, rx) = channel(); scope(move |_| { @@ -16,6 +17,7 @@ fn spawn_then_join_in_worker() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn spawn_then_join_outside_worker() { let (tx, rx) = channel(); spawn(move || tx.send(22).unwrap()); @@ -55,6 +57,7 @@ fn panic_fwd() { /// still active asynchronous tasks. We expect the thread-pool to stay /// alive and executing until those threads are complete. #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn termination_while_things_are_executing() { let (tx0, rx0) = channel(); let (tx1, rx1) = channel(); @@ -168,6 +171,7 @@ macro_rules! test_order { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn lifo_order() { // In the absence of stealing, `spawn()` jobs on a thread will run in LIFO order. let vec = test_order!(spawn, spawn); @@ -176,6 +180,7 @@ fn lifo_order() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn fifo_order() { // In the absence of stealing, `spawn_fifo()` jobs on a thread will run in FIFO order. let vec = test_order!(spawn_fifo, spawn_fifo); @@ -184,6 +189,7 @@ fn fifo_order() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn lifo_fifo_order() { // LIFO on the outside, FIFO on the inside let vec = test_order!(spawn, spawn_fifo); @@ -195,6 +201,7 @@ fn lifo_fifo_order() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn fifo_lifo_order() { // FIFO on the outside, LIFO on the inside let vec = test_order!(spawn_fifo, spawn); @@ -232,6 +239,7 @@ macro_rules! test_mixed_order { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn mixed_lifo_fifo_order() { let vec = test_mixed_order!(spawn, spawn_fifo); let expected = vec![3, -1, 2, -2, 1, -3, 0]; @@ -239,6 +247,7 @@ fn mixed_lifo_fifo_order() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn mixed_fifo_lifo_order() { let vec = test_mixed_order!(spawn_fifo, spawn); let expected = vec![0, -3, 1, -2, 2, -1, 3]; diff --git a/rayon-core/src/test.rs b/rayon-core/src/test.rs index 46d63a7df..25b8487f7 100644 --- a/rayon-core/src/test.rs +++ b/rayon-core/src/test.rs @@ -5,6 +5,7 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, Barrier}; #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn worker_thread_index() { let pool = ThreadPoolBuilder::new().num_threads(22).build().unwrap(); assert_eq!(pool.current_num_threads(), 22); @@ -14,6 +15,7 @@ fn worker_thread_index() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn start_callback_called() { let n_threads = 16; let n_called = Arc::new(AtomicUsize::new(0)); @@ -40,6 +42,7 @@ fn start_callback_called() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn exit_callback_called() { let n_threads = 16; let n_called = Arc::new(AtomicUsize::new(0)); @@ -69,6 +72,7 @@ fn exit_callback_called() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn handler_panics_handled_correctly() { let n_threads = 16; let n_called = Arc::new(AtomicUsize::new(0)); @@ -119,6 +123,7 @@ fn handler_panics_handled_correctly() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn check_config_build() { let pool = ThreadPoolBuilder::new().num_threads(22).build().unwrap(); assert_eq!(pool.current_num_threads(), 22); @@ -134,6 +139,7 @@ fn check_error_send_sync() { #[allow(deprecated)] #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn configuration() { let start_handler = move |_| {}; let exit_handler = move |_| {}; @@ -154,6 +160,7 @@ fn configuration() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn default_pool() { ThreadPoolBuilder::default().build().unwrap(); } @@ -162,6 +169,7 @@ fn default_pool() { /// the pool is done with them, allowing them to be used with rayon again /// later. e.g. WebAssembly want to have their own pool of available threads. #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn cleared_current_thread() -> Result<(), ThreadPoolBuildError> { let n_threads = 5; let mut handles = vec![]; diff --git a/rayon-core/src/thread_pool/test.rs b/rayon-core/src/thread_pool/test.rs index 142ba6b79..aa9a4d31d 100644 --- a/rayon-core/src/thread_pool/test.rs +++ b/rayon-core/src/thread_pool/test.rs @@ -16,6 +16,7 @@ fn panic_propagate() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn workers_stop() { let registry; @@ -43,6 +44,7 @@ fn join_a_lot(n: usize) { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn sleeper_stop() { use std::{thread, time}; @@ -89,6 +91,7 @@ fn wait_for_counter(mut counter: Arc) -> usize { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn failed_thread_stack() { // Note: we first tried to force failure with a `usize::MAX` stack, but // macOS and Windows weren't fazed, or at least didn't fail the way we want. @@ -140,6 +143,7 @@ fn panic_thread_name() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn self_install() { let pool = ThreadPoolBuilder::new().num_threads(1).build().unwrap(); @@ -148,6 +152,7 @@ fn self_install() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn mutual_install() { let pool1 = ThreadPoolBuilder::new().num_threads(1).build().unwrap(); let pool2 = ThreadPoolBuilder::new().num_threads(1).build().unwrap(); @@ -167,6 +172,7 @@ fn mutual_install() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn mutual_install_sleepy() { use std::{thread, time}; @@ -195,6 +201,7 @@ fn mutual_install_sleepy() { #[test] #[allow(deprecated)] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn check_thread_pool_new() { let pool = ThreadPool::new(crate::Configuration::new().num_threads(22)).unwrap(); assert_eq!(pool.current_num_threads(), 22); @@ -220,6 +227,7 @@ macro_rules! test_scope_order { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn scope_lifo_order() { let vec = test_scope_order!(scope => spawn); let expected: Vec = (0..10).rev().collect(); // LIFO -> reversed @@ -227,6 +235,7 @@ fn scope_lifo_order() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn scope_fifo_order() { let vec = test_scope_order!(scope_fifo => spawn_fifo); let expected: Vec = (0..10).collect(); // FIFO -> natural order @@ -251,6 +260,7 @@ macro_rules! test_spawn_order { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn spawn_lifo_order() { let vec = test_spawn_order!(spawn); let expected: Vec = (0..10).rev().collect(); // LIFO -> reversed @@ -258,6 +268,7 @@ fn spawn_lifo_order() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn spawn_fifo_order() { let vec = test_spawn_order!(spawn_fifo); let expected: Vec = (0..10).collect(); // FIFO -> natural order @@ -265,6 +276,7 @@ fn spawn_fifo_order() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn nested_scopes() { // Create matching scopes for every thread pool. fn nest<'scope, OP>(pools: &[ThreadPool], scopes: Vec<&Scope<'scope>>, op: OP) @@ -301,6 +313,7 @@ fn nested_scopes() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn nested_fifo_scopes() { // Create matching fifo scopes for every thread pool. fn nest<'scope, OP>(pools: &[ThreadPool], scopes: Vec<&ScopeFifo<'scope>>, op: OP) @@ -337,6 +350,7 @@ fn nested_fifo_scopes() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn in_place_scope_no_deadlock() { let pool = ThreadPoolBuilder::new().num_threads(1).build().unwrap(); let (tx, rx) = channel(); @@ -352,6 +366,7 @@ fn in_place_scope_no_deadlock() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn in_place_scope_fifo_no_deadlock() { let pool = ThreadPoolBuilder::new().num_threads(1).build().unwrap(); let (tx, rx) = channel(); diff --git a/rayon-core/tests/double_init_fail.rs b/rayon-core/tests/double_init_fail.rs index b3ddbeb88..15915304d 100644 --- a/rayon-core/tests/double_init_fail.rs +++ b/rayon-core/tests/double_init_fail.rs @@ -2,6 +2,7 @@ use rayon_core::ThreadPoolBuilder; use std::error::Error; #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn double_init_fail() { let result1 = ThreadPoolBuilder::new().build_global(); assert!(result1.is_ok()); diff --git a/rayon-core/tests/init_zero_threads.rs b/rayon-core/tests/init_zero_threads.rs index ebd73c585..3c1ad251c 100644 --- a/rayon-core/tests/init_zero_threads.rs +++ b/rayon-core/tests/init_zero_threads.rs @@ -1,6 +1,7 @@ use rayon_core::ThreadPoolBuilder; #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn init_zero_threads() { ThreadPoolBuilder::new() .num_threads(0) diff --git a/rayon-core/tests/scoped_threadpool.rs b/rayon-core/tests/scoped_threadpool.rs index db3d0b874..534e8bbf4 100644 --- a/rayon-core/tests/scoped_threadpool.rs +++ b/rayon-core/tests/scoped_threadpool.rs @@ -7,6 +7,7 @@ struct Local(i32); scoped_tls::scoped_thread_local!(static LOCAL: Local); #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn missing_scoped_tls() { LOCAL.set(&Local(42), || { let pool = ThreadPoolBuilder::new() @@ -21,6 +22,7 @@ fn missing_scoped_tls() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn spawn_scoped_tls_threadpool() { LOCAL.set(&Local(42), || { LOCAL.with(|x| { @@ -63,6 +65,7 @@ fn spawn_scoped_tls_threadpool() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn build_scoped_tls_threadpool() { LOCAL.set(&Local(42), || { LOCAL.with(|x| { diff --git a/rayon-core/tests/stack_overflow_crash.rs b/rayon-core/tests/stack_overflow_crash.rs index e87e151a9..7dcde43c4 100644 --- a/rayon-core/tests/stack_overflow_crash.rs +++ b/rayon-core/tests/stack_overflow_crash.rs @@ -40,10 +40,12 @@ fn overflow_code() -> Option { } #[test] +#[cfg_attr(not(any(unix, windows)), ignore)] fn stack_overflow_crash() { // First check that the recursive call actually causes a stack overflow, // and does not get optimized away. let status = run_ignored("run_with_small_stack"); + assert!(!status.success()); #[cfg(any(unix, windows))] assert_eq!(status.code(), overflow_code()); #[cfg(target_os = "linux")] diff --git a/src/iter/test.rs b/src/iter/test.rs index 94323d79d..c72068df7 100644 --- a/src/iter/test.rs +++ b/src/iter/test.rs @@ -468,6 +468,7 @@ fn check_cmp_gt_to_seq() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn check_cmp_short_circuit() { // We only use a single thread in order to make the short-circuit behavior deterministic. let pool = ThreadPoolBuilder::new().num_threads(1).build().unwrap(); @@ -497,6 +498,7 @@ fn check_cmp_short_circuit() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn check_partial_cmp_short_circuit() { // We only use a single thread to make the short-circuit behavior deterministic. let pool = ThreadPoolBuilder::new().num_threads(1).build().unwrap(); @@ -526,6 +528,7 @@ fn check_partial_cmp_short_circuit() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn check_partial_cmp_nan_short_circuit() { // We only use a single thread to make the short-circuit behavior deterministic. let pool = ThreadPoolBuilder::new().num_threads(1).build().unwrap(); diff --git a/tests/cross-pool.rs b/tests/cross-pool.rs index f0a2128a7..835e2e27f 100644 --- a/tests/cross-pool.rs +++ b/tests/cross-pool.rs @@ -2,6 +2,7 @@ use rayon::prelude::*; use rayon::ThreadPoolBuilder; #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn cross_pool_busy() { let pool1 = ThreadPoolBuilder::new().num_threads(1).build().unwrap(); let pool2 = ThreadPoolBuilder::new().num_threads(1).build().unwrap(); diff --git a/tests/named-threads.rs b/tests/named-threads.rs index fd1b0be2d..dadb37ba6 100644 --- a/tests/named-threads.rs +++ b/tests/named-threads.rs @@ -4,6 +4,7 @@ use rayon::prelude::*; use rayon::*; #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn named_threads() { ThreadPoolBuilder::new() .thread_name(|i| format!("hello-name-test-{}", i)) diff --git a/tests/octillion.rs b/tests/octillion.rs index cff2b1192..1af9ad8ba 100644 --- a/tests/octillion.rs +++ b/tests/octillion.rs @@ -68,7 +68,14 @@ fn two_threads R, R: Send>(f: F) -> R { } #[test] -#[cfg_attr(not(target_pointer_width = "64"), ignore)] +#[cfg_attr( + any( + not(target_pointer_width = "64"), + target_os = "emscripten", + target_family = "wasm" + ), + ignore +)] fn find_last_octillion() { // It would be nice if `find_last` could prioritize the later splits, // basically flipping the `join` args, without needing indexed `rev`. @@ -78,32 +85,49 @@ fn find_last_octillion() { } #[test] -#[cfg_attr(not(target_pointer_width = "64"), ignore)] +#[cfg_attr( + any( + not(target_pointer_width = "64"), + target_os = "emscripten", + target_family = "wasm" + ), + ignore +)] fn find_last_octillion_inclusive() { let x = two_threads(|| octillion_inclusive().find_last(|_| true)); assert_eq!(x, Some(OCTILLION)); } #[test] -#[cfg_attr(not(target_pointer_width = "64"), ignore)] +#[cfg_attr( + any( + not(target_pointer_width = "64"), + target_os = "emscripten", + target_family = "wasm" + ), + ignore +)] fn find_last_octillion_flat() { let x = two_threads(|| octillion_flat().find_last(|_| true)); assert_eq!(x, Some(OCTILLION - 1)); } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn find_any_octillion() { let x = two_threads(|| octillion().find_any(|x| *x > OCTILLION / 2)); assert!(x.is_some()); } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn find_any_octillion_flat() { let x = two_threads(|| octillion_flat().find_any(|x| *x > OCTILLION / 2)); assert!(x.is_some()); } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn filter_find_any_octillion() { let x = two_threads(|| { octillion() @@ -114,6 +138,7 @@ fn filter_find_any_octillion() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn filter_find_any_octillion_flat() { let x = two_threads(|| { octillion_flat() @@ -124,6 +149,7 @@ fn filter_find_any_octillion_flat() { } #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn fold_find_any_octillion_flat() { let x = two_threads(|| octillion_flat().fold(|| (), |_, _| ()).find_any(|_| true)); assert!(x.is_some()); diff --git a/tests/par_bridge_recursion.rs b/tests/par_bridge_recursion.rs index 4def0a9e4..3a48ef143 100644 --- a/tests/par_bridge_recursion.rs +++ b/tests/par_bridge_recursion.rs @@ -4,6 +4,7 @@ use std::iter::once_with; const N: usize = 100_000; #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn par_bridge_recursion() { let pool = rayon::ThreadPoolBuilder::new() .num_threads(10) diff --git a/tests/sort-panic-safe.rs b/tests/sort-panic-safe.rs index 00a973106..5d2be7d58 100644 --- a/tests/sort-panic-safe.rs +++ b/tests/sort-panic-safe.rs @@ -117,6 +117,7 @@ macro_rules! test { thread_local!(static SILENCE_PANIC: Cell = Cell::new(false)); #[test] +#[cfg_attr(any(target_os = "emscripten", target_family = "wasm"), ignore)] fn sort_panic_safe() { let prev = panic::take_hook(); panic::set_hook(Box::new(move |info| { From e3dd0a5f6dd57ffda9761b1dd5963b2b5c4156ff Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Fri, 17 Feb 2023 11:36:41 -0800 Subject: [PATCH 4/6] ci: test wasm32-wasi with wasmtime --- .github/workflows/ci.yaml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index fe9b40694..23f2032a2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -65,16 +65,24 @@ jobs: - run: cargo test --verbose --package rayon - run: cargo test --verbose --package rayon-core - # wasm won't actually work without threading, but it builds + # wasm32-unknown-unknown builds, and even has the runtime fallback for + # unsupported threading, but we don't have an environment to execute in. + # wasm32-wasi can test the fallback by running in wasmtime. wasm: name: WebAssembly runs-on: ubuntu-latest + env: + CARGO_TARGET_WASM32_WASI_RUNNER: /home/runner/.wasmtime/bin/wasmtime steps: - uses: actions/checkout@v3 - uses: dtolnay/rust-toolchain@stable with: - target: wasm32-unknown-unknown + targets: wasm32-unknown-unknown,wasm32-wasi - run: cargo check --verbose --target wasm32-unknown-unknown + - run: cargo check --verbose --target wasm32-wasi + - run: curl https://wasmtime.dev/install.sh -sSf | bash + - run: cargo test --verbose --target wasm32-wasi --package rayon + - run: cargo test --verbose --target wasm32-wasi --package rayon-core fmt: name: Format From 118a7445c610f1f1ddfb397a6b18cd31cb04d24e Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Tue, 21 Feb 2023 12:45:41 -0800 Subject: [PATCH 5/6] Add tests that broadcast does allow spawns to run ... even on "fallback" targets like `wasm32-wasi`! --- rayon-core/src/broadcast/test.rs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/rayon-core/src/broadcast/test.rs b/rayon-core/src/broadcast/test.rs index a2b3acdd8..3ae11f7f6 100644 --- a/rayon-core/src/broadcast/test.rs +++ b/rayon-core/src/broadcast/test.rs @@ -228,3 +228,35 @@ fn broadcast_sleep_race() { }); } } + +#[test] +fn broadcast_after_spawn_broadcast() { + let (tx, rx) = crossbeam_channel::unbounded(); + + // Queue a non-blocking spawn_broadcast. + crate::spawn_broadcast(move |ctx| tx.send(ctx.index()).unwrap()); + + // This blocking broadcast runs after all prior broadcasts. + crate::broadcast(|_| {}); + + // The spawn_broadcast **must** have run by now on all threads. + let mut v: Vec<_> = rx.try_iter().collect(); + v.sort_unstable(); + assert!(v.into_iter().eq(0..crate::current_num_threads())); +} + +#[test] +fn broadcast_after_spawn() { + let (tx, rx) = crossbeam_channel::bounded(1); + + // Queue a regular spawn on a thread-local deque. + crate::registry::in_worker(move |_, _| { + crate::spawn(move || tx.send(22).unwrap()); + }); + + // Broadcast runs after the local deque is empty. + crate::broadcast(|_| {}); + + // The spawn **must** have run by now. + assert_eq!(22, rx.try_recv().unwrap()); +} From 26c249fbb0368a66ab9ca457e025e55e556fab0e Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Tue, 21 Feb 2023 13:13:01 -0800 Subject: [PATCH 6/6] Document the global fallback --- rayon-core/src/lib.rs | 18 +++++++++++++++++- src/lib.rs | 5 +++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/rayon-core/src/lib.rs b/rayon-core/src/lib.rs index d0d2ea406..8ef4d6b04 100644 --- a/rayon-core/src/lib.rs +++ b/rayon-core/src/lib.rs @@ -26,7 +26,23 @@ //! [`join()`]: struct.ThreadPool.html#method.join //! [`ThreadPoolBuilder`]: struct.ThreadPoolBuilder.html //! -//! ## Restricting multiple versions +//! # Global fallback when threading is unsupported +//! +//! Rayon uses `std` APIs for threading, but some targets have incomplete implementations that +//! always return `Unsupported` errors. The WebAssembly `wasm32-unknown-unknown` and `wasm32-wasi` +//! targets are notable examples of this. Rather than panicking on the unsupported error when +//! creating the implicit global threadpool, Rayon configures a fallback mode instead. +//! +//! This fallback mode mostly functions as if it were using a single-threaded "pool", like setting +//! `RAYON_NUM_THREADS=1`. For example, `join` will execute its two closures sequentially, since +//! there is no other thread to share the work. However, since the pool is not running independent +//! of the main thread, non-blocking calls like `spawn` may not execute at all, unless a lower- +//! priority call like `broadcast` gives them an opening. The fallback mode does not try to emulate +//! anything like thread preemption or `async` task switching. +//! +//! Explicit `ThreadPoolBuilder` methods always report their error without any fallback. +//! +//! # Restricting multiple versions //! //! In order to ensure proper coordination between threadpools, and especially //! to make sure there's only one global threadpool, `rayon-core` is actively diff --git a/src/lib.rs b/src/lib.rs index 25a5e16a7..e35160b3e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -76,6 +76,11 @@ //! [the `collections` from `std`]: https://doc.rust-lang.org/std/collections/index.html //! [`std`]: https://doc.rust-lang.org/std/ //! +//! # Targets without threading +//! +//! Rayon has limited support for targets without `std` threading implementations. +//! See the [`rayon_core`] documentation for more information about its global fallback. +//! //! # Other questions? //! //! See [the Rayon FAQ][faq].