Search code examples
multithreadingrustclone

How can I clone a Random Number Generator for different threads in Rust?


I'm working on a Rust implementation of a ray tracer based on a C++ codebase (PBRT, if anyone is familiar with it). One of the classes that the C++ version defines is a series of samplers in order to reduce noise in the rendered image. In the rendering process, this sampler is cloned into each rendering thread whenever random numbers are needed. This is how I chose to do it in Rust, which I admit is a little convoluted:

#[derive(Clone)]
pub struct PixelSampler {
    samples_1d: Vec<Vec<f64>>,
    samples_2d: Vec<Vec<Point2<f64>>>,
    current_1d_dimension: i32,
    current_2d_dimension: i32,
    rng: rngs::ThreadRng
}
pub enum Samplers {
    StratifiedSampler {x_samples: i64, y_samples: i64, jitter_samples: bool, pixel: PixelSampler },
    ZeroTwoSequenceSampler { pixel: PixelSampler }
}

impl Clone for Samplers {
    fn clone(&self) -> Self {
        match self {
            Samplers::StratifiedSampler { x_samples, y_samples, jitter_samples, pixel } => { 
                Samplers::StratifiedSampler {x_samples: *x_samples,
                                             y_samples: *y_samples,
                                             jitter_samples: *jitter_samples,
                                             pixel: pixel.clone() }
             }
            Samplers::ZeroTwoSequenceSampler { pixel } => { Samplers::ZeroTwoSequenceSampler{ pixel: pixel.clone() } }
        }
    }
}

And then I also have an Integrator which has a Samplers variant field. In my main rendering loop, I have the following loop that runs for each thread:

for _ in 0..NUM_THREADS {
    let int_clone = integrator.clone(); // integrator just contains a Samplers
    thread_vec.push(thread::spawn(move || {
        loop {
            // do main rendering loop
        }
    }));
}

But when I compile with this, I get the error:

"the trait std::marker::Send is not implemented for std::ptr::NonNull<rand::rngs::adapter::reseeding::ReseedingRng<rand_chacha::chacha::ChaCha20Core, rand_core::os::OsRng>>".

My understanding was that because I only move the cloned version into the thread, implementing Send was not necessary. What am I doing wrong?


Solution

  • As the thread_rng() documentation says, it's:

    [...]essentially just a reference to the PRNG in thread-local memory.

    So by cloning the "rng" you didn't duplicate the generator with its state (which I assume was your intention), but created a new handle to the thread-local RNG. This handle is intentionally not Send because it accesses the thread-local RNG without locking for efficiency.

    If you want your struct to contain an actual RNG, and cloning the struct to duplicate it, you can use the StdRng type, which is the recommended, efficient and secure RNG. To instantiate it, use the methods in the SeedableRng trait. For example:

    #[derive(Clone)]
    pub struct PixelSampler {
        samples_1d: Vec<Vec<f64>>,
        samples_2d: Vec<Vec<Point2<f64>>>,
        current_1d_dimension: i32,
        current_2d_dimension: i32,
        rng: StdRng,
    }
    // ...
    
    let sampler = Samplers::ZeroTwoSequenceSampler {
        pixel: PixelSampler {
            samples_1d: vec![],
            samples_2d: vec![],
            current_1d_dimension: 0,
            current_2d_dimension: 0,
            rng: SeedableRng::from_entropy(),
        },
    };
    // compiles because StdRng is Send
    std::thread::spawn(move || sampler).join().unwrap();
    

    Full example in the playground.