Pregunta ¿Qué significa thread_local en C ++ 11?


Estoy confundido con la descripción de thread_local en C ++ 11. Según entiendo, cada hilo tiene una copia única de variables locales en una función. Se puede acceder a las variables globales / estáticas por todos los hilos (posiblemente acceso sincronizado usando bloqueos). ¿Y las variables thread_local son visibles para todos los hilos, pero solo pueden ser modificadas por el hilo para el que están definidas? ¿Es correcto?


75
2017-08-16 09:05


origen


Respuestas:


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

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

Algo que es thread-local se crea en la creación de hilo y se elimina cuando se detiene el hilo.

Algunos ejemplos siguen.

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

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

Otro ejemplo es algo así como strtok donde el estado de tokenización se almacena en una base específica de subprocesos. De esta forma, un solo hilo puede estar seguro de que otros hilos no arruinarán sus esfuerzos de tokenización, al mismo tiempo que podrán mantener el estado en múltiples llamadas a strtok - Esto básicamente representa strtok_r (la versión de seguridad de subprocesos) redundante.

Ambos ejemplos permiten que exista la variable local de subprocesos dentro la función que lo usa. En el código prehebra, simplemente sería una variable de duración de almacenamiento estática dentro de la función. Para los hilos, eso se modifica para enhebrar la duración de almacenamiento local.

Otro ejemplo sería algo así como errno. No quieres que se modifiquen los hilos por separado errno después de que una de sus llamadas falla, pero antes de que pueda verificar la variable, y sin embargo, solo quiere una copia por hilo.

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


82
2017-08-16 09:13



Cuando declaras una variable thread_local entonces cada hilo tiene su propia copia. Cuando lo menciona 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 más. Cada hilo tiene su propia copia de i, que se asigna a, se incrementa y luego se imprime. El hilo ejecutándose main tambié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 eso es especial a ese respecto --- si toma la dirección de un thread_local variable, entonces solo tiene un puntero normal a un objeto normal, que puede pasar libremente entre los 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 i se pasa a la función hilo, luego la copia de i perteneciente al hilo principal se puede asignar a aunque es thread_local. Este programa dará como resultado "42". Si haces esto, entonces debes cuidar que *p no se accede después de que el hilo al que pertenece ha salido; de lo contrario, se obtiene un puntero colgante y un comportamiento indefinido como cualquier otro caso en el que se destruye el objeto apuntado.

thread_local las variables se inicializan "antes del primer uso", de modo que si nunca son tocadas por un hilo dado, no necesariamente se inicializan. Esto es para permitir que los compiladores eviten construir cada thread_local variable en el programa para un hilo que es completamente autónomo y no toca ninguno de ellos. p.ej.

struct my_class{
    my_class(){
        std::cout<<"hello";
    }
    ~my_class(){
        std::cout<<"goodbye";
    }
};

void f(){
    thread_local my_class;
}

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. Ni llamadas de subprocesos f, entonces el thread_local el objeto nunca se usa. Por lo tanto, no se especifica si el compilador construirá 0, 1 o 2 instancias de my_classy el resultado puede ser "", "hellohellogoodbyegoodbye" o "hellogoodbye".


80
2017-08-16 10:41



El almacenamiento Thread-local está en cada aspecto como almacenamiento estático (= global), solo que cada hilo tiene una copia separada del objeto. La vida útil del objeto comienza en el inicio del hilo (para variables globales) o en la inicialización inicial (estática del bloque local) y finaliza cuando el hilo finaliza (es decir, cuando join() se llama).

En consecuencia, solo las variables que también podrían ser declaradas static puede ser declarado como thread_local, es decir, variables globales (más precisamente: variables "en el ámbito del espacio de nombres"), miembros de clases estáticas y variables estáticas de bloques (en cuyo caso static está implícito).

Como ejemplo, supongamos que tiene un grupo de subprocesos y desea saber qué tan bien se estaba 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, p. 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;
     }
};

15
2017-08-16 09:23