Loading page…
Rust walkthroughs
Loading page…
Thread Local Storage (TLS) allows each thread to have its own independent copy of a variable. Unlike regular static variables which are shared across all threads, thread-local variables have a separate instance per thread. This is useful for maintaining per-thread state without synchronization overhead.
Key concepts:
thread_local!, provides access methodsWhen to use Thread Local Storage:
When NOT to use Thread Local Storage:
use std::thread;
thread_local!(static COUNTER: i32 = 0);
fn main() {
COUNTER.with(|c| {
println!("Main thread counter: {}", c);
});
let handle = thread::spawn(|| {
COUNTER.with(|c| {
println!("Spawned thread counter: {}", c);
});
});
handle.join().unwrap();
}use std::thread;
use std::cell::RefCell;
thread_local!(static COUNTER: RefCell<i32> = RefCell::new(0));
fn increment() {
COUNTER.with(|c| {
*c.borrow_mut() += 1;
});
}
fn get_counter() -> i32 {
COUNTER.with(|c| *c.borrow())
}
fn main() {
increment();
increment();
println!("Main: {}", get_counter());
let handle = thread::spawn(|| {
increment();
increment();
increment();
println!("Thread: {}", get_counter());
});
handle.join().unwrap();
println!("Main again: {}", get_counter());
}use std::thread;
use std::cell::RefCell;
thread_local!(static BUFFER: RefCell<Vec<u8>> = RefCell::new(Vec::new()));
fn process_data(data: &[u8]) -> Vec<u8> {
BUFFER.with(|buf| {
let mut buffer = buf.borrow_mut();
buffer.clear();
buffer.extend_from_slice(data);
buffer.push(b'!');
buffer.clone()
})
}
fn main() {
let result1 = process_data(b"hello");
println!("Result 1: {:?}", String::from_utf8_lossy(&result1));
let handle = thread::spawn(|| {
let result = process_data(b"world");
println!("Thread result: {:?}", String::from_utf8_lossy(&result));
});
handle.join().unwrap();
}use std::thread;
use std::cell::RefCell;
// Note: In practice, use rand::thread_rng() which is already thread-local
struct SimpleRng {
state: u64,
}
impl SimpleRng {
fn new(seed: u64) -> Self {
Self { state: seed }
}
fn next(&mut self) -> u64 {
// Simple LCG
self.state = self.state.wrapping_mul(1103515245).wrapping_add(12345);
self.state
}
}
thread_local!(static RNG: RefCell<SimpleRng> = RefCell::new(SimpleRng::new(1)));
fn random_u64() -> u64 {
RNG.with(|rng| rng.borrow_mut().next())
}
fn main() {
println!("Main: {}", random_u64());
println!("Main: {}", random_u64());
let handle = thread::spawn(|| {
// Different seed in new thread
RNG.with(|rng| *rng.borrow_mut() = SimpleRng::new(2));
println!("Thread: {}", random_u64());
println!("Thread: {}", random_u64());
});
handle.join().unwrap();
println!("Main again: {}", random_u64());
}use std::thread;
use std::cell::RefCell;
#[derive(Debug, Clone)]
struct RequestContext {
request_id: String,
user_id: Option<String>,
}
thread_local!(static CONTEXT: RefCell<RequestContext> = RefCell::new(RequestContext {
request_id: String::new(),
user_id: None,
}));
fn set_context(request_id: &str, user_id: Option<&str>) {
CONTEXT.with(|ctx| {
let mut ctx = ctx.borrow_mut();
ctx.request_id = request_id.to_string();
ctx.user_id = user_id.map(|s| s.to_string());
});
}
fn log_context(message: &str) {
CONTEXT.with(|ctx| {
let ctx = ctx.borrow();
println!("[{}] {} (user: {:?})", ctx.request_id, message, ctx.user_id);
});
}
fn main() {
set_context("req-001", Some("alice"));
log_context("Processing request");
let handle = thread::spawn(|| {
set_context("req-002", Some("bob"));
log_context("Processing in thread");
});
handle.join().unwrap();
log_context("Back in main");
}use std::thread;
use std::cell::RefCell;
use std::collections::HashMap;
thread_local!(static METRICS: RefCell<HashMap<String, u64>> = RefCell::new(HashMap::new()));
fn increment_metric(name: &str) {
METRICS.with(|m| {
*m.borrow_mut().entry(name.to_string()).or_insert(0) += 1;
});
}
fn get_metrics() -> HashMap<String, u64> {
METRICS.with(|m| m.borrow().clone())
}
fn process_items(items: &[&str]) {
for item in items {
increment_metric(&format!("processed_{}", item));
increment_metric("total");
}
}
fn main() {
process_items(&["a", "b", "a", "c"]);
println!("Main metrics: {:?}", get_metrics());
let handle = thread::spawn(|| {
process_items(&["x", "y", "x", "x", "z"]);
println!("Thread metrics: {:?}", get_metrics());
});
handle.join().unwrap();
println!("Main still: {:?}", get_metrics());
}use std::thread;
use std::cell::RefCell;
struct Database {
connection_string: String,
connected: bool,
}
impl Database {
fn new(conn_str: &str) -> Self {
println!("Creating database connection for thread");
Self {
connection_string: conn_str.to_string(),
connected: true,
}
}
}
thread_local!(static DB: RefCell<Option<Database>> = RefCell::new(None));
fn get_db() -> &'static RefCell<Option<Database>> {
// Lazy initialization pattern
DB.with(|db| {
if db.borrow().is_none() {
*db.borrow_mut() = Some(Database::new("localhost:5432"));
}
db
});
// This is a bit of a hack - in practice use OnceLock
DB.with(|db| db)
}
fn query(sql: &str) {
DB.with(|db| {
if let Some(db) = db.borrow().as_ref() {
println!("Query on {}: {}", db.connection_string, sql);
}
});
}
fn main() {
query("SELECT 1");
let handle = thread::spawn(|| {
query("SELECT 2");
});
handle.join().unwrap();
}use std::thread;
use std::cell::RefCell;
use std::io::Write;
thread_local!(static LOGGER: RefCell<Vec<String>> = RefCell::new(Vec::new()));
fn log(message: &str) {
LOGGER.with(|logs| {
logs.borrow_mut().push(format!("[{}] {}",
std::thread::current().name().unwrap_or("unknown"),
message
));
});
}
fn get_logs() -> Vec<String> {
LOGGER.with(|logs| logs.borrow().clone())
}
fn main() {
log("Starting");
let handle = thread::Builder::new()
.name("worker".to_string())
spawn(move || {
log("Working");
log("Done");
});
handle.unwrap().join().unwrap();
log("Finishing");
println!("Main logs: {:?}", get_logs());
}use std::thread;
use std::cell::RefCell;
thread_local!(static DEPTH: RefCell<usize> = RefCell::new(0));
fn recursive_function(n: usize) -> usize {
DEPTH.with(|d| *d.borrow_mut() += 1);
let current_depth = DEPTH.with(|d| *d.borrow());
println!("Depth: {}, computing for n={}", current_depth, n);
let result = if n <= 1 {
1
} else {
n * recursive_function(n - 1)
};
DEPTH.with(|d| *d.borrow_mut() -= 1);
result
}
fn main() {
println!("Result: {}", recursive_function(5));
}use std::thread;
use std::cell::Cell;
thread_local!(static STATE: Cell<i32> = Cell::new(0));
fn get_state() -> i32 {
STATE.with(|s| s.get())
}
fn set_state(value: i32) {
STATE.with(|s| s.set(value));
}
fn main() {
set_state(42);
println!("Main: {}", get_state());
let handle = thread::spawn(|| {
set_state(100);
println!("Thread: {}", get_state());
});
handle.join().unwrap();
println!("Main again: {}", get_state());
}use std::thread;
use std::sync::Arc;
use std::cell::RefCell;
struct SharedConfig {
name: String,
}
thread_local!(static CACHE: RefCell<Option<Arc<SharedConfig>>> = RefCell::new(None));
fn get_cached_config() -> Arc<SharedConfig> {
let config = Arc::new(SharedConfig {
name: String::from("shared-config"),
});
CACHE.with(|cache| {
if let Some(cached) = cache.borrow().as_ref() {
Arc::clone(cached)
} else {
cache.borrow_mut() = Some(Arc::clone(&config));
config
}
})
}
fn main() {
let config = get_cached_config();
println!("Config: {}", config.name);
}use std::thread;
use std::cell::RefCell;
// Reusable buffer for string parsing
thread_local!(static STRING_BUFFER: RefCell<String> = RefCell::new(String::new()));
fn process_string(input: &str) -> String {
STRING_BUFFER.with(|buf| {
let mut buffer = buf.borrow_mut();
buffer.clear();
buffer.push_str(input);
buffer.push_str("_processed");
buffer.clone()
})
}
fn main() {
println!("{}", process_string("hello"));
println!("{}", process_string("world"));
}use std::thread;
use std::cell::Cell;
const INIT_VALUE: i32 = 100;
thread_local!(static VALUE: Cell<i32> = Cell::new(INIT_VALUE));
fn main() {
VALUE.with(|v| println!("Initial: {}", v.get()));
VALUE.with(|v| v.set(200));
VALUE.with(|v| println!("After set: {}", v.get()));
}use std::thread;
use std::cell::RefCell;
thread_local!(static DATA: RefCell<Vec<i32>> = RefCell::new(Vec::new()));
fn main() {
// with() - access through closure
DATA.with(|d| {
d.borrow_mut().push(1);
d.borrow_mut().push(2);
println!("Inside with: {:?}", d.borrow());
});
// try_with() - fallible version (can fail during thread destruction)
match DATA.try_with(|d| d.borrow().len()) {
Ok(len) => println!("Length: {}", len),
Err(_) => println!("Thread is being destroyed"),
}
}use std::thread;
use std::cell::RefCell;
use std::collections::HashMap;
thread_local!(static CACHE: RefCell<HashMap<u64, u64>> = RefCell::new(HashMap::new()));
fn expensive_computation(n: u64) -> u64 {
CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
if let Some(&result) = cache.get(&n) {
return result;
}
// Simulate expensive computation
let result = n * n + n + 1;
cache.insert(n, result);
result
})
}
fn main() {
println!("Compute 5: {}", expensive_computation(5));
println!("Compute 5 again: {}", expensive_computation(5)); // Cached
println!("Compute 10: {}", expensive_computation(10));
}Thread Local Macro:
// With immutable value
thread_local!(static NAME: Type = value);
// With mutable value (RefCell)
thread_local!(static NAME: RefCell<Type> = RefCell::new(value));
// With simple mutable value (Cell)
thread_local!(static NAME: Cell<Type> = Cell::new(value));LocalKey Methods:
| Method | Description |
|--------|-------------|
| with(f) | Access value through closure |
| try_with(f) | Like with, but returns Result |
When to Use RefCell vs Cell:
| Type | Use Case |
|------|----------|
| Cell<T> | Simple Copy types, replacement |
| RefCell<T> | Complex types, need references |
Common Patterns:
| Pattern | Description | |---------|-------------| | Per-thread buffer | Avoid allocation in hot paths | | Per-thread RNG | Thread-safe random numbers | | Per-thread context | Request tracking | | Per-thread metrics | Thread-local counters | | Memoization cache | Per-thread result caching | | Recursion tracking | Track call depth |
Key Points:
RefCell for mutable data, Cell for simple valueswith() closuretry_with() handles thread destruction edge caseArc for thread-safe shared references