thread_local! în Rust

Macro-uri de verificare și panică Rust 1.0 (inițializare const { }: 1.59; metodele get/set/with_borrow: 1.73)

Declară variabile statice de care fiecare fir de execuție are propria copie, inițializată la prima folosire în acel fir.

Sintaxă
thread_local! {
    static NUME: Cell<u32> = const { Cell::new(0) };
    static ALT: RefCell<Vec<String>> = RefCell::new(Vec::new());
}
NUME.with(|v| ...);
NUME.set(5);  NUME.get();                  // pentru Cell
ALT.with_borrow_mut(|v| v.push(...));      // pentru RefCell

Ce face thread_local!

O variabilă declarată în thread_local! arată ca un static, dar fiecare fir de execuție are propria valoare. Modificările făcute de un fir nu se văd în celelalte, deci nu e nevoie de Mutex sau de tipuri atomice. Valoarea este creată la prima folosire în fiecare fir și distrusă când firul se termină.

Accesul se face prin metode, nu direct: NUME.with(|v| ...) dă o referință la valoarea firului curent. Pentru valori modificabile se folosește Cell (valori Copy: get, set) sau RefCell (orice valoare: with_borrow, with_borrow_mut).

Forma = const { ... } spune că valoarea inițială se poate calcula la compilare, ceea ce face accesul mai rapid. Exemple de folosire: contoare sau statistici per fir, un generator de numere aleatorii per fir, un tampon reutilizat.

Exemple

Exemplul 1: Fiecare fir are contorul lui

Cele trei fire incrementează același CONTOR, dar fiecare își vede doar propria copie, deci fiecare ajunge la numărul lui de pași. Contorul firului main rămâne 0. Rezultatele sunt colectate prin join, ca ordinea să fie fixă.

use std::cell::Cell;
use std::thread;

thread_local! {
    static CONTOR: Cell<u32> = const { Cell::new(0) };
}

fn proceseaza(pasi: u32) -> u32 {
    for _ in 0..pasi {
        CONTOR.set(CONTOR.get() + 1);
    }
    CONTOR.get()
}

fn main() {
    let fire: Vec<_> = [3, 5, 2]
        .into_iter()
        .map(|pasi| thread::spawn(move || proceseaza(pasi)))
        .collect();
    for (i, fir) in fire.into_iter().enumerate() {
        println!("firul {i}: contor = {}", fir.join().unwrap());
    }
    println!("main: contor = {}", CONTOR.get());
}

Rezultat

firul 0: contor = 3
firul 1: contor = 5
firul 2: contor = 2
main: contor = 0

Exemplul 2: Un jurnal per fir cu RefCell

Fiecare fir adaugă mesaje în propriul vector, apoi îl returnează. with_borrow_mut dă acces de modificare, take() scoate vectorul și lasă unul gol în loc.

use std::cell::RefCell;
use std::thread;

thread_local! {
    static JURNAL: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
}

fn noteaza(mesaj: String) {
    JURNAL.with_borrow_mut(|j| j.push(mesaj));
}

fn main() {
    let fire: Vec<_> = ["Iași", "Cluj"]
        .into_iter()
        .map(|oras| {
            thread::spawn(move || {
                noteaza(format!("încep {oras}"));
                noteaza(format!("termin {oras}"));
                JURNAL.take()
            })
        })
        .collect();
    for fir in fire {
        println!("{:?}", fir.join().unwrap());
    }
    noteaza("doar în main".to_string());
    println!("main: {} mesaj(e)", JURNAL.with_borrow(|j| j.len()));
}

Rezultat

["încep Iași", "termin Iași"]
["încep Cluj", "termin Cluj"]
main: 1 mesaj(e)

Greșeli frecvente

  • Te aștepți ca valoarea modificată într-un fir să fie văzută în altul. Pentru date comune ai nevoie de Arc<Mutex<T>> sau de un static atomic.
  • Folosești RefCell::borrow_mut de două ori în același timp în același fir (de exemplu o funcție apelată din with_borrow_mut care face din nou with_borrow_mut): panică already borrowed.
  • Te bazezi pe Drop-ul valorilor din thread_local! în firul principal: la ieșirea din program, nu este garantat că se apelează.

Comenzi înrudite

  • const și static: const definește o valoare fixă calculată la compilare; static definește o variabilă globală cu o singură adresă, care trăiește tot programul.
  • closures și move: Funcții anonime, scrise cu |parametri|, care pot folosi variabilele din jur. move le dă proprietatea asupra acelor variabile.
  • cfg!: Evaluează la compilare o condiție de configurare (sistem de operare, mod debug, feature Cargo) și o transformă în true sau false.