Frage Was bedeutet thread_local in C ++ 11?


Ich bin verwirrt mit der Beschreibung von thread_local in C ++ 11. Mein Verständnis ist, jeder Thread hat eine eindeutige Kopie von lokalen Variablen in einer Funktion. Auf die globalen / statischen Variablen kann von allen Threads zugegriffen werden (möglicherweise synchronisierter Zugriff unter Verwendung von Sperren). Und die Variablen thread_local sind für alle Threads sichtbar, können aber nur durch den Thread geändert werden, für den sie definiert sind. Ist es richtig?


75
2017-08-16 09:05


Ursprung


Antworten:


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

Es wird zur aktuellen Automatik hinzugefügt (existiert während eines Blocks / einer Funktion), statisch (existiert für die Programmdauer) und dynamisch (existiert auf dem Heap zwischen Zuweisung und Freigabe).

Etwas Thread-Local wird bei der Thread-Erstellung erzeugt und entsorgt, wenn der Thread aufhört.

Einige Beispiele folgen.

Stellen Sie sich einen Zufallszahlengenerator vor, bei dem der Seed auf einer Thread-Basis gepflegt werden muss. Die Verwendung eines threadlokalen Seeds bedeutet, dass jeder Thread unabhängig von anderen Threads eine eigene Zufallszahlenfolge erhält.

Wenn Ihr Seed eine lokale Variable innerhalb der Zufallsfunktion ist, wird sie jedes Mal initialisiert, wenn Sie sie aufgerufen haben, und Sie erhalten jedes Mal dieselbe Nummer. Wenn es sich um einen globalen Thread handelte, würden sich die Threads gegenseitig stören.

Ein anderes Beispiel ist etwas wie strtok wobei der Tokenisierungszustand auf einer Thread-spezifischen Basis gespeichert wird. Auf diese Weise kann ein einzelner Thread sicher sein, dass andere Threads seine Tokenisierungsbemühungen nicht vermasseln, während er trotzdem in der Lage ist, den Status über mehrere Aufrufe an zu halten strtok - Das macht grundsätzlich strtok_r (die Thread-sichere Version) redundant.

Diese beiden Beispiele ermöglichen es, dass die lokale Thread-Variable existiert innerhalb die Funktion, die es verwendet. In vorgefädeltem Code wäre es einfach eine statische Speicherdauervariable innerhalb der Funktion. Bei Threads wurde das geändert, um die lokale Speicherdauer zu verketten.

Noch ein anderes Beispiel wäre etwas wie errno. Sie möchten keine separaten Threads ändern errno nachdem einer Ihrer Anrufe fehlschlägt, aber bevor Sie die Variable überprüfen können und Sie dennoch nur eine Kopie pro Thread wünschen.

Diese Seite hat eine angemessene Beschreibung der verschiedenen Speicherdauerspezifizierer.


82
2017-08-16 09:13



Wenn Sie eine Variable deklarieren thread_local dann hat jeder Thread seine eigene Kopie. Wenn Sie auf den Namen verweisen, wird die mit dem aktuellen Thread verknüpfte 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 wird "2349", "3249", "4239", "4329", "2439" oder "3429" ausgeben, aber niemals etwas anderes. Jeder Thread hat seine eigene Kopie von i, die zugewiesen, inkrementiert und dann gedruckt wird. Der Thread läuft main hat auch eine eigene Kopie, die am Anfang zugewiesen und dann unverändert belassen wird. Diese Kopien sind völlig unabhängig und jede hat eine andere Adresse.

Es ist nur das Name Das ist in dieser Hinsicht besonders - wenn Sie die Adresse eines thread_local Variable dann haben Sie nur einen normalen Zeiger auf ein normales Objekt, das Sie zwischen Threads frei übergeben 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;
}

Seit der Adresse von i wird an die Thread-Funktion übergeben, dann die Kopie von i Zugehörig zum Haupt-Thread kann zugewiesen werden, obwohl es ist thread_local. Dieses Programm wird also "42" ausgeben. Wenn Sie dies tun, müssen Sie darauf achten *p wird nicht aufgerufen, nachdem der Thread, zu dem es gehört, beendet wurde, andernfalls erhalten Sie einen ungeeigneten Zeiger und ein undefiniertes Verhalten, genau wie in jedem anderen Fall, in dem das angegebene Objekt zerstört wird.

thread_local Variablen werden "vor der ersten Verwendung" initialisiert. Wenn sie also nie von einem bestimmten Thread berührt werden, werden sie nicht notwendigerweise immer initialisiert. Auf diese Weise können Compiler vermeiden, dass alle Konstrukte erstellt werden thread_local Variable im Programm für einen Thread, der vollständig in sich abgeschlossen 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;
}

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. Keine Threadaufrufe fso thread_local Objekt wird nie verwendet. Es ist daher nicht spezifiziert, ob der Compiler 0, 1 oder 2 Instanzen von my_class, und die Ausgabe kann "", "hellohellogoodbyegoodbye" oder "hellogoodbye" sein.


80
2017-08-16 10:41



Thread-lokaler Speicher ist in jedem Aspekt wie statischer (= globaler) Speicher, nur dass jeder Thread eine separate Kopie des Objekts hat. Die Lebensdauer des Objekts beginnt entweder beim Threadstart (für globale Variablen) oder bei der ersten Initialisierung (für blocklokale Statik) und endet, wenn der Thread endet (d. H. Wenn join() wird genannt).

Folglich nur Variablen, die auch deklariert werden könnten static kann als deklariert werden thread_local, d. h. globale Variablen (genauer: Variablen "im Namensraumbereich"), statische Klassenmitglieder und block-statische Variablen (in diesem Fall static ist impliziert).

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-Verwendungsstatistiken drucken, z. mit einer Implementierung wie folgt:

struct Counter
{
     unsigned int c = 0;
     void increment() { ++c; }
     ~Counter()
     {
         std::cout << "Thread #" << std::this_thread::id() << " was called "
                   << c << " times" << std::endl;
     }
};

15
2017-08-16 09:23