Imagine that we have some object foo: Foo. Some of the time, when we need to do multithreaded computation on foo, we clone and move it. However, there are also times when we don't need to clone it, which may be expensive, and can instead put it in an Arc to share its data between threads. In the question title, "shared" means "inside an Arc", and "isolated" means in only some places, i.e. not program-wide.
I can think of two umbrella ways to do this:
- Have the callee consume the object and wrap/unwrap in
Arcinternally, returning the object along with any other data in a tuple. - Accept
Arc<Foo>as the callee, delegating wrap/unwrap responsibility to the caller.
However, neither of these seems particularly clean. (1) has a complicated return type that might complicate or hinder e.g. chained operations, and (2) forces the caller to do work that it shouldn't really need to know about.
I'm curious if there is an idiomatic pattern for handling this sort of situation. For reference, I've included a basic example of the two patterns I describe above.
use std::sync::Arc;
use std::thread::{self, JoinHandle};
use std::time::Duration;
fn main() {
let foo = Foo(0);
// pattern 1
let (foo, _res) = process(foo);
// pattern 2
let foo = Arc::new(foo);
let _res = process2(Arc::clone(&foo));
let foo = Arc::try_unwrap(foo).unwrap();
}
#[derive(Debug)]
struct Foo(u8);
impl Clone for Foo {
/// an expensive clone operation that we want to avoid when possible
fn clone(&self) -> Self {
thread::sleep(Duration::from_secs(10));
Foo(self.0)
}
}
/// complicated return type
fn process(foo: Foo) -> (Foo, u8) {
let foo = Arc::new(foo);
let mut threads = Vec::<JoinHandle<()>>::new();
for _ in 1..=5 {
let f = Arc::clone(&foo);
let t = thread::spawn(move || {
println!("{:?}", f);
});
threads.push(t);
}
threads.into_iter().for_each(|t| t.join().unwrap());
(Arc::try_unwrap(foo).unwrap(), 1)
}
/// forces the caller to do work that should be hidden
fn process2(foo: Arc<Foo>) -> u8 {
let mut threads = Vec::<JoinHandle<()>>::new();
for _ in 1..=5 {
let f = Arc::clone(&foo);
let t = thread::spawn(move || {
println!("{:?}", f);
});
threads.push(t);
}
threads.into_iter().for_each(|t| t.join().unwrap());
1
}
CodePudding user response:
This is a good use for scoped threads (provided by crossbeam::scope or the higher-level utilities of rayon), which allow you to simply use an &Foo inside the threads. This avoids needing to move the value into and out of an Arc at all.
Also, if you use rayon you can greatly simplify your code by removing all of the explicit thread creation and joining in favor of its parallel iterators.
use rayon::iter::{ParallelIterator, IntoParallelIterator};
fn main() {
let foo = Foo(0);
let _res = process(&foo);
}
#[derive(Debug)]
struct Foo(u8);
fn process(foo: &Foo) -> u8 {
(1..=5).into_par_iter().for_each(|_i: usize| {
println!("{:?}", foo);
});
1
}
