O que significa thread_local em C ++ 11?

131

Estou confuso com a descrição de thread_localem C ++ 11. Pelo que entendi, cada thread possui uma cópia exclusiva de variáveis ​​locais em uma função. As variáveis ​​globais / estáticas podem ser acessadas por todos os threads (possivelmente acesso sincronizado usando bloqueios). E as thread_localvariáveis ​​são visíveis para todos os segmentos, mas só podem ser modificadas pelo segmento para o qual foram definidas? Está correto?

polapts
fonte

Respostas:

150

Duração do armazenamento local do encadeamento é um termo usado para se referir a dados que são aparentemente globais ou estáticos (do ponto de vista das funções que o utilizam), mas, na realidade, existe uma cópia por encadeamento.

Ele adiciona ao automático atual (existe durante um bloco / função), estático (existe durante o programa) e dinâmico (existe no monte entre alocação e desalocação).

Algo local do encadeamento é criado na criação do encadeamento e descartado quando o encadeamento é interrompido.

Alguns exemplos a seguir.

Pense em um gerador de números aleatórios onde a semente deve ser mantida por thread. Usar uma semente local de encadeamento significa que cada encadeamento obtém sua própria sequência de números aleatórios, independente de outros encadeamentos.

Se sua semente fosse uma variável local dentro da função aleatória, ela seria inicializada toda vez que você a chamasse, fornecendo o mesmo número a cada vez. Se fosse global, os threads interfeririam nas seqüências um do outro.

Outro exemplo é algo como strtokonde o estado de tokenização é armazenado em uma base específica do encadeamento. Dessa forma, um único encadeamento pode ter certeza de que outros encadeamentos não estragarão seus esforços de tokenização, enquanto ainda é capaz de manter o estado de várias chamadas para strtok- isso basicamente torna strtok_rredundante (a versão segura do encadeamento).

Ambos os exemplos permitem que a variável local do encadeamento exista na função que a utiliza. No código pré-encadeado, seria simplesmente uma variável estática de duração de armazenamento dentro da função. Para encadeamentos, isso é modificado para encadear a duração do armazenamento local.

Ainda outro exemplo seria algo parecido errno. Você não deseja que threads separados sejam modificados errnodepois que uma das suas chamadas falhar, mas antes de poder verificar a variável e, no entanto, deseja apenas uma cópia por thread.

Este site tem uma descrição razoável dos diferentes especificadores de duração de armazenamento.

paxdiablo
fonte
4
Usar thread local não resolve os problemas com strtok. strtokestá quebrado mesmo em um único ambiente de encadeamento.
22612 James Bond
11
Desculpe, deixe-me reformular isso. Ele não apresenta novos problemas com o strtok :-) #
1655
7
Na verdade, rsignifica "reentrante", que não tem nada a ver com a segurança do thread. É verdade que você pode fazer algumas coisas funcionarem com segurança com o armazenamento local de threads, mas não pode fazê-las reentrarem.
Kerrek SB
5
Em um ambiente de thread único, as funções precisam ser reentradas apenas se fizerem parte de um ciclo no gráfico de chamadas. Uma função folha (que não chama outras funções), por definição, não faz parte de um ciclo, e não há uma boa razão para strtokchamar outras funções.
precisa saber é o seguinte
3
isso iria atrapalhar: while (something) { char *next = strtok(whatever); someFunction(next); // someFunction calls strtok }
japreiss
135

Quando você declara uma variável thread_local, cada thread tem sua própria cópia. Quando você se refere a ele pelo nome, a cópia associada ao encadeamento atual é usada. por exemplo

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

Esse código produzirá "2349", "3249", "4239", "4329", "2439" ou "3429", mas nunca mais nada. Cada encadeamento possui sua própria cópia i, atribuída, incrementada e impressa. O encadeamento em execução maintambém possui sua própria cópia, a qual é atribuída no início e depois deixada inalterada. Essas cópias são totalmente independentes e cada uma possui um endereço diferente.

É apenas o nome que é especial a esse respeito - se você pegar o endereço de uma thread_localvariável, basta ter um ponteiro normal para um objeto normal, que você pode passar livremente entre os threads. por exemplo

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

Como o endereço de ié passado para a função de encadeamento, a cópia de ipertencer ao encadeamento principal pode ser atribuída, mesmo que seja thread_local. Este programa produzirá, portanto, "42". Se você fizer isso, precisará tomar cuidado para que *pnão seja acessado após a saída do thread ao qual pertence, caso contrário, você receberá um ponteiro oscilante e um comportamento indefinido, como em qualquer outro caso em que o objeto apontado seja destruído.

thread_localAs variáveis ​​são inicializadas "antes do primeiro uso"; portanto, se nunca forem tocadas por um determinado encadeamento, nunca serão necessariamente inicializadas. Isso permite que os compiladores evitem construir todas as thread_localvariáveis ​​do programa para um thread que seja totalmente independente e não toque em nenhuma delas. por exemplo

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

Neste programa, existem 2 threads: o thread principal e o thread criado manualmente. Nenhum thread é chamado f, portanto o thread_localobjeto nunca é usado. Portanto, não é especificado se o compilador construirá 0, 1 ou 2 instâncias my_classe a saída pode ser "", "hellohellogoodbyegoodbye" ou "hellogoodbye".

Anthony Williams
fonte
1
Eu acho que é importante notar que a cópia local da thread da variável é uma cópia da variável recém-inicializada. Isto é, se você adicionar uma g()chamada para o início threadFunc, em seguida, a saída será 0304029ou alguma outra permutação dos pares 02, 03e 04. Ou seja, mesmo que 9 seja atribuído iantes da criação dos threads, os threads obtêm uma cópia recém-construída de ionde i=0. Se ifor atribuído com thread_local int i = random_integer(), cada thread obterá um novo número inteiro aleatório.
Mark H
Não é exatamente uma permutação de 02, 03, 04, pode haver outras seqüências como020043
Hongxu Chen
Boato interessante que acabei de encontrar: O GCC suporta o uso do endereço de uma variável thread_local como argumento de modelo, mas outros compiladores não (no momento em que escrevemos; tentei clang, vstudio). Não sei ao certo o que o padrão tem a dizer sobre isso ou se essa é uma área não especificada.
jwd 11/07
23

O armazenamento local do encadeamento é, em todos os aspectos, como estático (= global), apenas que cada encadeamento tenha uma cópia separada do objeto. O tempo de vida do objeto começa no início do encadeamento (para variáveis ​​globais) ou na primeira inicialização (para estática local do bloco) e termina quando o encadeamento termina (ou seja, quando join()é chamado).

Consequentemente, apenas variáveis ​​que também podem ser declaradas staticpodem ser declaradas como thread_local, isto é, variáveis ​​globais (mais precisamente: variáveis ​​"no escopo do espaço de nome"), membros de classe estática e variáveis ​​estáticas de bloco (nesse caso, staticimplícitas).

Como exemplo, suponha que você tenha um conjunto de encadeamentos e queira saber o quão bem sua carga de trabalho estava sendo equilibrada:

thread_local Counter c;

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

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

Isso imprimiria estatísticas de uso de threads, por exemplo, com uma implementação como esta:

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
fonte