Que signifie le thread_local en C ++ 11?

131

Je suis confus avec la description de thread_localC ++ 11. Ma compréhension est que chaque thread a une copie unique des variables locales dans une fonction. Les variables globales / statiques sont accessibles par tous les threads (éventuellement accès synchronisé à l'aide de verrous). Et les thread_localvariables sont visibles par tous les threads mais ne peuvent être modifiées que par le thread pour lequel elles sont définies? Est-ce correct?

polapts
la source

Réponses:

151

La durée de stockage locale du thread est un terme utilisé pour désigner des données qui sont apparemment une durée de stockage globale ou statique (du point de vue des fonctions qui l'utilisent) mais en réalité, il y a une copie par thread.

Il s'ajoute au courant automatique (existe pendant un bloc / fonction), statique (existe pour la durée du programme) et dynamique (existe sur le tas entre l'allocation et la désallocation).

Quelque chose qui est local au thread est mis en place lors de la création du thread et éliminé lorsque le thread s'arrête.

Quelques exemples suivent.

Pensez à un générateur de nombres aléatoires où la graine doit être maintenue sur une base par thread. L'utilisation d'une graine locale au thread signifie que chaque thread obtient sa propre séquence de nombres aléatoires, indépendante des autres threads.

Si votre valeur de départ était une variable locale dans la fonction aléatoire, elle serait initialisée à chaque fois que vous l'appeliez, vous donnant le même nombre à chaque fois. S'il s'agissait d'un global, les threads interféreraient les uns avec les autres.

Un autre exemple est quelque chose comme strtokoù l'état de tokenisation est stocké sur une base spécifique au thread. De cette façon, un seul thread peut être sûr que d'autres threads ne gâcheront pas ses efforts de tokenisation, tout en étant capable de maintenir l'état sur plusieurs appels à strtok- cela rend fondamentalement strtok_r(la version thread-safe) redondante.

Ces deux exemples permettent à la variable locale de thread d'exister dans la fonction qui l'utilise. Dans le code pré-threadé, il s'agirait simplement d'une variable de durée de stockage statique dans la fonction. Pour les threads, cela est modifié pour tenir compte de la durée de stockage local des threads.

Encore un autre exemple serait quelque chose comme errno. Vous ne voulez pas que des threads séparés soient modifiés errnoaprès l'échec de l'un de vos appels, mais avant de pouvoir vérifier la variable, et pourtant vous ne voulez qu'une copie par thread.

Ce site a une description raisonnable des différents spécificateurs de durée de stockage.

paxdiablo
la source
4
L'utilisation de thread local ne résout pas les problèmes avec strtok. strtokest cassé même dans un environnement à thread unique.
James Kanze
11
Désolé, permettez-moi de reformuler cela. Cela n'introduit aucun nouveau problème avec strtok :-)
paxdiablo
7
En fait, cela rsignifie «rentrant», ce qui n'a rien à voir avec la sécurité des fils. Il est vrai que vous pouvez faire fonctionner certaines choses en toute sécurité avec le stockage local des threads, mais vous ne pouvez pas les faire rentrer.
Kerrek SB
5
Dans un environnement monothread, les fonctions doivent être réentrantes uniquement si elles font partie d'un cycle dans le graphe d'appel. Une fonction feuille (qui n'appelle pas d'autres fonctions) ne fait par définition pas partie d'un cycle, et il n'y a aucune bonne raison d' strtokappeler d'autres fonctions.
MSalters
3
cela while (something) { char *next = strtok(whatever); someFunction(next); // someFunction calls strtok }
gâcherait
135

Lorsque vous déclarez une variable, thread_localchaque thread a sa propre copie. Lorsque vous y faites référence par son nom, la copie associée au thread actuel est utilisée. par exemple

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

Ce code affichera "2349", "3249", "4239", "4329", "2439" ou "3429", mais jamais rien d'autre. Chaque fil a sa propre copie de i, qui est assignée, incrémentée puis imprimée. Le fil en coursmain également sa propre copie, qui est affectée au début et laissée inchangée. Ces copies sont entièrement indépendantes et ont chacune une adresse différente.

C'est seulement le nom qui est spécial à cet égard --- si vous prenez l'adresse d'une thread_localvariable, vous avez juste un pointeur normal vers un objet normal, que vous pouvez passer librement entre les threads. par exemple

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

Depuis l'adresse de i est passée à la fonction de thread, alors la copie de l' iappartenance au thread principal peut être affectée même si elle l'est thread_local. Ce programme affichera donc "42". Si vous faites cela, vous devez faire attention à *pne pas y accéder après la sortie du thread auquel il appartient, sinon vous obtenez un pointeur suspendu et un comportement indéfini comme dans tout autre cas où l'objet pointé est détruit.

thread_localles variables sont initialisées "avant la première utilisation", donc si elles ne sont jamais touchées par un thread donné, elles ne sont pas nécessairement jamais initialisées. Cela permet aux compilateurs d'éviter de construire toutes les thread_localvariables du programme pour un thread entièrement autonome et qui ne touche à aucune d'elles. par exemple

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

Dans ce programme, il y a 2 threads: le thread principal et le thread créé manuellement. Aucun des threads n'appelle f, donc l' thread_localobjet n'est jamais utilisé. Il n'est donc pas spécifié si le compilateur construira 0, 1 ou 2 instances de my_class, et la sortie peut être "", "hellohellogoodbyegoodbye" ou "hellogoodbye".

Anthony Williams
la source
1
Je pense qu'il est important de noter que la copie locale du thread de la variable est une copie nouvellement initialisée de la variable. Autrement dit, si vous ajoutez un g()appel au début threadFunc, la sortie sera 0304029ou une autre permutation des paires 02, 03et 04. Autrement dit, même si 9 est affecté à iavant la création des threads, les threads obtiennent une copie fraîchement construite de iwhere i=0. Si iest assigné avec thread_local int i = random_integer(), alors chaque thread obtient un nouvel entier aléatoire.
Mark H du
Pas exactement une permutation 02, 03, 04, il peut y avoir d' autres séquences comme020043
Hongxu Chen
Tidbit intéressant que je viens de trouver: GCC prend en charge l'utilisation de l'adresse d'une variable thread_local comme argument de modèle, mais d'autres compilateurs ne le font pas (au moment d'écrire ces lignes; essayé clang, vstudio). Je ne sais pas ce que la norme a à dire à ce sujet, ou s'il s'agit d'un domaine non spécifié.
jwd
23

Le stockage local de thread est dans tous les aspects comme le stockage statique (= global), seulement que chaque thread a une copie distincte de l'objet. La durée de vie de l'objet commence soit au début du thread (pour les variables globales) soit à la première initialisation (pour la statique locale au bloc), et se termine lorsque le thread se termine (c'est-à-dire quandjoin() est appelé).

Par conséquent, seules les variables qui pourraient également être déclarées staticpeuvent être déclarées comme thread_local, c'est-à-dire des variables globales (plus précisément: des variables "à la portée de l'espace de noms"), des membres de classe statiques et des variables statiques de bloc (auquel casstatic est implicite).

À titre d'exemple, supposons que vous ayez un pool de threads et que vous souhaitiez savoir dans quelle mesure votre charge de travail était équilibrée:

thread_local Counter c;

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

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

Cela afficherait les statistiques d'utilisation des threads, par exemple avec une implémentation comme celle-ci:

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
la source