RustArena

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:

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

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:

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

Previous: Atomics: lock-free counters and flags