thread_local în C++

Fiecare fir de execuție (thread) primește propria copie a variabilei, creată când pornește firul și distrusă la final.

Sintaxă
thread_local int contor = 0;

Ce face thread_local

O variabilă globală obișnuită este comună tuturor firelor de execuție, deci accesul simultan trebuie protejat. O variabilă thread_local are câte o copie separată pentru fiecare fir: fiecare lucrează pe a lui, fără conflicte și fără std::mutex.

Se poate folosi pentru variabile globale, membri static ai claselor și variabile static locale. Copia unui fir este inițializată cel târziu la prima folosire în acel fir și distrusă când firul se termină.

Exemple tipice: un generator de numere aleatorii pentru fiecare fir, un contor de operații per fir, un tampon (buffer) temporar refolosit.

Exemple

Exemplul 1: Fiecare fir are propriul contor

Ambele fire cresc „același” contor, dar fiecare are copia lui, deci fiecare ajunge la exact numărul lui de pași. Firul principal (main) are și el o copie, rămasă la 0. Rezultatele sunt salvate și afișate după join, ca ordinea afișării să nu depindă de fire.

#include <functional>
#include <print>
#include <thread>

thread_local int contor = 0;

void lucreaza(int pasi, int& rezultat) {
    for (int i = 0; i < pasi; ++i) {
        ++contor;
    }
    rezultat = contor;
}

int main() {
    int r1 = 0, r2 = 0;
    std::thread a(lucreaza, 1000, std::ref(r1));
    std::thread b(lucreaza, 500, std::ref(r2));
    a.join();
    b.join();
    std::println("Firul a: {}", r1);
    std::println("Firul b: {}", r2);
    std::println("Firul principal: {}", contor);
}

Rezultat

Firul a: 1000
Firul b: 500
Firul principal: 0

Exemplul 2: Comparație cu o variabilă comună

comun este o variabilă std::atomic, comună tuturor firelor, deci adună pașii ambelor fire. propriu este separat pentru fiecare fir.

#include <atomic>
#include <functional>
#include <print>
#include <thread>

std::atomic<int> comun = 0;
thread_local int propriu = 0;

void lucreaza(int& rezultat) {
    for (int i = 0; i < 100; ++i) {
        ++comun;
        ++propriu;
    }
    rezultat = propriu;
}

int main() {
    int r1 = 0, r2 = 0;
    std::thread a(lucreaza, std::ref(r1));
    std::thread b(lucreaza, std::ref(r2));
    a.join();
    b.join();
    std::println("comun = {}, propriu: {} și {}", comun.load(), r1, r2);
}

Rezultat

comun = 200, propriu: 100 și 100

Greșeli frecvente

  • Trimiți adresa unei variabile thread_local altui fir și o folosești după ce firul proprietar s-a terminat. Copia lui a fost distrusă, deci accesul este comportament nedefinit.
  • Te aștepți ca o valoare setată în main într-o variabilă thread_local să fie văzută de firele noi. Fiecare fir nou pornește cu propria copie, inițializată din nou.

Comenzi înrudite

  • static: Are trei sensuri: variabilă locală care își păstrează valoarea între apeluri, membru comun al clasei sau nume vizibil doar în fișierul curent.
  • constinit: Garantează că o variabilă globală sau statică este inițializată la compilare, dar fără să o facă constantă.
  • volatile: Spune compilatorului că valoarea se poate schimba din afara programului, deci fiecare citire și scriere trebuie făcută efectiv.
  • std::thread: Pornește o funcție pe un fir de execuție separat, care rulează în paralel cu restul programului.
  • std::atomic: Variabilă care poate fi citită și modificată sigur de mai multe fire, fără mutex.