Was bedeutet thread_local in C ++ 11?

130

Ich bin verwirrt mit der Beschreibung thread_localin C ++ 11. Nach meinem Verständnis verfügt jeder Thread über eine eindeutige Kopie lokaler Variablen in einer Funktion. Auf die globalen / statischen Variablen kann von allen Threads zugegriffen werden (möglicherweise synchronisierter Zugriff über Sperren). Und die thread_localVariablen sind für alle Threads sichtbar, können aber nur von dem Thread geändert werden, für den sie definiert sind? Ist es richtig?

Polapts
quelle

Antworten:

149

Thread-lokale Speicherdauer ist ein Begriff, der verwendet wird, um Daten zu bezeichnen, die scheinbar global oder statisch gespeichert sind (aus Sicht der Funktionen, die sie verwenden), aber tatsächlich gibt es eine Kopie pro Thread.

Es ergänzt die aktuelle automatische (existiert während eines Blocks / einer Funktion), statische (existiert für die Programmdauer) und dynamische (existiert auf dem Heap zwischen Zuweisung und Freigabe).

Etwas Thread-Lokales wird bei der Thread-Erstellung ins Leben gerufen und beim Stoppen des Threads entsorgt.

Es folgen einige Beispiele.

Stellen Sie sich einen Zufallszahlengenerator vor, bei dem der Startwert pro Thread verwaltet werden muss. Die Verwendung eines threadlokalen Startwerts bedeutet, dass jeder Thread unabhängig von anderen Threads eine eigene Zufallszahlenfolge erhält.

Wenn Ihr Startwert eine lokale Variable innerhalb der Zufallsfunktion wäre, würde er jedes Mal initialisiert, wenn Sie ihn aufrufen, und Ihnen jedes Mal dieselbe Nummer geben. Wenn es global wäre, würden Threads die Sequenzen des anderen stören.

Ein anderes Beispiel ist so etwas wie strtokdas Speichern des Tokenisierungsstatus auf einer threadspezifischen Basis. Auf diese Weise kann ein einzelner Thread sicher sein, dass andere Threads ihre Tokenisierungsbemühungen nicht vermasseln, während der Status über mehrere Aufrufe an beibehalten werden kann strtok- dies macht strtok_r(die thread-sichere Version) im Grunde genommen überflüssig.

Diese beiden Beispiele erlauben die Gewinde lokale Variable zu existieren innerhalb der Funktion , die verwendet es. In Code vor dem Thread ist dies einfach eine statische Variable für die Speicherdauer innerhalb der Funktion. Bei Threads wird dies geändert, um die lokale Speicherdauer des Threads zu gewährleisten.

Ein weiteres Beispiel wäre so etwas wie errno. Sie möchten nicht, dass separate Threads geändert werden, errnonachdem einer Ihrer Aufrufe fehlgeschlagen ist, aber bevor Sie die Variable überprüfen können, und Sie möchten dennoch nur eine Kopie pro Thread.

Diese Site enthält eine angemessene Beschreibung der verschiedenen Speicherdauer-Spezifizierer.

paxdiablo
quelle
4
Die Verwendung von thread local löst die Probleme mit nicht strtok. strtokist sogar in einer einzelnen Thread-Umgebung defekt.
James Kanze
11
Entschuldigung, lassen Sie mich das umformulieren. Es bringt keine neuen Probleme mit strtok :-)
paxdiablo
7
Eigentlich rsteht das für "Wiedereinsteiger", was nichts mit Thread-Sicherheit zu tun hat. Es ist wahr, dass Sie einige Dinge mit thread-lokalem Speicher threadsicher machen können, aber Sie können sie nicht wieder einführen lassen.
Kerrek SB
5
In einer Single-Threaded-Umgebung müssen Funktionen nur dann neu eingegeben werden, wenn sie Teil eines Zyklus im Aufrufdiagramm sind. Eine Blattfunktion (eine, die keine anderen Funktionen aufruft) ist per Definition nicht Teil eines Zyklus, und es gibt keinen guten Grund, warum strtokandere Funktionen aufgerufen werden sollten.
MSalters
3
das würde es vermasseln: while (something) { char *next = strtok(whatever); someFunction(next); // someFunction calls strtok }
japreiss
134

Wenn Sie eine Variable deklarieren, thread_localhat jeder Thread eine eigene Kopie. Wenn Sie namentlich darauf verweisen, wird die dem aktuellen Thread zugeordnete Kopie verwendet. z.B

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;
}

