web-dev-qa-db-fra.com

Que signifie thread_local dans C ++ 11?

Je suis confus avec la description de thread_local en C++ 11. D'après ce que j'ai compris, chaque fil a une copie unique des variables locales dans une fonction. Les variables globales/statiques sont accessibles à tous les threads (éventuellement, accès synchronisé à l'aide de verrous). Et le 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?

102
polapts

La durée de stockage local des threads est un terme utilisé pour faire référence à des données qui sont apparemment une durée de stockage globale ou statique (du point de vue des fonctions qui les utilisent), mais en réalité, il y a une copie par thread.

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

Quelque chose qui est thread-local est créé 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 par thread. L'utilisation d'une graine locale à un thread signifie que chaque thread obtient sa propre séquence de nombres aléatoires, indépendante des autres threads.

Si votre graine était une variable locale dans la fonction random, elle serait initialisée à chaque fois que vous l'appeliez, vous donnant le même numéro à chaque fois. Si c'était un global, les threads interféreraient avec les séquences de chacun.

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 vont pas gâcher ses efforts de création de jetons, tout en restant en mesure de maintenir l'état sur plusieurs appels à strtok - cela rend fondamentalement strtok_r (version thread-safe) redondant.

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 a été modifié pour traiter la durée de stockage local.

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

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

119
paxdiablo

Lorsque vous déclarez une variable thread_local alors chaque fil a sa propre copie. Lorsque vous vous y référez par nom, la copie associée au fil 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 générera "2349", "3249", "4239", "4329", "2439" ou "3429", mais jamais rien d'autre. Chaque thread a sa propre copie de i, qui est assignée à, incrémentée puis imprimée. Le thread qui exécute main possède également sa propre copie, affectée au début puis laissée inchangée. Ces copies sont entièrement indépendantes et chacune a une adresse différente.

Ce n'est que le nom qui est spécial à cet égard --- si vous prenez l'adresse d'un thread_local variable alors vous avez juste un pointeur normal sur 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;
}

Puisque l'adresse de i est transmise à la fonction thread, la copie de i appartenant au thread principal peut être affectée même s'il s'agit de thread_local. Ce programme affichera donc "42". Si vous faites cela, alors vous devez vous assurer que *p n'est pas accessible après la fin du thread auquel il appartient, sinon vous obtenez un pointeur en suspens et un comportement indéfini, comme dans tout autre cas où l'objet pointé est détruit.

thread_local les variables sont initialisées "avant la première utilisation", donc si elles ne sont jamais touchées par un fil donné, elles ne le sont pas nécessairement. Ceci permet aux compilateurs d'éviter de construire tous les thread_local variable dans le programme pour un fil de discussion entièrement autonome qui ne les touche pas. 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 existe 2 threads: le thread principal et le thread créé manuellement. Ni le thread appelle f, donc le thread_local objet n'est jamais utilisé. Il n’est donc pas précisé si le compilateur construira 0, 1 ou 2 instances de my_class, et la sortie peut être "", "hellohellogoodbyegoodbye" ou "hellogoodbye".

107
Anthony Williams

Le stockage local des threads ressemble dans tous les aspects au stockage statique (= global), mais chaque thread possède une copie distincte de l'objet. La durée de vie de l'objet commence au début du thread (pour les variables globales) ou à la première initialisation (pour la statique bloc-local), et se termine à la fin du thread (c'est-à-dire lorsque join() est appelée).

Par conséquent, seules les variables qui pourraient également être déclarées static peuvent être déclarées 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-blocs (auquel cas static est impliqué).

Par exemple, supposons que vous ayez un pool de threads et que vous souhaitiez savoir à quel point votre charge de travail était 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 permet d’imprimer les statistiques d’utilisation du fil, 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;
     }
};
20
Kerrek SB