Co oznacza thread_local w C ++ 11?

135

Jestem zdezorientowany z opisem thread_localw C ++ 11. Rozumiem, że każdy wątek ma unikalną kopię zmiennych lokalnych w funkcji. Dostęp do zmiennych globalnych / statycznych można uzyskać we wszystkich wątkach (prawdopodobnie dostęp zsynchronizowany za pomocą blokad). A thread_localzmienne są widoczne dla wszystkich wątków, ale mogą być modyfikowane tylko przez wątek, dla którego są zdefiniowane? Czy to jest poprawne?

polapts
źródło

Odpowiedzi:

159

Lokalny czas trwania wątku to termin używany w odniesieniu do danych, które wydają się być globalnym lub statycznym czasem przechowywania (z punktu widzenia funkcji, które go używają), ale w rzeczywistości istnieje jedna kopia na wątek.

Dodaje do aktualnego automatycznego (istnieje podczas bloku / funkcji), statycznego (istnieje przez czas trwania programu) i dynamicznego (istnieje na stercie między alokacją a zwolnieniem).

Coś, co jest lokalne dla wątku, powstaje podczas tworzenia wątku i jest usuwane, gdy wątek się zatrzymuje.

Oto kilka przykładów.

Pomyśl o generatorze liczb losowych, w którym ziarno musi być utrzymywane na podstawie wątku. Użycie lokalnego ziarna wątku oznacza, że ​​każdy wątek otrzymuje własną sekwencję liczb losowych, niezależną od innych wątków.

Jeśli twoje ziarno było lokalną zmienną w funkcji losowej, byłoby inicjalizowane za każdym razem, gdy ją wywołasz, dając ci za każdym razem tę samą liczbę. Gdyby był globalny, wątki interferowałyby ze sobą w sekwencjach.

Innym przykładem jest strtoksytuacja, w której stan tokenizacji jest przechowywany na podstawie określonego wątku. W ten sposób pojedynczy wątek może mieć pewność, że inne wątki nie zepsują jego wysiłków związanych z tokenizacją, a jednocześnie będą w stanie utrzymać stan wielu wywołań strtok- to w zasadzie powoduje, że strtok_r(wersja bezpieczna dla wątków) jest zbędna.

Oba te przykłady pozwalają na istnienie zmiennej lokalnej wątku w funkcji, która jej używa. W kodzie wstępnie wątkowym byłaby to po prostu statyczna zmienna czasu przechowywania w ramach funkcji. W przypadku wątków jest to modyfikowane do czasu trwania lokalnego magazynu wątków.

Jeszcze innym przykładem może być coś podobnego errno. Nie chcesz, aby oddzielne wątki modyfikowały się errnopo niepowodzeniu jednego z wywołań, ale zanim będziesz mógł sprawdzić zmienną, a mimo to chcesz mieć tylko jedną kopię na wątek.

Ta witryna zawiera rozsądny opis różnych specyfikacji czasu przechowywania.

