use std::sync::Arc;
#[derive(Clone, Default)]
pub struct WorkStealingContext {
pool: Option<Arc<rayon::ThreadPool>>,
}
impl WorkStealingContext {
#[must_use]
pub fn new() -> Self {
Self { pool: None }
}
#[must_use]
pub fn with_pool(pool: Arc<rayon::ThreadPool>) -> Self {
Self { pool: Some(pool) }
}
#[must_use]
pub fn with_rayon_pool(mut self, pool: Arc<rayon::ThreadPool>) -> Self {
self.pool = Some(pool);
self
}
#[must_use]
pub fn num_threads(&self) -> usize {
match &self.pool {
Some(p) => p.current_num_threads(),
None => rayon::current_num_threads(),
}
}
pub fn install<F, R>(&self, f: F) -> R
where
F: FnOnce() -> R + Send,
R: Send,
{
match &self.pool {
Some(pool) => pool.install(f),
None => f(),
}
}
pub fn par_map_slices_mut<T, F>(&self, data: &mut [T], chunk_size: usize, f: F)
where
T: Send,
F: Fn(&mut [T]) + Send + Sync,
{
if chunk_size == 0 || data.is_empty() {
return;
}
let num_chunks = (data.len() + chunk_size - 1) / chunk_size;
if !super::should_parallelize(num_chunks, self.num_threads()) {
for chunk in data.chunks_mut(chunk_size) {
f(chunk);
}
return;
}
use rayon::prelude::*;
match &self.pool {
Some(pool) => {
pool.install(|| {
data.par_chunks_mut(chunk_size).for_each(&f);
});
}
None => {
data.par_chunks_mut(chunk_size).for_each(f);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
use std::sync::Mutex;
use std::thread::ThreadId;
#[test]
fn test_default_context_increments_all_chunks() {
let ctx = WorkStealingContext::new();
let mut data = vec![0u32; 100];
ctx.par_map_slices_mut(&mut data, 10, |chunk| {
for v in chunk.iter_mut() {
*v = 1;
}
});
assert!(data.iter().all(|&v| v == 1));
}
#[test]
fn test_with_custom_pool() {
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(2)
.build()
.expect("failed to build pool");
let ctx = WorkStealingContext::with_pool(Arc::new(pool));
assert_eq!(ctx.num_threads(), 2);
let mut data = vec![0i64; 64];
ctx.par_map_slices_mut(&mut data, 16, |chunk| {
for v in chunk.iter_mut() {
*v += 7;
}
});
assert!(data.iter().all(|&v| v == 7));
}
#[test]
fn test_with_rayon_pool_builder() {
let pool = Arc::new(
rayon::ThreadPoolBuilder::new()
.num_threads(2)
.build()
.expect("pool build failed"),
);
let ctx = WorkStealingContext::new().with_rayon_pool(pool);
let mut data = vec![1u8; 32];
ctx.par_map_slices_mut(&mut data, 8, |chunk| {
for v in chunk.iter_mut() {
*v = v.wrapping_mul(2);
}
});
assert!(data.iter().all(|&v| v == 2));
}
#[test]
fn test_small_workload_runs_serially_on_caller_thread() {
let pool = Arc::new(
rayon::ThreadPoolBuilder::new()
.num_threads(2)
.build()
.expect("pool build failed"),
);
let ctx = WorkStealingContext::with_pool(pool);
let caller_thread = std::thread::current().id();
let mut data = vec![0u8; 4];
let seen: Mutex<HashSet<ThreadId>> = Mutex::new(HashSet::new());
ctx.par_map_slices_mut(&mut data, 2, |chunk| {
seen.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(std::thread::current().id());
for v in chunk.iter_mut() {
*v = 9;
}
});
assert!(data.iter().all(|&v| v == 9));
let observed = seen.lock().unwrap_or_else(|e| e.into_inner());
assert_eq!(
*observed,
HashSet::from([caller_thread]),
"expected serial fallback on the caller thread only"
);
}
#[test]
fn test_large_workload_dispatches_onto_pool_threads() {
let pool = Arc::new(
rayon::ThreadPoolBuilder::new()
.num_threads(2)
.build()
.expect("pool build failed"),
);
let ctx = WorkStealingContext::with_pool(pool);
let caller_thread = std::thread::current().id();
let mut data = vec![0u8; 32];
let seen: Mutex<HashSet<ThreadId>> = Mutex::new(HashSet::new());
ctx.par_map_slices_mut(&mut data, 1, |chunk| {
seen.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(std::thread::current().id());
chunk[0] = 5;
});
assert!(data.iter().all(|&v| v == 5));
let observed = seen.lock().unwrap_or_else(|e| e.into_inner());
assert!(
!observed.contains(&caller_thread),
"expected work to run on dedicated pool threads, not the caller thread"
);
assert!(
observed.len() <= 2,
"observed {} distinct threads, expected <= 2",
observed.len()
);
}
#[test]
fn test_empty_data_is_noop() {
let ctx = WorkStealingContext::new();
let mut data: Vec<u32> = vec![];
ctx.par_map_slices_mut(&mut data, 8, |_chunk| panic!("should not be called"));
}
#[test]
fn test_chunk_size_zero_is_noop() {
let ctx = WorkStealingContext::new();
let mut data = vec![42u32; 8];
ctx.par_map_slices_mut(&mut data, 0, |_chunk| panic!("should not be called"));
assert!(data.iter().all(|&v| v == 42));
}
}