Sharing state between threads costs something. An Arc<Mutex<T>> means an allocation, an atomic refcount, and a lock on every access. Sometimes that is exactly right. Often the threads never actually needed to see each other's data, and the whole apparatus is paid for nothing.
thread_local! gives each thread its own private copy of a static:
thread_local! {
static CALLS: Cell<u64> = const { Cell::new(0) };
}Every thread that touches CALLS gets a fresh Cell initialised on first use, dropped when that thread ends. There is no sharing, so there is nothing to lock. It is a static in the sense that it lives at the top level and outlives every function call, but it is not one value: there are as many as there are threads.
You never get a reference out. Access goes through with, which hands your closure a shared reference for the length of the call:
CALLS.with(|calls| calls.set(calls.get() + 1));The reference cannot escape, which is precisely what makes it safe.
Because the reference is shared, the value has to provide its own interior mutability. Cell<T> is the cheap option for Copy types. For anything else, RefCell<T> does the borrow check at runtime, within the one thread that owns it:
thread_local! {
static LOG: RefCell<Vec<String>> = const {
RefCell::new(Vec::new())
};
}
LOG.with_borrow_mut(|log| log.push("event".into()));
LOG.with_borrow(|log| log.len());with_borrow and with_borrow_mut are shorthand for with plus borrow. The const { ... } initialiser is an optimisation: when the value can be built in a const context, the compiler skips the lazy initialisation check on every access.
Keep a per thread counter and a per thread log, then prove they do not leak between threads.
bumppub fn bump() -> u64Increment the calling thread's counter and return the new value. A thread that has never called it starts from 0, so the first call returns 1.
record and eventspub fn record(event: &str)
pub fn events() -> Vec<String>record appends to the calling thread's log. events returns a copy of it, in the order things were recorded, and leaves the log alone.
record("open");
record("close");
assert_eq!(events(), vec!["open", "close"]);counts_per_threadpub fn counts_per_thread(bumps: &[u64]) -> Vec<u64>Spawn one thread per entry, have it call bump that many times, and return each thread's final count in input order.
assert_eq!(counts_per_thread(&[3, 1, 2]), vec![3, 1, 2]);The result being identical to the input is the lesson, not a coincidence. Each worker counts alone.
events has to clone. The borrow only lives as long as the closure.bump must not change the count on the thread that spawned it.thread_local! block is fine, separated by semicolons.Cell::get and Cell::set are all bump needs. There is no += on a Cell.LOG.with_borrow(|log| log.clone()) is the whole of events.JoinHandles first and join them afterwards. Joining inside the spawn loop runs the workers one at a time.thread::scope lets the closures borrow from bumps without any cloning.Sharing state between threads costs something. An Arc<Mutex<T>> means an allocation, an atomic refcount, and a lock on every access. Sometimes that is exactly right. Often the threads never actually needed to see each other's data, and the whole apparatus is paid for nothing.
thread_local! gives each thread its own private copy of a static:
thread_local! {
static CALLS: Cell<u64> = const { Cell::new(0) };
}Every thread that touches CALLS gets a fresh Cell initialised on first use, dropped when that thread ends. There is no sharing, so there is nothing to lock. It is a static in the sense that it lives at the top level and outlives every function call, but it is not one value: there are as many as there are threads.
You never get a reference out. Access goes through with, which hands your closure a shared reference for the length of the call:
CALLS.with(|calls| calls.set(calls.get() + 1));The reference cannot escape, which is precisely what makes it safe.
Because the reference is shared, the value has to provide its own interior mutability. Cell<T> is the cheap option for Copy types. For anything else, RefCell<T> does the borrow check at runtime, within the one thread that owns it:
thread_local! {
static LOG: RefCell<Vec<String>> = const {
RefCell::new(Vec::new())
};
}
LOG.with_borrow_mut(|log| log.push("event".into()));
LOG.with_borrow(|log| log.len());with_borrow and with_borrow_mut are shorthand for with plus borrow. The const { ... } initialiser is an optimisation: when the value can be built in a const context, the compiler skips the lazy initialisation check on every access.
Keep a per thread counter and a per thread log, then prove they do not leak between threads.
bumppub fn bump() -> u64Increment the calling thread's counter and return the new value. A thread that has never called it starts from 0, so the first call returns 1.
record and eventspub fn record(event: &str)
pub fn events() -> Vec<String>record appends to the calling thread's log. events returns a copy of it, in the order things were recorded, and leaves the log alone.
record("open");
record("close");
assert_eq!(events(), vec!["open", "close"]);counts_per_threadpub fn counts_per_thread(bumps: &[u64]) -> Vec<u64>Spawn one thread per entry, have it call bump that many times, and return each thread's final count in input order.
assert_eq!(counts_per_thread(&[3, 1, 2]), vec![3, 1, 2]);The result being identical to the input is the lesson, not a coincidence. Each worker counts alone.
events has to clone. The borrow only lives as long as the closure.bump must not change the count on the thread that spawned it.thread_local! block is fine, separated by semicolons.Cell::get and Cell::set are all bump needs. There is no += on a Cell.LOG.with_borrow(|log| log.clone()) is the whole of events.JoinHandles first and join them afterwards. Joining inside the spawn loop runs the workers one at a time.thread::scope lets the closures borrow from bumps without any cloning.