¿Qué significa thread_local en C ++ 11?

131

Estoy confundido con la descripción de thread_localen C ++ 11. Entiendo que cada hilo tiene una copia única de variables locales en una función. Todos los subprocesos pueden acceder a las variables globales / estáticas (posiblemente acceso sincronizado mediante bloqueos). ¿Y las thread_localvariables son visibles para todos los hilos pero solo pueden modificarse por el hilo para el que están definidas? ¿Es correcto?

polapts
fuente

Respuestas:

151

La duración del almacenamiento local de subprocesos es un término utilizado para referirse a datos que aparentemente tienen una duración de almacenamiento global o estática (desde el punto de vista de las funciones que lo usan), pero en realidad, hay una copia por subproceso.

Se agrega a la automática actual (existe durante un bloque / función), estática (existe durante la duración del programa) y dinámica (existe en el montón entre asignación y desasignación).

Algo que es local de subprocesos se crea cuando se crea un subproceso y se elimina cuando el subproceso se detiene.

Algunos ejemplos siguen.

Piense en un generador de números aleatorios donde la semilla debe mantenerse por subproceso. Usar una semilla local de hilo significa que cada hilo obtiene su propia secuencia de números aleatorios, independiente de otros hilos.

Si su semilla era una variable local dentro de la función aleatoria, se inicializaría cada vez que la llamara, dándole el mismo número cada vez. Si fuera global, los hilos interferirían entre sí con las secuencias.

Otro ejemplo es algo así como strtokel estado de tokenización se almacena en una base específica de subproceso. De esa manera, un solo subproceso puede estar seguro de que otros subprocesos no arruinarán sus esfuerzos de tokenización, al tiempo que puede mantener el estado en múltiples llamadas a strtok, esto básicamente hace que strtok_rla versión segura para subprocesos sea redundante.

Ambos ejemplos permiten que la variable local de hilo exista dentro de la función que la usa. En el código previamente enhebrado, sería simplemente una variable de duración de almacenamiento estático dentro de la función. Para subprocesos, eso se modifica para la duración del almacenamiento local de subprocesos.

Sin embargo, otro ejemplo sería algo así errno. No desea que los hilos separados se modifiquen errnodespués de que una de sus llamadas falle, pero antes de que pueda verificar la variable, y sin embargo, solo desea una copia por hilo.

Este sitio tiene una descripción razonable de los diferentes especificadores de duración de almacenamiento.

paxdiablo
fuente
44
Usar thread local no resuelve los problemas con strtok. strtokse rompe incluso en un entorno de un solo subproceso.
James Kanze
11
Lo siento, déjame reformular eso. No presenta ningún problema nuevo con strtok :-)
paxdiablo
77
En realidad, rsignifica "reentrante", que no tiene nada que ver con la seguridad del hilo. Es cierto que puede hacer que algunas cosas funcionen de forma segura con subprocesos con almacenamiento local de subprocesos, pero no puede hacer que vuelvan a entrar.
Kerrek SB
55
En un entorno de subproceso único, las funciones deben ser reentrantes solo si forman parte de un ciclo en el gráfico de llamadas. Una función de hoja (una que no llama a otras funciones) no es, por definición, parte de un ciclo, y no hay una buena razón por la que strtokdeba llamar a otras funciones.
MSalters
3
esto lo estropearía: while (something) { char *next = strtok(whatever); someFunction(next); // someFunction calls strtok }
japreiss
135

Cuando declaras una variable thread_local, cada hilo tiene su propia copia. Cuando se refiere a él por su nombre, se utiliza la copia asociada con el hilo actual. p.ej

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

Este código generará "2349", "3249", "4239", "4329", "2439" o "3429", pero nunca otra cosa. Cada hilo tiene su propia copia de i, que se asigna, se incrementa y luego se imprime. El hilo en ejecución maintambién tiene su propia copia, que se asigna al principio y luego se deja sin cambios. Estas copias son completamente independientes y cada una tiene una dirección diferente.

Es solo el nombre lo que es especial a ese respecto: si toma la dirección de una thread_localvariable, simplemente tiene un puntero normal a un objeto normal, que puede pasar libremente entre hilos. p.ej

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

Dado que la dirección de ise pasa a la función de subproceso, la copia de ipertenencia al subproceso principal se puede asignar aunque sea thread_local. Este programa generará así "42". Si hace esto, debe tener cuidado de que *pno se acceda después de que el hilo al que pertenece haya salido, de lo contrario, obtendrá un puntero colgante y un comportamiento indefinido como cualquier otro caso en el que se destruye el objeto señalado.

thread_locallas variables se inicializan "antes del primer uso", por lo que si nunca son tocadas por un hilo dado, entonces no necesariamente se inicializan. Esto es para permitir que los compiladores eviten construir cada thread_localvariable en el programa para un hilo que sea completamente autónomo y no toque ninguna de ellas. p.ej

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

En este programa hay 2 hilos: el hilo principal y el hilo creado manualmente. Ninguno de los hilos llama f, por lo que el thread_localobjeto nunca se usa. Por lo tanto, no se especifica si el compilador construirá 0, 1 o 2 instancias de my_class, y la salida puede ser "", "hellohellogoodbyegoodbye" o "hellogoodbye".

Anthony Williams
fuente
1
Creo que es importante tener en cuenta que la copia local de hilo de la variable es una copia de variable recién inicializada. Es decir, si se agrega una g()llamada al comienzo de threadFunc, a continuación, la salida será 0304029o alguna otra permutación de los pares 02, 03y 04. Es decir, aunque se asigna 9 iantes de que se creen los hilos, los hilos obtienen una copia recién construida de idónde i=0. Si ise le asigna thread_local int i = random_integer(), entonces cada hilo obtiene un nuevo entero aleatorio.
Mark H
No es exactamente una permutación de 02, 03, 04, pueden existir otras secuencias como020043
Hongxu Chen
Dato interesante que acabo de encontrar: GCC admite el uso de la dirección de una variable thread_local como argumento de plantilla, pero otros compiladores no lo hacen (a partir de este escrito; intentado clang, vstudio). No estoy seguro de qué dice el estándar sobre eso, o si esta es un área no especificada.
jwd
23

El almacenamiento local de subprocesos está en todos los aspectos como el almacenamiento estático (= global), solo que cada subproceso tiene una copia separada del objeto. El tiempo de vida del objeto comienza en el inicio del subproceso (para las variables globales) o en la primera inicialización (para las estadísticas locales de bloque), y finaliza cuando finaliza el subproceso (es decir, cuando join()se llama).

En consecuencia, solo las variables que también podrían declararse staticpueden declararse como thread_local, es decir, variables globales (más precisamente: variables "en el ámbito del espacio de nombres"), miembros de clase estáticos y variables estáticas en bloque (en cuyo caso staticestá implícito).

Como ejemplo, suponga que tiene un grupo de subprocesos y desea saber qué tan bien se está equilibrando su carga de trabajo:

thread_local Counter c;

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

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

Esto imprimiría estadísticas de uso de hilos, por ejemplo, con una implementación 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
fuente