Dieser Code gibt "2349", "3249", "4239", "4329", "2439" oder "3429" aus, aber niemals etwas anderes. Jeder Thread hat eine eigene Kopie von i, die zugewiesen, inkrementiert und dann gedruckt wird. Der laufende Thread mainhat auch eine eigene Kopie, die zu Beginn zugewiesen und dann unverändert gelassen wird. Diese Kopien sind völlig unabhängig und haben jeweils eine andere Adresse.

Diesbezüglich ist nur der Name besonders - wenn Sie die Adresse einer thread_localVariablen verwenden, haben Sie nur einen normalen Zeiger auf ein normales Objekt, das Sie frei zwischen Threads übertragen können. z.B

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;
}

Da die Adresse von ian die Thread-Funktion übergeben wird, kann die Kopie der iZugehörigkeit zum Haupt-Thread zugewiesen werden, obwohl dies der Fall ist thread_local. Dieses Programm gibt somit "42" aus. Wenn Sie dies tun, müssen Sie darauf achten, dass *pnach dem Beenden des Threads, zu dem er gehört, nicht darauf zugegriffen wird. Andernfalls erhalten Sie einen baumelnden Zeiger und ein undefiniertes Verhalten, genau wie in jedem anderen Fall, in dem das Objekt, auf das verwiesen wird, zerstört wird.

thread_localVariablen werden "vor der ersten Verwendung" initialisiert. Wenn sie also nie von einem bestimmten Thread berührt werden, werden sie nicht unbedingt jemals initialisiert. Auf diese Weise können Compiler vermeiden, dass jede thread_localVariable im Programm für einen Thread erstellt wird, der vollständig in sich geschlossen ist und keinen von ihnen berührt. z.B

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();
}

In diesem Programm gibt es 2 Threads: den Haupt-Thread und den manuell erstellten Thread. Keiner der Threads wird aufgerufen f, daher wird das thread_localObjekt niemals verwendet. Es ist daher nicht spezifiziert, ob der Compiler 0, 1 oder 2 Instanzen von erstellt my_class, und die Ausgabe kann "", "hellohellogoodbyegoodbye" oder "hellogoodbye" sein.

Anthony Williams
quelle
1
Ich denke, es ist wichtig zu beachten, dass die thread-lokale Kopie der Variablen eine neu initialisierte Kopie der Variablen ist. Das heißt, wenn Sie einen hinzufügen g()Aufruf an den Anfang threadFunc, dann wird der Ausgang sein 0304029einige andere Permutation der Paare oder 02, 03und 04. Das heißt, obwohl 9 zugewiesen wurde, ibevor die Threads erstellt wurden, erhalten die Threads eine frisch erstellte Kopie von iwhere i=0. Wenn imit zugewiesen thread_local int i = random_integer(), erhält jeder Thread eine neue zufällige Ganzzahl.
Mark H
Nicht gerade eine Permutation 02, 03, 04, kann es andere Sequenzen wie020043
Hongxu Chen
Interessanter Leckerbissen, den ich gerade gefunden habe: GCC unterstützt die Verwendung der Adresse einer thread_local-Variablen als Vorlagenargument, andere Compiler jedoch nicht (zum Zeitpunkt dieses Schreibens; versuchtes Clang, vstudio). Ich bin mir nicht sicher, was der Standard dazu zu sagen hat oder ob dies ein nicht spezifizierter Bereich ist.
jwd
23

Thread-lokaler Speicher ist in jeder Hinsicht wie statischer (= globaler) Speicher, nur dass jeder Thread eine separate Kopie des Objekts hat. Die Lebensdauer des Objekts beginnt entweder beim Thread-Start (für globale Variablen) oder bei der ersten Initialisierung (für blocklokale Statik) und endet, wenn der Thread endet (dh wannjoin() aufgerufen wird).

Folglich können nur Variablen deklariert staticwerden , die ebenfalls deklariert werden könntenthread_local , dh globale Variablen (genauer: Variablen "im Namespace-Bereich"), statische Klassenmitglieder und blockstatische Variablen (in diesem Fall staticimpliziert).

Angenommen, Sie haben einen Thread-Pool und möchten wissen, wie gut Ihre Arbeitslast ausgeglichen wurde:

thread_local Counter c;

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

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

Dies würde Thread-Nutzungsstatistiken drucken, z. B. mit einer Implementierung wie dieser:

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
quelle