Using a global RNG (alternatives to static mut)

I'm trying to write a "quick and dirty" simulation of a random process. My main concern is writing the logic of the process itself, which is relatively complex (at least for my level of Rust knowledge...)

I know it's not good practice, but I want to avoid having to pass round a RNG through my various functions, just so that I have it available in the low-level functions that implement the randomness in my model. In other languages I've worked with, I'd use a global variable - with a certain amount of distaste, and promises to "fix it later", but in the interests of focusing on the important aspects of the problem.

I've seen a lot of dire warnings about static mut, so I assume that's not what I should use here. The trouble is, most of those warnings suggest alternatives like interior mutability, which is way beyond my level of understanding, and not something I want to get sucked into, just to get my little simulation to work.

Is there a good solution to this problem? I'm aware of (and in agreement with) the arguments saying that global state is bad. But not being able to focus on my actual code is worse, in this case. I would be perfectly happy with some sort of "you don't need to understand this" magic incantation for now - heck, one day, when I do have the time, I'd probably take the time to understand such a solution (and in the process learn more about Rust :slightly_smiling_face:), but for now I just want to get back to my actual code...

One option is to use rand's thread-local RNG and just call rand::rng() to get a new handle to it whenever you need some randomness.

1 Like

Hmm, it never occurred to me that rand::rng() would return the same RNG. I thought it was returning a newly-created RNG each time. Re-reading the docs, I now see that's what it says (although in my defence, there's a lot of background detail that, while certainly valuable, obscures this simple approach).

Thanks for the quick answer!

Putting a Mutex into a (non-mut) static, which is a form of interior mutability, is far easier to use correctly than a static mut is.

rand::rng() is a good choice when you don't care about the details of your random numbers, but it is, itself, a usage of interior mutability, and if you wanted to produce your own version with different properties, you'd write your own — not strictly Mutex but a thread_local RefCell, perhaps.

The only reasonable way to not use interior mutability anywhere in your program is to not use global state.

2 Likes

Thanks. I may have expressed myself badly - I certainly don't mind using interior mutability (or indeed any language feature). What I want to avoid is having to understand it enough to implement my own equivalent of global state, as that would at this stage be a distraction from my main goal of implementing my application.

Understood. But again, that's for later (I still consider myself very definitely a beginner at this point, and using interior mutability to write my own shared state mechanism definitely sounds more like at least an "intermediate" task).

I consider my immediate need solved by rand::rng(), but I'm still interested in what options I would have had if that didn't exist - so your comments are helpful in that regard.

That's a lot closer to what I was expecting to end up with from my original question - what built in (standard library, or well known crate) options are there to manage global state, and how are they used in practice? Better still, where should I have looked, and/or what search terms should I have known, to find that sort of information myself. Everything I tried led me to explanations of how to implement interior mutability, not useful tools that encapsulated it for me.

In practice, I'm not (currently) using multiple threads, so a Mutex feels like overkill. What would be the single-threaded equivalent? A RefCell? Is that what the borrow_mut method is for?

My naive attempt at this using a RefCell would be:

static my_rng: RefCell = ???

fn use_rng() {
    let mut rng = my_rng.borrow_mut();
    let a_random_number: f32 = rng.random();
}

But I'm not sure what I'd put at ??? to initialise the my_rng variable. Is that what std::sync::LazyLock is for? And again, as I'm not using threads, is sync the right place to be looking for this sort of functionality?

More questions than answers, as you can see - this is basically the rabbit hole I end up going down, and I feel like I'm missing something basic. Not so much because I think that mutable global state is a good thing, but because it's something people coming from other languages are used to having, and I hope there's a better path away from it than just "this doesn't exist, you need to redesign your program or learn some advanced stuff".

When following up a different question, I discovered OnceCell. Is that what I should be using, I wonder? I tried

use std::cell::OnceCell;

static test: OnceCell<Vec<u32>> = OnceCell::new();

fn main() {
    test.set(vec![1,2,3]);
    let x = test.get_mut().unwrap();
    x[1] = 0;
    println!("{:?}", x);
}

and got an error because OnceCell isn't Sync. I guess global statics have to be thread-safe, even if I'm not using threads... I hope the compiler is smart enough to omit locks that I won't in practice need (in my real use case, the RNG is accessed in a tight inner loop).

But switching to OnceLock got me "error[E0596]: cannot borrow immutable static item test as mutable" - which makes sense, as get_mut takes &mut self. Presumably that means OnceLock and OnceCell don't implement interior mutability?

So I guess OnceLock is a red herring.

Use static_init. Add there lines of code on your end, and forget the rest (for the time being).

use static_init::dynamic as static_init;
#[static_init] 
static mut RNG: ... = Rng::create_rng();
1 Like

You can't (safely) escape the multi thread nature of Rust. Using a Mutex is quite cheap and simple. No reason to be afraid of it:

use std::sync::Mutex;

struct SloppyRng {
    state: u32,
}

impl SloppyRng {
    const fn new() -> Self {
        Self { state: 123 }
    }
    
    fn random(&mut self) -> u32 {
        self.state += 1;
        self.state
    }
}

static SLOPPY_RNG: Mutex<SloppyRng> = Mutex::new(SloppyRng::new());

fn random() -> u32 {
    let mut sloppy_rng = SLOPPY_RNG.lock().unwrap();
    sloppy_rng.random()
}

fn main() {
    println!("random() = {}", random());
    println!("random() = {}", random());
    println!("random() = {}", random());
}

To avoid a Mutex you can use a thread_local.

