Cosa significa thread_local in C ++ 11?


131

Sono confuso con la descrizione di thread_localin C ++ 11. La mia comprensione è che ogni thread ha una copia univoca delle variabili locali in una funzione. Le variabili globali / statiche sono accessibili da tutti i thread (possibilmente accesso sincronizzato tramite blocchi). E le thread_localvariabili sono visibili a tutti i thread ma possono essere modificate solo dal thread per cui sono definite? È corretto?

Risposte:


151

La durata della memoria locale del thread è un termine usato per riferirsi a dati che sembrano una durata della memoria apparentemente globale o statica (dal punto di vista delle funzioni che lo utilizzano), ma in realtà esiste una copia per thread.

Aggiunge l'attuale automatico (esiste durante un blocco / funzione), statico (esiste per la durata del programma) e dinamico (esiste sull'heap tra allocazione e deallocazione).

Qualcosa che è locale del thread viene creato alla creazione del thread e eliminato quando il thread si interrompe.

Seguono alcuni esempi.

Pensa a un generatore di numeri casuali in cui il seme deve essere mantenuto su una base per thread. L'uso di un seed di thread locale significa che ogni thread ottiene la propria sequenza numerica casuale, indipendente da altri thread.

Se il tuo seme fosse una variabile locale all'interno della funzione casuale, verrebbe inizializzato ogni volta che lo chiamavi, dandoti lo stesso numero ogni volta. Se fosse globale, i thread interferirebbero con le sequenze reciproche.

Un altro esempio è quello in strtokcui lo stato di tokenizzazione è memorizzato su una base specifica del thread. In questo modo, un singolo thread può essere sicuro che altri thread non rovineranno i suoi sforzi di tokenizzazione, pur essendo in grado di mantenere lo stato su più chiamate a strtok- questo rende sostanzialmente strtok_r(la versione thread-safe) ridondante.

Entrambi questi esempi consentono l'esistenza all'interno della variabile locale del thread della funzione che la utilizza. Nel codice pre-thread, sarebbe semplicemente una variabile di durata della memoria statica all'interno della funzione. Per i thread, questo è stato modificato per durare la durata della memoria locale.

Ancora un altro esempio sarebbe qualcosa di simile errno. Non vuoi modificare i thread separati errnodopo il fallimento di una delle tue chiamate, ma prima di poter controllare la variabile, eppure vuoi solo una copia per thread.

Questo sito contiene una descrizione ragionevole dei diversi identificatori della durata della memorizzazione.


4
L'uso di thread local non risolve i problemi con strtok. strtokviene interrotto anche in un singolo ambiente thread.
James Kanze,

11
Mi dispiace, lascia che lo riformuli. Non introduce nuovi problemi con strtok :-)
paxdiablo

7
In realtà, rsta per "rientro", che non ha nulla a che fare con la sicurezza del thread. È vero che è possibile far funzionare alcune cose in modo thread-safe con l'archiviazione locale dei thread, ma non è possibile ripristinarle.
Kerrek SB,

5
In un ambiente a thread singolo, le funzioni devono essere rientrate solo se fanno parte di un ciclo nel grafico della chiamata. Una funzione foglia (una che non chiama altre funzioni) non è per definizione parte di un ciclo e non vi è alcun motivo valido per strtokchiamare altre funzioni.
Saluti

3
questo while (something) { char *next = strtok(whatever); someFunction(next); // someFunction calls strtok }
rovinerebbe

135

Quando dichiari una variabile, thread_localogni thread ha la sua copia. Quando ti riferisci ad esso per nome, viene utilizzata la copia associata al thread corrente. per esempio

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

Questo codice genererà "2349", "3249", "4239", "4329", "2439" o "3429", ma mai nient'altro. Ogni thread ha una propria copia di i, che viene assegnata, incrementata e quindi stampata. Il thread è in esecuzionemain ha anche una sua copia, che viene assegnata all'inizio e quindi lasciata invariata. Queste copie sono completamente indipendenti e ognuna ha un indirizzo diverso.

È solo il nome che è speciale al riguardo --- se prendi l'indirizzo di una thread_localvariabile allora hai solo un normale puntatore a un oggetto normale, che puoi passare liberamente tra i thread. per esempio

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

Poiché l'indirizzo di iviene passato alla funzione thread, è ipossibile assegnare la copia di appartenenza al thread principale anche se lo è thread_local. Questo programma genererà quindi "42". Se lo fai, devi fare attenzione a *pnon accedere dopo che il thread a cui appartiene è uscito, altrimenti otterrai un puntatore penzolante e un comportamento indefinito proprio come qualsiasi altro caso in cui l'oggetto puntato viene distrutto.

thread_localle variabili vengono inizializzate "prima del primo utilizzo", quindi se non vengono mai toccate da un determinato thread, non vengono necessariamente inizializzate. Questo per consentire ai compilatori di evitare di costruire ogni thread_localvariabile nel programma per un thread completamente autonomo e che non tocchi nessuno di essi. per esempio

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 questo programma ci sono 2 thread: il thread principale e il thread creato manualmente. Nessuno dei thread chiama f, quindi l' thread_localoggetto non viene mai utilizzato. Non è quindi specificato se il compilatore costruirà 0, 1 o 2 istanze di my_classe l'output potrebbe essere "", "hellohellogoodbyegoodbye" o "hellogoodbye".


1
Penso che sia importante notare che la copia thread-local della variabile è una copia della variabile appena inizializzata. Cioè, se si aggiunge una g()chiamata al inizio threadFunc, allora l'uscita sarà 0304029o qualche altro permutazione delle coppie 02, 03e 04. Cioè, anche se 9 è assegnato a iprima della creazione dei thread, i thread ottengono una copia appena costruita di idove i=0. Se iviene assegnato con thread_local int i = random_integer(), ogni thread ottiene un nuovo numero intero casuale.
Mark H,

Non esattamente una permutazione 02, 03, 04, ci possono essere altre sequenze come020043
Hongxu Chen

Idee interessanti che ho appena trovato: GCC supporta l'utilizzo dell'indirizzo di una variabile thread_local come argomento template, ma altri compilatori non lo fanno (al momento della stesura di questo documento; provato clang, vstudio). Non sono sicuro di ciò che lo standard ha da dire al riguardo, o se questa è un'area non specificata.
jwd

23

L'archiviazione locale di thread è sotto ogni aspetto come l'archiviazione statica (= globale), solo che ogni thread ha una copia separata dell'oggetto. Il tempo di vita dell'oggetto inizia all'inizio del thread (per variabili globali) o alla prima inizializzazione (per statica locale del blocco) e termina quando termina il thread (ovvero quandojoin() viene chiamato).

Di conseguenza, solo le variabili che potrebbero anche essere dichiarate staticpossono essere dichiarate come thread_local, cioè variabili globali (più precisamente: variabili "nell'ambito dello spazio dei nomi"), membri della classe statica e variabili statiche a blocchi (nel qual casostatic è implicito).

Ad esempio, supponiamo che tu abbia un pool di thread e desideri sapere quanto il tuo carico di lavoro è stato bilanciato:

thread_local Counter c;

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

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

Ciò stamperebbe le statistiche sull'utilizzo del thread, ad esempio con un'implementazione come questa:

struct Counter
{
     unsigned int c = 0;
     void increment() { ++c; }
     ~Counter()
     {
         std::cout << "Thread #" << std::this_thread::id() << " was called "
                   << c << " times" << std::endl;
     }
};
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.