Thread-Local Storage
Thread-Local Storage (TLS) gives each operating-system thread its own isolated, lazily initialized instance of a static variable. Declared with the std::thread_local! macro, a thread-local variable has type std::thread::LocalKey<T>, requires zero cross-thread synchronization (T does not need to implement Sync or Send), and runs T's Drop destructor automatically when each individual thread exits.
How thread_local! Works Across Threads
Declaration:
thread_local! {
static SCRATCH_BUF: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
}
Runtime Memory Layout (Zero Contention Between Threads!):
+-----------------------------------+ +-----------------------------------+
| Thread 1 TLS Segment | | Thread 2 TLS Segment |
| SCRATCH_BUF: RefCell<Vec<u8>> | | SCRATCH_BUF: RefCell<Vec<u8>> |
| (Allocated & dropped by T1) | | (Allocated & dropped by T2) |
+-----------------------------------+ +-----------------------------------+
Accessing LocalKey<T> (And Special Convenience Methods on Cell and RefCell)
Because a thread-local variable is destroyed when the current thread terminates, LocalKey<T> cannot hand out a bare 'static reference &'static T (which could be sent to another thread that outlives the current thread!). Instead, access is scoped to a closure via .with(|val: &T| ...)—with direct convenience methods unlocked when T is Cell<U> or RefCell<U>:
LocalKey Type |
Method | Signature / Behavior |
|---|---|---|
Any LocalKey<T> |
.with(f) |
fn with<F, R>(&'static self, f: F) -> R where F: FnOnce(&T) -> R |
Any LocalKey<T> |
.try_with(f) |
Returns Err(AccessError) instead of panicking if called after the TLS value was already destroyed during thread shutdown |
LocalKey<Cell<T>> |
.get() (T: Copy), .set(val), .take() (T: Default), .replace(val) |
Directly reads or writes the thread-local Cell<T> without needing a .with(|c| ...) closure! |
LocalKey<RefCell<T>> |
.with_borrow(f), .with_borrow_mut(f), .set(val), .take() |
Directly borrows &T or &mut T inside f without calling .borrow() / .borrow_mut() manually! |
Using const { ... } Initializers and LocalKey<RefCell<T>> Methods
Using const { ... } in a thread_local! initializer enables fast, zero-branch static TLS initialization in the binary image:
use std::cell::{Cell, RefCell};
use std::thread;
thread_local! {
static NEXT_REQUEST_ID: Cell<u64> = const { Cell::new(1) };
static THREAD_LOG: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
}
fn record_event(action: &str) {
// Direct `.get()` and `.set()` on `LocalKey<Cell<u64>>`:
let req_id = NEXT_REQUEST_ID.get();
NEXT_REQUEST_ID.set(req_id + 1);
// Direct `.with_borrow_mut()` on `LocalKey<RefCell<Vec<String>>>`:
THREAD_LOG.with_borrow_mut(|log| {
log.push(format!("[req #{req_id}] {action}"));
});
}
fn main() {
record_event("main_init");
record_event("main_ready");
let worker = thread::spawn(|| {
// Worker thread starts with its own fresh `NEXT_REQUEST_ID = 1` and empty `THREAD_LOG`:
record_event("worker_job_a");
THREAD_LOG.take()
});
let worker_events = worker.join().unwrap();
let main_events = THREAD_LOG.take();
println!("main thread log: {:?}", main_events);
println!("worker thread log: {:?}", worker_events);
}
main thread log: ["[req #1] main_init", "[req #2] main_ready"]
worker thread log: ["[req #1] worker_job_a"]