paxdiablo
źródło
4
Korzystanie z wątku lokalnego nie rozwiązuje problemów z plikami strtok. strtokjest zepsuty nawet w środowisku jednowątkowym.
James Kanze,
11
Przepraszam, pozwól mi to przeformułować. Nie wprowadza żadnych nowych problemów ze strtokiem :-)
paxdiablo
8
Właściwie roznacza to „re-entrant”, co nie ma nic wspólnego z bezpieczeństwem nici. Prawdą jest, że możesz sprawić, by niektóre rzeczy działały bezpiecznie wątkowo dzięki pamięci lokalnej wątkowej, ale nie możesz ich ponownie wprowadzić.
Kerrek SB,
6
W środowisku jednowątkowym funkcje muszą być ponownie wprowadzane tylko wtedy, gdy są częścią cyklu w grafie wywołań. Funkcja liścia (taka, która nie wywołuje innych funkcji) z definicji nie jest częścią cyklu i nie ma dobrego powodu, dla którego miałaby strtokwywoływać inne funkcje.
MSalters
5
to by to while (something) { char *next = strtok(whatever); someFunction(next); // someFunction calls strtok }
zepsuło
139

Kiedy deklarujesz zmienną, thread_localkażdy wątek ma swoją własną kopię. Kiedy odnosisz się do niego po nazwie, używana jest kopia skojarzona z bieżącym wątkiem. na przykład

thread_local int i=0;

void f(int newval){
    i=newval;
}

void g(){
    std::cout<<i;
}

void threadfunc(int id){
    f(id);
    ++i;
    g();
}

int main(){
    i=9;
    std::thread t1(threadfunc,1);
    std::thread t2(threadfunc,2);
    std::thread t3(threadfunc,3);

    t1.join();
    t2.join();
    t3.join();
    std::cout<<i<<std::endl;
}

Ten kod zwróci „2349”, „3249”, „4239”, „4329”, „2439” lub „3429”, ale nigdy więcej. Każdy wątek ma swoją własną kopię i, do której jest przypisywana, zwiększana, a następnie drukowana. Działający wątek mainma również swoją własną kopię, która jest przypisywana na początku, a następnie pozostawiana bez zmian. Kopie te są całkowicie niezależne i każda ma inny adres.

Jedynie nazwa jest pod tym względem wyjątkowa - jeśli bierzesz adres thread_localzmiennej, masz po prostu normalny wskaźnik do normalnego obiektu, który możesz swobodnie przekazywać między wątkami. na przykład

thread_local int i=0;

void thread_func(int*p){
    *p=42;
}

int main(){
    i=9;
    std::thread t(thread_func,&i);
    t.join();
    std::cout<<i<<std::endl;
}

Ponieważ adres ijest przekazywany do funkcji iwątku, można przypisać kopię przynależności do wątku głównego, mimo że tak jest thread_local. Ten program zwróci zatem „42”. Jeśli to zrobisz, musisz uważać, aby *pnie uzyskać dostępu po wyjściu wątku, do którego należy, w przeciwnym razie otrzymasz wiszący wskaźnik i niezdefiniowane zachowanie, tak jak w każdym innym przypadku, gdy wskazany obiekt zostanie zniszczony.

thread_localzmienne są inicjalizowane „przed pierwszym użyciem”, więc jeśli nigdy nie są dotykane przez dany wątek, to niekoniecznie są one nigdy inicjalizowane. Ma to pozwolić kompilatorom na uniknięcie konstruowania każdej thread_localzmiennej w programie dla wątku, który jest całkowicie niezależny i nie dotyka żadnego z nich. na przykład

struct my_class{
    my_class(){
        std::cout<<"hello";
    }
    ~my_class(){
        std::cout<<"goodbye";
    }
};

void f(){
    thread_local my_class unused;
}

void do_nothing(){}

int main(){
    std::thread t1(do_nothing);
    t1.join();
}

W tym programie są 2 wątki: wątek główny i wątek utworzony ręcznie. Żaden wątek nie jest wywoływany f, więc thread_localobiekt nigdy nie jest używany. W związku z tym nie jest określone, czy kompilator skonstruuje 0, 1 czy 2 wystąpienia my_class, a wyjściem może być „”, „hellohellogoodbyegoodbye” lub „hellogoodbye”.

Anthony Williams
źródło
1
Myślę, że należy zauważyć, że lokalna kopia zmiennej dla wątku jest nowo zainicjowaną kopią zmiennej. Oznacza to, że jeśli dodać g()wezwanie do początku threadFunc, to wyjście będzie 0304029albo jakiś inny permutacji par 02, 03oraz 04. Oznacza to, że nawet jeśli 9 jest przypisane do iprzed utworzeniem wątków, wątki otrzymują świeżo skonstruowaną kopię igdzie i=0. Jeśli ijest przypisane za pomocą thread_local int i = random_integer(), każdy wątek otrzymuje nową losową liczbę całkowitą.
Mark H
Niezupełnie permutacją 02, 03, 04, mogą istnieć inne sekwencje, takie jak020043
Hongxu Chen
Ciekawa ciekawostka, którą właśnie znalazłem: GCC obsługuje używanie adresu zmiennej thread_local jako argumentu szablonu, ale inne kompilatory nie (w chwili pisania tego tekstu; próbował clang, vstudio). Nie jestem pewien, co ma do powiedzenia na ten temat norma lub czy jest to obszar nieokreślony.
jwd
23

Pamięć lokalna wątku jest pod każdym względem podobna do pamięci statycznej (= globalnej), z tą różnicą, że każdy wątek ma oddzielną kopię obiektu. Czas życia obiektu zaczyna się albo na początku wątku (dla zmiennych globalnych), albo przy pierwszej inicjalizacji (dla statystyki lokalnej bloku) i kończy się, gdy wątek się kończy (tj. Kiedy join()jest wywoływany).

W konsekwencji tylko zmienne, które mogą być również zadeklarowane, staticmogą być zadeklarowane jako thread_local, tj. Zmienne globalne (a dokładniej: zmienne „w zakresie przestrzeni nazw”), statyczne składowe klas i zmienne statyczne bloku (w tym przypadku staticjest to implikowane).

Na przykład załóżmy, że masz pulę wątków i chcesz wiedzieć, jak dobrze równoważono obciążenie pracą:

thread_local Counter c;

void do_work()
{
    c.increment();
    // ...
}

int main()
{
    std::thread t(do_work);   // your thread-pool would go here
    t.join();
}

Spowoduje to wydrukowanie statystyk użycia wątku, np. W takiej implementacji:

struct Counter
{
     unsigned int c = 0;
     void increment() { ++c; }
     ~Counter()
     {
         std::cout << "Thread #" << std::this_thread::id() << " was called "
                   << c << " times" << std::endl;
     }
};
Kerrek SB
źródło