¿Qué significa thread_local en C++ 11?
multithreading c++11 (3)
Cuando declaras una variable thread_local
, 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 subproceso que ejecuta 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.
Solo el nombre es especial en ese aspecto: si tomas la dirección de una variable thread_local
, entonces solo tienes un puntero normal a un objeto normal, que puedes 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;
}
Como la dirección de i
se pasa a la función de subproceso, la copia de i
pertenece al subproceso principal puede asignarse aunque sea thread_local
. Este programa dará como resultado "42". Si hace esto, debe tener cuidado de que *p
no 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 donde se destruye el objeto apuntado.
thread_local
variables thread_local
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 variable thread_local
en el programa para un subproceso 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. Ninguno de los hilos llama a f
, por lo que el objeto thread_local
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".
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?
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 termina el hilo (es decir, cuando se llama a join()
).
En consecuencia, solo las variables que también pueden declararse static
pueden declararse 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 está implícito static
).
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 las estadísticas de uso de subprocesos, 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;
}
};
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 único hilo puede estar seguro de que otros hilos no arruinarán sus esfuerzos de tokenización, mientras que aún puede mantener el estado en múltiples llamadas a strtok
, lo que básicamente hace que strtok_r
(la versión segura para subprocesos) sea redundante.
Ambos ejemplos permiten que la variable local de subproceso exista dentro de la función que lo utiliza. 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 más sería algo así como errno
. No desea que los hilos separados modifiquen errno
después de que una de sus llamadas falle, 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.