How can I share immutable data between threads in Rust?

Viewed 3405

I'm working on a ray tracer in Rust as a way to learn the language, and the single-threaded version works fine. I want to speed it up by multithreading it, and multithreading a raytracer in C/C++ is relatively easy because most of the data shared is read-only (the only problems occur when writing the pixel data). But I'm having a lot more trouble with Rust and its ownership rules.

I have a trait Hittable: Send + Sync for the different types of things (spheres, meshes) that can get hit in the world, and I left the implementation for Send and Sync blank because I don't actually need either of them. And then I have a vec of the world objects of type Vec<Box<dyn Hittable>>. For the actual multithreading, I'm trying something like this:

    let pixels_mutex: Arc<Mutex<Vec<Vec<(f64, f64, f64, u32)>>>> = Arc::new(Mutex::new(pixels));
    let vec_arc: Arc<Vec<Box<dyn Hittable>>> = Arc::new(vec);

    let mut thread_vec: Vec<thread::JoinHandle<()>> = Vec::new();

    for _ in 0..NUM_THREADS {
        let camera_clone = camera.clone();
        thread_vec.push(thread::spawn(move || {
            for r in 0..RAYS_PER_THREAD {
                if r % THREAD_UPDATE == 0 {
                    println!("Thread drawing ray {} of {} ({:.2}%)", r, RAYS_PER_THREAD, (r as f64 * 100.) / (RAYS_PER_THREAD as f64));
                }
                let u: f64 = util::rand();
                let v: f64 = util::rand();
                let ray = camera_clone.get_ray(u, v);
                let res = geometry::thread_safe_cast_ray(&ray, &vec_arc, MAX_DEPTH);
                let i = (u * IMAGE_WIDTH as f64).floor() as usize;
                let j = (v * IMAGE_HEIGHT as f64).floor() as usize;
                util::thread_safe_increment_color(&pixels_mutex, j, i, &res);
            }
        }));
    }

    for handle in thread_vec {
        handle.join().unwrap();
    }

I have the thread_safe_increment_color implemented and that seems fine, but I'm holding off on doing thread_safe_cast_ray until I get this loop working. The problem I'm running into with this code is that each thread tries to move vec_arc into its closure, which violates the ownership rule. I tried making a clone of vec_arc like I did with camera, but the compiler wouldn't let me, which I think is because my Hittable trait doesn't require the Copy or Clone trait. And my structs that implement Hittable can't simply derive(Copy, Clone) because they contain a Box<dyn Material>, where Material is another trait that represents the material of the object.

I thought this would be way easier since I know most of the data (other than pixels_mutex) is read-only. How can I share vec_arc (and for that matter, pixels_mutex) between the threads I'm creating?

1 Answers

You should clone vec_arc like you do with camera. Since it is an Arc, cloning it is cheap and doesn't require the Vec and its elements to be cloneable. You just need to ensure that you clone the smart pointer and not the vector, and use the associated function:

let vec_arc = Arc::clone(&vec_arc);

Another option is to use scoped threads, in which case you don't need to use Arc at all, you can simply make the closure non-move and access the local variables directly. Your code would then look like this:

crossbeam::scope(|s| {
    for _ in 0..NUM_THREADS {
        let camera_clone = camera.clone();
        thread_vec.push(s.spawn(|_| {
            // your code here, using `vec` directly instead of `vec_arc`
            ...
        }).unwrap());
    }
}).unwrap();  // here threads are joined automatically
Related