_Thread_local [C11] es un especificador de duración de almacenamiento que le indica al compilador que cada hilo debe tener su propia instancia privada de una variable. A diferencia de una variable static, que es compartida por todos los hilos, o una variable automática (local), que reside en la pila (stack) de cada hilo, las variables _Thread_local residen en un segmento de memoria especial llamado TLS (Thread Local Storage).
Esto funciona así porque el modelo de ejecución de hilos requiere que ciertos estados globales —como el código de error errno— sean únicos por hilo para evitar que una operación en el hilo A sobrescriba el error reportado por el hilo B. El uso de TLS permite que el acceso a estos datos sea extremadamente rápido, ya que el sistema operativo y el cargador suelen implementar esto mediante un puntero especial (como el registro fs en x86_64) que apunta a la base del bloque de memoria del hilo actual. Debes usarlo cuando necesites mantener un estado persistente durante la vida del hilo (como un buffer de trabajo o un contador de logs) sin incurrir en la sobrecarga de un mutex. Si intentas aplicarlo a una variable local automática dentro de una función, el compilador fallará; la variable debe tener ámbito de archivo o ser static. Si lo usas mal, podrías asumir que una variable es global cuando en realidad cada hilo está viendo una copia distinta, o podrías intentar inicializarla con una expresión no constante, lo cual es ilegal para variables con duración de almacenamiento estática.
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
/*
* _Thread_local [C11] garantiza que cada hilo tenga su propia copia
* independiente de estas variables. No hay contención de memoria.
*/
_Thread_local int thread_task_count = 0;
_Thread_local char thread_identity[16];
void* ejecutar_tarea(void* arg) {
int id = *((int*)arg);
// Escribimos en la memoria TLS única de este hilo.
snprintf(thread_identity, sizeof(thread_identity), "ID-%d", id);
for (int i = 0; i < 3; i++) {
// Incrementamos nuestro contador privado.
// No necesitamos mutex porque nadie más toca esta variable.
thread_task_count++;
printf("[%s] Iteración %d: contador local = %d\n",
thread_identity, i + 1, thread_task_count);
}
return NULL;
}
int main(void) {
pthread_t hilos[3];
int ids[3];
for (int i = 0; i < 3; i++) {
ids[i] = i + 1;
// La creación del hilo inicializa su bloque TLS.
if (pthread_create(&hilos[i], NULL, ejecutar_tarea, &ids[i]) != 0) {
perror("Error al crear el hilo");
return EXIT_FAILURE;
}
}
for (int i = 0; i < 3; i++) {
pthread_join(hilos[i], NULL);
}
return EXIT_SUCCESS;
}
Desglose del funcionamiento
En el código anterior, thread_task_count y thread_identity no se encuentran en el segmento .data convencional ni en la pila (stack) de la función ejecutar_tarea. Si fueran globales normales, los hilos pisarían los datos de los demás, resultando en una condición de carrera (race condition) y salidas de printf inconsistentes.
Al marcar thread_task_count como _Thread_local, el compilador calcula un desplazamiento (offset) constante desde un registro de base de hilo. Cuando ejecutar_tarea incrementa thread_task_count, la CPU está operando sobre una dirección de memoria que es única para el contexto de ese hilo. Fíjate en snprintf: cada hilo escribe su propia cadena en thread_identity. Aunque todos los hilos ejecutan la misma función y la misma instrucción de memoria, el hardware las redirige a regiones de TLS distintas.
Es importante notar que la inicialización de thread_task_count = 0 es una expresión constante. Si intentaras inicializarla con el resultado de una función, el compilador daría un error, ya que estas variables se preparan en el momento en que el hilo es creado, antes de que el cuerpo de la función se ejecute.
El error frecuente
Un error común es intentar declarar una variable _Thread_local dentro de una función sin el calificador static.
void funcion_con_error() {
// ERROR DE COMPILACIÓN: storage class specifier _Thread_local
// cannot be used with automatic variables.
_Thread_local int x = 10;
}
El motivo es estructural: una variable automática vive en el stack frame y su dirección depende del puntero de pila (stack pointer). El modelo TLS está diseñado para variables cuya dirección es predecible y estable durante toda la vida del hilo. Para usar memoria por hilo en el ámbito de una función, debes declarar la variable como static _Thread_local. Si usas _Thread_local con variables estáticas, el compilador y el enlazador se encargan de reservar espacio en el segmento TLS para cada hilo que se cree. Herramientas como AddressSanitizer son excelentes para detectar si intentas acceder a memoria de un hilo que ya ha finalizado, pero los errores de declaración de _Thread_local se detectarán en fase de compilación.
N° 70