Step 7 of 7
Global state: OnceLock and LazyLock
Some data really is global: a lookup table used everywhere, a counter for ids, a config loaded at startup. A static item lives for the whole program and every thread can see it, so its type must be Sync. A static's starting value must be computable at compile time, and statics are immutable: COUNTER += 1 on a static COUNTER: u32 fails with error[E0594]: cannot assign to immutable static item `COUNTER` .
Three tools cover nearly every need:
- Atomics can be created at compile time, so
static NEXT_ID: AtomicU64 = AtomicU64::new(1);is a global counter, nounsafeneeded. (Mutex::newworks in a static too.) LazyLock<T>(std::sync) holds a value that's built at run time, by a closure that runs on first use, exactly once, even if several threads get there at the same moment. After that it derefs toT. Use it for tables and other values that can't be built at compile time:static PRICES: HashMap<..> = HashMap::new();fails witherror[E0015]: cannot call non-const associated function.OnceLock<T>is a slot you fill once at run time:get_or_init(|| ...)returns the value, initializing it on the first call;set(value)fills it (and returnsErrif it was already full);get()returnsOption<&T>. Use it when the value depends on something only known while running, like a command-line argument.
use std::collections::HashMap;
use std::sync::LazyLock;
use std::sync::atomic::{AtomicU64, Ordering};
use std::thread;
static NEXT_TICKET: AtomicU64 = AtomicU64::new(1);
static PRICES: LazyLock<HashMap<&'static str, u32>> =
LazyLock::new(|| HashMap::from([("standard", 40), ("vip", 120), ("child", 15)]));
fn next_ticket() -> u64 {
NEXT_TICKET.fetch_add(1, Ordering::Relaxed)
}
fn main() {
let mut tickets: Vec<u64> = thread::scope(|s| {
let handles: Vec<_> = (0..3).map(|_| s.spawn(next_ticket)).collect();
handles.into_iter().map(|h| h.join().unwrap()).collect()
});
tickets.sort();
println!("tickets issued: {tickets:?}");
println!("vip costs {}", PRICES["vip"]);
println!("student price: {:?}", PRICES.get("student"));
}
tickets issued: [1, 2, 3]
vip costs 120
student price: None
s.spawn(next_ticket) passes the function itself as the thread's closure. Which thread got which ticket varies, so the tickets are sorted before printing; fetch_add guarantees no two threads get the same number.
OnceLock with get_or_init is the classic "load once" helper:
use std::sync::OnceLock;
struct Config {
region: String,
retries: u32,
}
static CONFIG: OnceLock<Config> = OnceLock::new();
fn config() -> &'static Config {
CONFIG.get_or_init(|| {
println!("loading config (only once)");
Config { region: String::from("eu-west"), retries: 3 }
})
}
fn main() {
println!("{} with {} retries", config().region, config().retries);
println!("still {}", config().region);
}
loading config (only once)
eu-west with 3 retries
still eu-west
At work
- Globals make code harder to test, since every test shares them. Prefer passing state explicitly (an
Arc<AppState>given to each request handler is the norm in web services, module 41). Keep globals for true constants: lookup tables, compiled regexes (static RE: LazyLock<Regex>). - Older code uses the
lazy_static!macro or theonce_cellcrate.LazyLockandOnceLockreplaced both in the standard library. static mutexists, but every access isunsafe(module 36). Use an atomic, aMutexor aOnceLockinstead.
Which tool when
| You need | Use |
|---|---|
| Hand values from one thread to another | a channel (mpsc) |
| Many threads read the same data | Arc<T> (or borrow it with thread::scope) |
| Many threads change the same data | Arc<Mutex<T>> (RwLock if reads dominate) |
| A shared counter or flag | an atomic |
| A global built once | LazyLock / OnceLock |
Your task
Shipping fees (in cents) are a global table: "local" 300, "national" 700, "eu" 1500, "world" 3500. Declare static SHIPPING: LazyLock<HashMap<&'static str, u64>> built on first use, and write fn total_with_shipping(zone: &str, subtotal: u64) -> Option<u64> that adds the zone's fee to subtotal, or returns None for an unknown zone.
Challenge 2
A greeting is set once at startup from the server's name, then read everywhere. With static GREETING: OnceLock<String>, write:
fn init_greeting(name: &str) -> bool: stores"Welcome to {name}"and returnstrue, or returnsfalse(changing nothing) if a greeting was already set.fn greeting() -> &'static str: the stored greeting, or"Welcome"if none was set yet.
Challenge 3
Write a global id generator and use it from several threads. Declare static NEXT_ID: AtomicU64 starting at 1 and fn next_id() -> u64 that hands out the next id. In a thread::scope, run 4 threads that each take 3 ids and return them in a Vec. Collect all 12 ids, sort them, and print:
ids: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
count: 12