However, a much better solution is to use a local variable.

If you have a lot of functions access the same state, you typically want to make those functions struct methods instead, and put your RNG in the struct.

1 Like

Interesting. I'm surprised this doesn't get mentioned a lot more, given how simple it seems to be. I'm curious about what the trade-offs are between that and the mutex approach.

OK, I certainly don't mind embracing multi-threadedness, my caution is mainly because in other languages, I've learned to be cautious of the overheads of synchronisation primitives. Is a mutex significantly more efficient than a traditional semaphore-style lock, then? Is it more like a critical section?

(Yes, this is why I was looking for a simple "just do this and don't think" solution to my original problem - I always manage to get sidetracked by interesting technical details, and lose track of the fact that I had a problem I was trying to solve :slightly_smiling_face:)

Thanks also for the mutex example. It does look nice & simple to use.

Unfortunately, that's not really an option. I have multiple structs, representing the various elements of my simulation. I'm not sure what the Rust equivalent of inheritance would be, but even so, making all of those structs "inherit" from a common base just so that I have somewhere to store a shared implementation detail (the RNG) doesn't really make sense to me.

Inheritance would be the wrong tool: you want to share one RNG, inheritance from a base class with an RNG would create a separate RNG for each struct.

You can put all your structs in a bigger struct that contains the RNG.

But if you have a bunch of randomized functions that operate on some smaller structs, passing a reference to the RNG to each such function as a second parameter is natural, clear and appropriate. I don't see why you'd want to avoid passing that parameter. It also makes it easier to test (you can pass a deterministically seeded RNG, for example) than using a global RNG.

Because the functions are nested multiple levels deep. Passing a RNG in at the top, when 3-4 layers of code do nothing with it apart from forward it onto a callee, feels like a bad design - the function signatures are cluttered with details that are unrelated to the job they are doing, and the implementation is unnecessarily exposed. I tried it, and I really didn't like the result.

But I don't think this is a particularly productive direction to take the discussion. Yes, I could pass the RNG through all the functions. I know how to do that, certainly. What I'm looking for is what alternatives there are - once I know what my options are, I can choose the one that best suits my design. But if I don't know what's available, my design choices will remain unnecessarily limited.

Thanks for the suggestions, but I'll continue looking at the alternatives. (As a reminder, this is now more about understanding what's available for the future - for my current problem, calling rand::rng() whenever I need the RNG is ideal).

Counterargument: part of defining the job they are doing is defining which random numbers they're going to use as additional inputs. But, of course, if you don’t care about deterministic/reproducible results then this is of no significance.

I would say that there is sort of a third option besides globals and explicit passing, which is to pass the RNG as a component of another input the function already needs. That might be called self or maybe ctx: &mut Context, but in any case, a lot of code does eventually have some additional information that it uses at all levels, and if you end up with such a thing, you can likely arrange to keep the RNG in there.

3 Likes

That improves “locality”. In some case that improves performance.

A “context”, mentioned by @kpreid above, can also do that.

Thinking some more about what this means, isn't it simply (in effect) making the whole application into a giant struct, with the global state becoming instance variables on that global struct? That would work, but in terms of design, it feels like all I'm doing is using global state while pretending not to. So it's certainly a workaround, but it doesn't feel any better than just using globals.

Whether the bigger struct is “the whole application” or not depends on how much more of the application there is.

But even if it is, there’s still significant differences between “there is one struct containing everything” and “there is static state”:

  • It's easier to write tests against the former than the latter — you don't have to make sure each test resets all static state that might affect the outcome of the test, but can just create a new struct for each test.
  • It's easier to read the program and understand all of the interdependencies by looking for accesses to the struct fields, than it is to understand the uses of static state which can be completely unmarked.
2 Likes

Even if your whole application is just running the one simulation, it's still a better abstraction to put the simulation state in a data structure, rather than having it be spread between seemingly unrelated global variables. main will create one instance of this and this way it's clear it's running one simulation. In the future you could decide to run several simulations back to back or in parallel, etc, which would be impossible if you use global variables as the simulation state.

1 Like

Dividing the app state cleanly into structs that can be passed as arguments can be difficult, because you really have to get the dependencies right and it often takes many refactors, for me anyway. But there is no other way to have a clear design.

By not doing this and using global data, the dependencies are completely unconstrained. The same thing is true if you use a single struct for all state and pass it to every function, so I think you have a good point, but I don't think anyone is suggesting to go that far.

1 Like

I'm surprised that atomics haven't been mentioned yet. If all you need is a quick-and-dirty non-cryptographic PRNG, there are several fast ones that only have 64 bits worth of state and good enough randomness properties. Here's Xorshift64 (playground):

use std::sync::atomic::*;

static STATE: AtomicU64 = AtomicU64::new(0x11_22_33_44_55_66_77_88);

fn rnd() -> u64 {
    let mut st = STATE.load(Ordering::Relaxed);
    st ^= st << 13;
    st ^= st >> 17;
    st ^= st << 5;
    STATE.store(st, Ordering::Relaxed);
    st
}

fn main() {
    for _ in 0..100 {
        println!("{}", rnd());
    }
}

My opinion: don't do that. That's not really what atomics are for. This code won't work properly in a multithreaded scenario (you'll get repeats).

Also, "good enough" depends on the application, but I definitely wouldn't trust Xorshift for any simulation where you're looking to gather some actual statistical results. There is essentially no reason to prefer it over rand::rngs::StdRng (which rand::rng uses). You'd be basically risking statistical correctness to gain what is typically a tiny performance benefit.