Questions about rand crate's thread rng and data race

The implementation of rand crate's thread rng basically looks like this (please point out if I'm missing something):

use std::{thread_local, rc::Rc, cell::UnsafeCell};

pub struct State;

impl State {
    pub fn gen(&mut self) -> u32 {
        todo!()
    }
}

thread_local! {
    static THREAD_RNG: Rc<UnsafeCell<State>> = Rc::new(UnsafeCell::new(State));
}

pub struct ThreadRng(Rc<UnsafeCell<State>>);

pub fn thread_rng() -> ThreadRng {
    ThreadRng(THREAD_RNG.with(Clone::clone))
}

impl ThreadRng {
    pub fn access(&mut self) -> u32 {
        let state = unsafe { &mut *self.0.get() };
        state.gen()
    }
}

It uses UnsafeCell to bypass the restriction that LocalKey itself and Rc can only get immutable references, and get pointer directly and convert to mutable reference. Rc's !Sync and !Send guarantee that it can only be used in the current thread, which logically seems to guarantee that there is only one place to access state at the same time. If data race can be avoided by guaranteeing that data can only be used in the current thread, then why can LocalKey itself only get immutable references? Can I implement another thread local state in a similar way to the above code and also ensure that there is no data race?

You can't give out a mutable reference from LocalKey::with, because then someone could write

LOCAL.with_mut(|a| {
    LOCAL.with_mut(|b| {
        // 💥 `a` and `b` are `&mut` to the same thing 💣
    });
});

The usage in rand is sound because they control access to the thread local and can show that there's no possible reentrancy that would introduce potential issues.

Your question strikes me as similar to this thread.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.