151 votes

Que signifie le thread_local en C++11 ?

Je suis confus avec la description de thread_local Si je comprends bien, chaque thread a une copie unique des variables locales dans une fonction. Les variables globales/statiques sont accessibles à tous les threads (accès éventuellement synchronisé à l'aide de verrous). Et les thread_local Les variables 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 ?

176voto

paxdiablo Points 341644

La durée de stockage locale au fil de l'eau est un terme utilisé pour désigner des données dont la durée de stockage semble globale ou statique (du point de vue des fonctions qui les utilisent) mais qui, en réalité, ne font l'objet que d'une seule copie par fil de l'eau.

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

Une chose qui est locale à un fil est créée à la création du fil et éliminée lorsque le fil s'arrête.

Voici quelques exemples.

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 signifie que chaque thread obtient sa propre séquence de nombres aléatoires, indépendamment des autres threads.

Si votre graine était une variable locale au sein de la fonction aléatoire, elle serait initialisée à chaque fois que vous l'appelez, vous donnant ainsi le même nombre à chaque fois. Si c'était une variable globale, les threads interféreraient avec les séquences des autres.

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

Ces deux exemples permettent l'existence de la variable locale "thread". sur la fonction qui l'utilise. Dans le code pré-filmé, il s'agirait simplement d'une variable de durée de stockage statique dans la fonction. Pour les threads, elle est modifiée pour devenir la durée de stockage locale du thread.

Un autre exemple serait quelque chose comme errno . Vous ne voulez pas que des fils séparés modifient errno après l'échec d'un de vos appels mais avant que vous ne puissiez vérifier la variable, et pourtant vous ne voulez qu'une copie par thread.

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

5 votes

L'utilisation de threads locaux ne résout pas les problèmes de strtok . strtok est cassé même dans un environnement à un seul fil.

14 votes

Désolé, laissez-moi reformuler ça. Il n'introduit pas de nouveau problèmes avec strtok :-)

11 votes

En fait, le r signifie "ré-entrant", ce qui n'a rien à voir avec la sécurité des fils. Il est vrai que l'on peut faire fonctionner certaines choses de manière sûre avec un stockage local, mais on ne peut pas les rendre ré-entrantes.

156voto

Anthony Williams Points 28904

Lorsque vous déclarez une variable thread_local alors chaque thread a sa propre copie. Lorsque vous y faites référence par son nom, c'est la copie associée au fil d'exécution actuel qui 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 produira "2349", "3249", "4239", "4329", "2439" ou "3429", mais jamais autre chose. Chaque thread a sa propre copie de i qui est affecté, incrémenté et ensuite imprimé. Le fil d'exécution main a aussi sa propre copie, qui est assignée au début et laissée inchangée. Ces copies sont totalement indépendantes, et chacune a une adresse différente.

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

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

Puisque l'adresse de i est transmis à la fonction thread, alors la copie de i appartenant au thread principal peut être assignée même si elle est thread_local . Ce programme produira donc "42". Si vous faites cela, vous devez alors faire attention à ce que *p n'est pas accédé après que le thread auquel il appartient soit sorti, sinon vous obtenez un pointeur suspendu et un comportement indéfini comme dans tout autre cas où l'objet pointé est détruit.

thread_local 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 initialisées. Ceci permet aux compilateurs d'éviter de construire chaque variable thread_local dans le programme pour un fil qui est entièrement autonome et ne touche aucun d'entre eux, 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 deux threads n'appelle f donc le thread_local n'est jamais utilisé. Il n'est donc pas précisé si le compilateur construira 0, 1 ou 2 instances de l'objet my_class et la sortie peut être "", "hellologoodbyegoodbye" ou "hellogoodbye".

2 votes

Je pense qu'il est important de noter que la copie thread-local de la variable est une copie nouvellement initialisée de la variable. C'est-à-dire que si vous ajoutez un g() au début de threadFunc alors la sortie sera 0304029 ou toute autre permutation des paires 02 , 03 et 04 . C'est-à-dire que, même si 9 est affecté à i avant que les threads ne soient créés, les threads obtiennent une copie fraîchement construite du fichier ii=0 . Si i est affecté à thread_local int i = random_integer() puis chaque thread reçoit un nouveau nombre entier aléatoire.

0 votes

Pas exactement une permutation de 02 , 03 , 04 il peut y avoir d'autres séquences comme 020043

0 votes

Une information intéressante que je viens de trouver : GCC supporte l'utilisation de l'adresse d'une variable thread_local comme argument de template, mais d'autres compilateurs ne le font pas (à ce jour ; j'ai essayé clang, vstudio). Je ne suis pas sûr de ce que la norme a à dire à ce sujet, ou si c'est un domaine non spécifié.

25voto

Kerrek SB Points 194696

Le stockage local des threads est en tous points semblable au stockage statique (= global), à ceci près que chaque thread dispose d'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 les statiques locales au bloc), et se termine lorsque le thread se termine (c'est-à-dire lorsque join() est appelé).

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

Par exemple, supposons que vous ayez un pool de threads et que vous souhaitiez savoir si votre charge de travail est bien é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 permettrait d'imprimer 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;
     }
};

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X