ExamplesBy LevelBy TopicLearning Paths
459 Fundamental

459: Thread-Local Storage

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "459: Thread-Local Storage" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functional Programming. Some per-request or per-thread state shouldn't be shared: random number generator seeds, per-thread error codes, per-thread profiling counters, locale settings. Key difference from OCaml: 1. **Ergonomics**: Rust's `thread_local!` is a language

Tutorial

The Problem

Some per-request or per-thread state shouldn't be shared: random number generator seeds, per-thread error codes, per-thread profiling counters, locale settings. Global shared state requires synchronization; passing context through every function is verbose. Thread-local storage (TLS) provides a third option: each thread has its own independent copy of a variable, accessible without synchronization. Accessing TLS is as fast as a local variable with OS thread support, and Rust's thread_local! makes it safe and ergonomic.

TLS appears in Rust's panic handling (PANIC_COUNT), allocator state, async executor task-local values, and per-thread performance counters.

🎯 Learning Outcomes

  • • Understand how thread_local! creates per-thread variable storage
  • • Learn how ThreadLocalKey::with(|val| ...) provides safe access to TLS values
  • • See how Cell<T> (for Copy types) and RefCell<T> (for complex types) work in TLS
  • • Understand why TLS values can't escape their thread (they're !Send)
  • • Learn the initialization: TLS values initialize on first access per thread
  • Code Example

    thread_local! {
        static COUNTER: Cell<usize> = Cell::new(0);
    }
    
    COUNTER.with(|c| {
        c.set(c.get() + 1);
    });

    Key Differences

  • Ergonomics: Rust's thread_local! is a language-level macro with clear semantics; OCaml 5.x's Domain.DLS requires explicit key creation and lookup.
  • Cell types: Rust uses Cell<T> or RefCell<T> for TLS to provide interior mutability; OCaml uses ref values which are always mutable.
  • Lifetime: Rust's TLS values live for the thread's duration; OCaml 5.x's Domain.DLS values live for the domain's duration.
  • Non-escaping: Rust's with callback prevents TLS references from escaping the thread; OCaml has no such enforcement.
  • OCaml Approach

    OCaml 4.x uses let state = ref initial_value per thread — module-level references are per-thread since each thread has its own OCaml runtime state in Thread contexts (this is subtler in OCaml 4.x). OCaml 5.x provides Domain.DLS.get/set (domain-local storage) as the explicit per-domain storage mechanism, analogous to thread-local storage for domains.

    Full Source

    #![allow(clippy::all)]
    //! # Thread-Local Storage — Per-Thread State
    //!
    //! Each thread gets its own copy of thread-local data.
    
    use std::cell::{Cell, RefCell};
    use std::sync::Arc;
    use std::thread;
    
    // Thread-local counter
    thread_local! {
        static COUNTER: Cell<usize> = const { Cell::new(0) };
    }
    
    // Thread-local string buffer
    thread_local! {
        static BUFFER: RefCell<String> = RefCell::new(String::new());
    }
    
    /// Increment the thread-local counter
    pub fn increment_counter() -> usize {
        COUNTER.with(|c| {
            let val = c.get() + 1;
            c.set(val);
            val
        })
    }
    
    /// Get the thread-local counter value
    pub fn get_counter() -> usize {
        COUNTER.with(|c| c.get())
    }
    
    /// Append to the thread-local buffer
    pub fn append_buffer(s: &str) {
        BUFFER.with(|b| {
            b.borrow_mut().push_str(s);
        });
    }
    
    /// Get the thread-local buffer contents
    pub fn get_buffer() -> String {
        BUFFER.with(|b| b.borrow().clone())
    }
    
    /// Clear the thread-local buffer
    pub fn clear_buffer() {
        BUFFER.with(|b| {
            b.borrow_mut().clear();
        });
    }
    
    /// Demonstrate thread-local isolation
    pub fn thread_local_isolation(num_threads: usize, increments: usize) -> Vec<usize> {
        let results = Arc::new(std::sync::Mutex::new(Vec::new()));
    
        let handles: Vec<_> = (0..num_threads)
            .map(|_| {
                let results = Arc::clone(&results);
                thread::spawn(move || {
                    // Each thread starts with counter = 0
                    for _ in 0..increments {
                        increment_counter();
                    }
                    let final_count = get_counter();
                    results.lock().unwrap().push(final_count);
                })
            })
            .collect();
    
        for h in handles {
            h.join().unwrap();
        }
    
        Arc::try_unwrap(results).unwrap().into_inner().unwrap()
    }
    
    /// Thread-local random number generator pattern
    pub mod rng {
        use std::cell::Cell;
    
        thread_local! {
            static SEED: Cell<u64> = Cell::new(12345);
        }
    
        pub fn next_u64() -> u64 {
            SEED.with(|s| {
                // Simple LCG
                let x = s.get().wrapping_mul(6364136223846793005).wrapping_add(1);
                s.set(x);
                x
            })
        }
    
        pub fn seed(value: u64) {
            SEED.with(|s| s.set(value));
        }
    }
    
    /// Thread-local allocation tracking
    pub mod alloc_tracking {
        use std::cell::Cell;
    
        thread_local! {
            static ALLOCATIONS: Cell<usize> = const { Cell::new(0) };
            static BYTES: Cell<usize> = const { Cell::new(0) };
        }
    
        pub fn record_allocation(bytes: usize) {
            ALLOCATIONS.with(|a| a.set(a.get() + 1));
            BYTES.with(|b| b.set(b.get() + bytes));
        }
    
        pub fn get_stats() -> (usize, usize) {
            (ALLOCATIONS.with(|a| a.get()), BYTES.with(|b| b.get()))
        }
    
        pub fn reset() {
            ALLOCATIONS.with(|a| a.set(0));
            BYTES.with(|b| b.set(0));
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_counter_basic() {
            // Reset for this test
            COUNTER.with(|c| c.set(0));
    
            assert_eq!(get_counter(), 0);
            increment_counter();
            assert_eq!(get_counter(), 1);
            increment_counter();
            assert_eq!(get_counter(), 2);
        }
    
        #[test]
        fn test_buffer() {
            clear_buffer();
    
            append_buffer("hello");
            append_buffer(" world");
            assert_eq!(get_buffer(), "hello world");
    
            clear_buffer();
            assert_eq!(get_buffer(), "");
        }
    
        #[test]
        fn test_thread_isolation() {
            let results = thread_local_isolation(4, 100);
    
            // Each thread should have counted to 100 independently
            for r in results {
                assert_eq!(r, 100);
            }
        }
    
        #[test]
        fn test_rng() {
            rng::seed(42);
            let a = rng::next_u64();
            let b = rng::next_u64();
            assert_ne!(a, b);
    
            // Reseeding gives same sequence
            rng::seed(42);
            assert_eq!(rng::next_u64(), a);
        }
    
        #[test]
        fn test_alloc_tracking() {
            alloc_tracking::reset();
    
            alloc_tracking::record_allocation(100);
            alloc_tracking::record_allocation(200);
    
            let (count, bytes) = alloc_tracking::get_stats();
            assert_eq!(count, 2);
            assert_eq!(bytes, 300);
    
            alloc_tracking::reset();
            let (count, bytes) = alloc_tracking::get_stats();
            assert_eq!(count, 0);
            assert_eq!(bytes, 0);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_counter_basic() {
            // Reset for this test
            COUNTER.with(|c| c.set(0));
    
            assert_eq!(get_counter(), 0);
            increment_counter();
            assert_eq!(get_counter(), 1);
            increment_counter();
            assert_eq!(get_counter(), 2);
        }
    
        #[test]
        fn test_buffer() {
            clear_buffer();
    
            append_buffer("hello");
            append_buffer(" world");
            assert_eq!(get_buffer(), "hello world");
    
            clear_buffer();
            assert_eq!(get_buffer(), "");
        }
    
        #[test]
        fn test_thread_isolation() {
            let results = thread_local_isolation(4, 100);
    
            // Each thread should have counted to 100 independently
            for r in results {
                assert_eq!(r, 100);
            }
        }
    
        #[test]
        fn test_rng() {
            rng::seed(42);
            let a = rng::next_u64();
            let b = rng::next_u64();
            assert_ne!(a, b);
    
            // Reseeding gives same sequence
            rng::seed(42);
            assert_eq!(rng::next_u64(), a);
        }
    
        #[test]
        fn test_alloc_tracking() {
            alloc_tracking::reset();
    
            alloc_tracking::record_allocation(100);
            alloc_tracking::record_allocation(200);
    
            let (count, bytes) = alloc_tracking::get_stats();
            assert_eq!(count, 2);
            assert_eq!(bytes, 300);
    
            alloc_tracking::reset();
            let (count, bytes) = alloc_tracking::get_stats();
            assert_eq!(count, 0);
            assert_eq!(bytes, 0);
        }
    }

    Deep Comparison

    Thread-Local Storage

    OCaml

    (* No built-in TLS; use Domain.DLS *)
    let key = Domain.DLS.new_key (fun () -> ref 0)
    let () = Domain.DLS.get key := 42
    

    Rust

    thread_local! {
        static COUNTER: Cell<usize> = Cell::new(0);
    }
    
    COUNTER.with(|c| {
        c.set(c.get() + 1);
    });
    

    Key Differences

    FeatureOCamlRust
    SyntaxDomain.DLSthread_local! macro
    Type'a keyStatic with Cell/RefCell
    CleanupOn domain exitOn thread exit

    Exercises

  • Per-thread RNG: Create thread_local! { static RNG: RefCell<XorShift> = ... } where XorShift is a simple random number generator seeded with the thread ID. Verify that different threads produce different random sequences.
  • Allocation tracking: Use thread_local! { static ALLOC_COUNT: Cell<usize> = const { Cell::new(0) } } to count allocations per thread. Wrap allocation-using code and verify counts are independent per thread.
  • Context propagation: Implement a "request context" pattern: thread_local! { static REQUEST_ID: Cell<u64> = const { Cell::new(0) } }. Write with_request(id, || ...) that sets the ID for the duration of the closure, then restores it — enabling implicit context propagation through function calls.
  • Open Source Repos