Loading page…
Rust walkthroughs
Loading page…
rand::rngs::ThreadRng vs rand::rngs::StdRng?ThreadRng and StdRng provide different thread-safety characteristics that influence when and how you use them. ThreadRng is a thread-local wrapper that provides automatic thread safety through isolation, while StdRng is a Send but not Sync type that can be moved between threads but requires explicit synchronization for shared access.
use rand::Rng;
use rand::rngs::{ThreadRng, StdRng};
fn basic_usage() {
// ThreadRng: thread-local, accessed via thread_rng()
let mut rng = rand::thread_rng();
let n: u32 = rng.gen();
// StdRng: explicitly created, can be moved between threads
let mut rng = StdRng::from_entropy();
let n: u32 = rng.gen();
}Both provide the same random number generation interface but differ in ownership semantics.
use rand::Rng;
use rand::rngs::ThreadRng;
fn thread_rng_characteristics() {
// ThreadRng is a reference to thread-local state
let rng1: ThreadRng = rand::thread_rng();
let rng2: ThreadRng = rand::thread_rng();
// Both references point to THE SAME thread-local RNG
// They share state within the thread
// ThreadRng is NOT Send (cannot cross threads)
// This fails to compile:
// std::thread::spawn(move || {
// let rng = rng1; // Error: ThreadRng is not Send
// });
// Each thread has its own ThreadRng instance
std::thread::spawn(|| {
let mut rng = rand::thread_rng();
println!("Thread RNG: {}", rng.gen::<u32>());
});
}ThreadRng references thread-local state that cannot cross thread boundaries.
use rand::Rng;
use rand::rngs::StdRng;
use std::sync::{Arc, Mutex};
fn std_rng_characteristics() {
// StdRng implements Send (can be moved between threads)
let mut rng = StdRng::from_entropy();
// Can move into another thread
std::thread::spawn(move || {
let n: u32 = rng.gen();
println!("Spawned thread: {}", n);
}).join().unwrap();
// StdRng does NOT implement Sync (cannot be shared)
// This would fail:
// let rng = Arc::new(StdRng::from_entropy());
// let r1 = rng.clone();
// let r2 = rng.clone();
// std::thread::spawn(move || r1.lock().unwrap().gen::<u32>());
// But can be shared via Mutex (Mutex<StdRng> is Sync)
let rng = Arc::new(Mutex::new(StdRng::from_entropy()));
let r1 = Arc::clone(&rng);
let r2 = Arc::clone(&rng);
let h1 = std::thread::spawn(move || {
r1.lock().unwrap().gen::<u32>()
});
let h2 = std::thread::spawn(move || {
r2.lock().unwrap().gen::<u32>()
});
println!("Thread 1: {}", h1.join().unwrap());
println!("Thread 2: {}", h2.join().unwrap());
}StdRng can be moved between threads but requires synchronization for shared access.
use rand::rngs::{ThreadRng, StdRng};
fn trait_comparison() {
// Trait implementations:
//
// ThreadRng: !Send, !Sync
// StdRng: Send, !Sync
// ThreadRng cannot leave its thread
fn takes_send<T: Send>(_: T) {}
// takes_send(rand::thread_rng()); // Compile error
// StdRng can be sent to another thread
takes_send(StdRng::from_entropy()); // OK
// Neither is Sync (cannot be shared immutably across threads)
fn takes_sync<T: Sync>(_: &T) {}
// takes_sync(&StdRng::from_entropy()); // Compile error
// takes_sync(&rand::thread_rng()); // Compile error
}The trait bounds determine how these types can be used in concurrent contexts.
use rand::Rng;
use rand::rngs::ThreadRng;
fn thread_local_behavior() {
// ThreadRng is implemented as:
// pub struct ThreadRng {
// // Pointer to thread-local state
// }
// Each thread gets its own RNG state
// No synchronization needed within a thread
let handle = std::thread::spawn(|| {
let mut rng = rand::thread_rng();
let v: u32 = rng.gen();
v
});
let result = handle.join().unwrap();
// Main thread has its own RNG
let mut rng = rand::thread_rng();
let v: u32 = rng.gen();
// These are independent - no race conditions possible
}Thread-locality provides automatic isolation between threads.
use rand::Rng;
use rand::rngs::StdRng;
use rand::SeedableRng;
fn seeding() {
// ThreadRng: seeded from system entropy, not reproducible
let r1 = rand::thread_rng().gen::<u32>();
let r2 = rand::thread_rng().gen::<u32>();
// Cannot control seed, results not reproducible
// StdRng: can be seeded for reproducibility
let seed = [0u8; 32];
let mut rng1 = StdRng::from_seed(seed);
let mut rng2 = StdRng::from_seed(seed);
let v1: u32 = rng1.gen();
let v2: u32 = rng2.gen();
// Same seed = same sequence
assert_eq!(v1, v2);
}StdRng allows controlled seeding for reproducible sequences.
use rand::Rng;
use rand::rngs::StdRng;
use rand::SeedableRng;
use std::sync::{Arc, Mutex};
fn shared_rng() {
// Pattern 1: Each thread has its own RNG
std::thread::spawn(|| {
let mut rng = rand::thread_rng();
println!("{}", rng.gen::<u32>());
});
// Pattern 2: Shared StdRng with Mutex
let shared_rng = Arc::new(Mutex::new(StdRng::from_entropy()));
let mut handles = vec![];
for _ in 0..4 {
let rng = Arc::clone(&shared_rng);
handles.push(std::thread::spawn(move || {
rng.lock().unwrap().gen::<u32>()
}));
}
for h in handles {
println!("Result: {}", h.join().unwrap());
}
}Choose between thread-local RNGs or explicitly shared state.
use rand::Rng;
use rand::rngs::{ThreadRng, StdRng};
use rand::SeedableRng;
use std::sync::{Arc, Mutex};
use std::time::Instant;
fn performance_comparison() {
const ITERATIONS: u64 = 1_000_000;
// ThreadRng: no synchronization overhead
let start = Instant::now();
let mut rng = rand::thread_rng();
for _ in 0..ITERATIONS {
let _: u32 = rng.gen();
}
let thread_rng_time = start.elapsed();
// StdRng: no synchronization (owned)
let start = Instant::now();
let mut rng = StdRng::from_entropy();
for _ in 0..ITERATIONS {
let _: u32 = rng.gen();
}
let std_rng_time = start.elapsed();
// Shared StdRng with Mutex: synchronization overhead
let shared = Arc::new(Mutex::new(StdRng::from_entropy()));
let start = Instant::now();
for _ in 0..ITERATIONS {
let _: u32 = shared.lock().unwrap().gen();
}
let shared_rng_time = start.elapsed();
println!("ThreadRng: {:?}", thread_rng_time);
println!("StdRng: {:?}", std_rng_time);
println!("Shared RNG: {:?}", shared_rng_time);
// Shared RNG is significantly slower due to lock contention
}Shared state introduces synchronization overhead.
use rand::Rng;
fn thread_rng_use_cases() {
// Use ThreadRng when:
// 1. You need random numbers within a single thread
// 2. You don't need reproducibility
// 3. You want maximum convenience
let mut rng = rand::thread_rng();
// Quick random values
let n: u32 = rng.gen();
let f: f64 = rng.gen();
let b: bool = rng.gen();
// Random ranges
let n: u32 = rng.gen_range(0..100);
// Random samples
let items = vec!["a", "b", "c", "d"];
let sample = items.choose(&mut rng);
// Shuffling
let mut nums = vec![1, 2, 3, 4, 5];
nums.shuffle(&mut rng);
}ThreadRng is the default choice for single-threaded or thread-local use.
use rand::{Rng, SeedableRng};
use rand::rngs::StdRng;
fn std_rng_use_cases() {
// Use StdRng when:
// 1. You need reproducibility (deterministic seeds)
// 2. You need to pass RNG to another thread
// 3. You want explicit control over RNG lifetime
// Reproducible sequences
fn deterministic_simulation(seed: u64) -> Vec<u32> {
let seed_bytes = seed.to_le_bytes();
let mut full_seed = [0u8; 32];
full_seed[..8].copy_from_slice(&seed_bytes);
let mut rng = StdRng::from_seed(full_seed);
(0..10).map(|_| rng.gen()).collect()
}
let run1 = deterministic_simulation(42);
let run2 = deterministic_simulation(42);
assert_eq!(run1, run2);
// Passing to threads
let rng = StdRng::from_entropy();
std::thread::spawn(move || {
let mut rng = rng;
rng.gen::<u32>()
}).join().unwrap();
}StdRng is for controlled, reproducible, or cross-thread scenarios.
use rand::{Rng, SeedableRng};
use rand::rngs::StdRng;
use std::thread;
fn parallel_rng() {
// Each thread gets its own seeded RNG
let master_seed = [42u8; 32];
let master_rng = StdRng::from_seed(master_seed);
// Create seeded RNGs for each thread
let mut thread_seeds: Vec<[u8; 32]> = (0..4)
.map(|_| master_rng.gen())
.collect();
let handles: Vec<_> = thread_seeds
.into_iter()
.map(|seed| {
thread::spawn(move || {
let mut rng = StdRng::from_seed(seed);
(0..10).map(|_| rng.gen::<u32>()).collect::<Vec<_>>()
})
})
.collect();
// Results are deterministic
for (i, h) in handles.into_iter().enumerate() {
println!("Thread {}: {:?}", i, h.join().unwrap());
}
}Create deterministic parallel RNGs by seeding each thread independently.
use rand::Rng;
use rand::rngs::StdRng;
use rand::SeedableRng;
// ThreadRng works in async contexts (thread-local)
async fn async_with_thread_rng() -> u32 {
let mut rng = rand::thread_rng();
rng.gen()
}
// StdRng can be held across await points
async fn async_with_std_rng() -> u32 {
let mut rng = StdRng::from_entropy();
// Can use rng across await points
some_async_work().await;
rng.gen()
}
async fn some_async_work() {}
// ThreadRng cannot be held across await points in a way
// that would move it between threads
async fn thread_rng_across_await() -> u32 {
// This is fine - thread_rng() is called in the current thread
let mut rng = rand::thread_rng();
rng.gen()
}Both work in async contexts, but StdRng provides more control.
use rand::{Rng, SeedableRng};
use rand::rngs::StdRng;
fn seedable_trait() {
// StdRng implements SeedableRng
let rng1 = StdRng::from_entropy(); // Random seed
let rng2 = StdRng::from_seed([0u8; 32]); // Specific seed
// Can also seed from a u64
use rand::rngs::StdRng;
use rand::SeedableRng;
let rng3 = StdRng::seed_from_u64(42);
// ThreadRng does not implement SeedableRng
// Cannot control its seed
// fork() creates independent RNGs from a parent
let (child1, child2) = StdRng::from_entropy().fork();
// Each child has different deterministic sequence
}StdRng implements SeedableRng for controlled initialization.
use rand::Rng;
use rand::rngs::{ThreadRng, StdRng};
fn security_notes() {
// Both ThreadRng and StdRng use ChaCha20 (cryptographically secure)
// They are suitable for security-sensitive applications
// ThreadRng:
// - Uses system entropy for seeding
// - Periodically reseeds from system entropy
// - Good for most security use cases
// StdRng:
// - Uses system entropy when created with from_entropy()
// - Does NOT automatically reseed
// - Explicit seed = reproducible but not secure
// For cryptographic keys, use OsRng instead:
use rand::rngs::OsRng;
let mut csprng = OsRng;
let key: [u8; 32] = csprng.gen();
}Both use cryptographically secure algorithms, but OsRng is preferred for secrets.
use rand::rngs::{ThreadRng, StdRng};
fn comparison() {
// ┌─────────────────┬───────────────────┬───────────────────┐
// │ Feature │ ThreadRng │ StdRng │
// ├─────────────────┼───────────────────┼───────────────────┤
// │ Send │ No │ Yes │
// │ Sync │ No │ No │
// │ Seedable │ No │ Yes │
// │ Thread-safe │ Yes (thread-local)│ With Mutex │
// │ Performance │ Best (no sync) │ Best (no sync) │
// │ Reproducible │ No │ Yes (with seed) │
// │ Convenience │ Best │ Good │
// │ Cross-thread │ No │ Yes (move only) │
// └─────────────────┴───────────────────┴───────────────────┘
}The choice depends on your thread-safety and reproducibility requirements.
use rand::{Rng, SeedableRng};
use rand::rngs::StdRng;
use std::sync::{Arc, Mutex};
// Pattern 1: ThreadRng for convenience
fn quick_random() -> u32 {
rand::thread_rng().gen()
}
// Pattern 2: StdRng for reproducibility
fn reproducible_random(seed: u64) -> u32 {
StdRng::seed_from_u64(seed).gen()
}
// Pattern 3: StdRng for cross-thread work
fn cross_thread_random() -> u32 {
let rng = StdRng::from_entropy();
std::thread::spawn(move || rng.gen::<u32>()).join().unwrap()
}
// Pattern 4: Shared RNG with explicit synchronization
struct SharedRng {
rng: Mutex<StdRng>,
}
impl SharedRng {
fn new() -> Self {
Self {
rng: Mutex::new(StdRng::from_entropy()),
}
}
fn gen<T: rand::distributions::uniform::SampleUniform>(&self) -> T
where
rand::distributions::Standard: rand::distributions::Distribution<T>
{
self.rng.lock().unwrap().gen()
}
}These patterns cover the common use cases.
The thread-safety guarantees differ fundamentally between ThreadRng and StdRng:
ThreadRng characteristics:
| Property | Value |
|----------|-------|
| Send | ❌ Not implemented |
| Sync | ❌ Not implemented |
| Thread safety | ✅ Via thread-local storage |
| Seedable | ❌ Cannot control seed |
| Best for | Single-thread convenience |
StdRng characteristics:
| Property | Value |
|----------|-------|
| Send | ✅ Can move between threads |
| Sync | ❌ Not without wrapper |
| Thread safety | ✅ With Mutex or per-thread |
| Seedable | ✅ Full control |
| Best for | Reproducibility, explicit control |
Decision guide:
// Need random numbers in one thread?
// → Use ThreadRng
let n = rand::thread_rng().gen::<u32>();
// Need reproducible sequences?
// → Use StdRng with seed
let mut rng = StdRng::seed_from_u64(42);
// Need to pass RNG to another thread?
// → Use StdRng
let rng = StdRng::from_entropy();
std::thread::spawn(move || rng.gen::<u32>());
// Need shared RNG across threads?
// → Use Arc<Mutex<StdRng>> or per-thread RNGs
let shared = Arc::new(Mutex::new(StdRng::from_entropy()));Key insight: ThreadRng achieves thread safety through isolation (each thread has its own RNG), while StdRng achieves thread safety through controlled ownership (move semantics and explicit synchronization). Choose based on whether you need convenience or control.