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 unstaticatomic. - Folosești
RefCell::borrow_mutde două ori în același timp în același fir (de exemplu o funcție apelată dinwith_borrow_mutcare face din nouwith_borrow_mut): panicăalready borrowed. - Te bazezi pe
Drop-ul valorilor dinthread_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.