1. Qué son los atributos de un hilo
Los atributos de un hilo son parámetros configurables que determinan cómo se crea y se ejecuta un hilo: el tamaño de su pila, si es detached o joinable, su política de planificación y su prioridad, entre otros. Se configuran a través de la estructura pthread_attr_t.
Hasta ahora hemos creado hilos con NULL como atributo, lo que usa valores por omisión. Pero en situaciones reales, es importante configurar los atributos correctamente:
- Pila pequeña: si el hilo no necesita mucha pila, se puede reducir para ahorrar memoria.
- Pila grande: si el hilo usa recursión profunda o variables locales grandes, se necesita una pila mayor.
- Detached: si el hilo no necesita ser esperado, se crea como detached para liberar recursos automáticamente.
- Prioridad: si un hilo es más importante que otros, se le asigna mayor prioridad.
Los atributos se configuran en una estructura pthread_attr_t antes de llamar a pthread_create. Una vez creado el hilo, los atributos no se pueden cambiar (salvo la prioridad, que se puede cambiar con pthread_setschedparam).
2. pthread_attr_t: la estructura
pthread_attr_t es una estructura opaca que contiene los atributos del hilo. Se declara en <pthread.h> y se manipula con funciones específicas.
Los atributos que se pueden configurar son:
| Atributo | Función de configuración | Valor por omisión |
|---|---|---|
| Estado (detached/joinable) | pthread_attr_setdetachstate() | PTHREAD_CREATE_JOINABLE |
| Tamaño de la pila | pthread_attr_setstacksize() | 8 MB (en Linux x86-64) |
| Dirección de la pila | pthread_attr_setstack() | Asignada por el sistema |
| Política de planificación | pthread_attr_setschedpolicy() | SCHED_OTHER (normal) |
| Parámetros de planificación | pthread_attr_setschedparam() | Prioridad 0 |
| Inherit scheduler | pthread_attr_setinheritsched() | PTHREAD_INHERIT_SCHED |
| Guard size | pthread_attr_setguardsize() | Una página (4096 bytes) |
Los atributos se configuran con funciones pthread_attr_setX() y se consultan con pthread_attr_getX().
3. Inicializar y destruir
Antes de usar una estructura pthread_attr_t, debe inicializarse con pthread_attr_init(). Después de usarla, debe destruirse con pthread_attr_destroy() para liberar los recursos internos.
#include
pthread_attr_t attr;
/* Inicializar con valores por omisión */
if (pthread_attr_init(&attr) != 0) {
perror("pthread_attr_init");
return 1;
}
/* Configurar atributos... */
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
/* Crear hilo con atributos */
pthread_t hilo;
pthread_create(&hilo, &attr, funcion, NULL);
/* Destruir la estructura de atributos */
pthread_attr_destroy(&attr);
La estructura de atributos se puede reutilizar para crear múltiples hilos con los mismos atributos. Después de crear todos los hilos, se destruye una sola vez.
Si se pasa NULL como atributo a pthread_create, se usan los valores por omisión y no se necesita inicializar ni destruir una estructura.
4. El tamaño de la pila
El tamaño de la pila es uno de los atributos más importantes. Determina cuánta memoria se reserva para la pila del hilo. Por omisión, en Linux x86-64, es 8 MB.
Se configura con pthread_attr_setstacksize():
#include
pthread_attr_t attr;
pthread_attr_init(&attr);
/* Establecer pila de 1 MB */
size_t stack_size = 1024 * 1024; /* 1 MB */
pthread_attr_setstacksize(&attr, stack_size);
/* Verificar */
size_t actual;
pthread_attr_getstacksize(&attr, &actual);
printf("Tamaño de pila: %zu bytes\n", actual);
El tamaño de la pila debe ser al menos PTHREAD_STACK_MIN (típicamente 16384 bytes en Linux). Si se establece un valor menor, pthread_create falla con EINVAL.
Razones para cambiar el tamaño de la pila:
- Pila pequeña: si el hilo no usa recursión profunda ni variables locales grandes, se puede reducir la pila para ahorrar memoria. Útil en sistemas con muchos hilos.
- Pila grande: si el hilo usa recursión profunda o variables locales grandes (por ejemplo, un arreglo de 1 MB), se necesita una pila mayor.
Si un hilo usa más pila de la que tiene asignada, se produce un desbordamiento de pila (stack overflow), que causa SIGSEGV y termina el proceso. No hay protección automática contra esto (a diferencia de la pila del proceso principal, que tiene una página guardia).
Además del tamaño, se puede establecer la dirección de la pila con pthread_attr_setstack(). Esto es útil cuando se quiere usar una pila pre-asignada (por ejemplo, en sistemas embebidos).
5. El estado detached
El estado detached determina si el hilo es joinable o detached. Por omisión, los hilos son joinable (PTHREAD_CREATE_JOINABLE). Se puede crear un hilo detached desde el principio con PTHREAD_CREATE_DETACHED:
pthread_attr_t attr;
pthread_attr_init(&attr);
/* Crear como detached */
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_t hilo;
pthread_create(&hilo, &attr, trabajo, NULL);
/* No se puede llamar a pthread_join */
/* El hilo se libera automáticamente al terminar */
pthread_attr_destroy(&attr);
Crear un hilo como detached desde el principio es más eficiente que crearlo como joinable y luego llamar a pthread_detach, porque evita la sincronización interna.
Un hilo detached:
- No se puede esperar con pthread_join (devuelve EINVAL).
- Se libera automáticamente al terminar.
- No se puede cancelar con pthread_cancel (devuelve EINVAL).
Los hilos detached son útiles para tareas de "fire and forget": un hilo que hace una tarea y termina, sin que el main necesite su resultado.
6. La política de planificación
La política de planificación determina cómo el planificador trata al hilo. En Linux, hay tres políticas:
| Política | Descripción | Prioridad |
|---|---|---|
| SCHED_OTHER (por omisión) | Planificación normal con CFS. La prioridad se ajusta dinámicamente. | 0 (nice) |
| SCHED_FIFO | First-In-First-Out: el hilo se ejecuta hasta que se bloquea o termina. Sin cuanto de tiempo. | 1-99 |
| SCHED_RR | Round-Robin: como SCHED_FIFO pero con cuanto de tiempo. Los hilos se turnan. | 1-99 |
Las políticas de tiempo real (SCHED_FIFO y SCHED_RR) requieren privilegios de root. Solo se usan para tareas críticas (audio, video, control industrial).
Para configurar la política de planificación, se usa pthread_attr_setschedpolicy() y pthread_attr_setschedparam():
pthread_attr_t attr;
pthread_attr_init(&attr);
/* Establecer política FIFO */
pthread_attr_setschedpolicy(&attr, SCHED_FIFO);
/* Establecer prioridad */
struct sched_param param;
param.sched_priority = 50; /* 1-99 para tiempo real */
pthread_attr_setschedparam(&attr, ¶m);
/* Heredar la política del padre (por omisión) */
pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);
El flag PTHREAD_EXPLICIT_SCHED indica que el hilo usará la política y prioridad especificadas en los atributos. El flag PTHREAD_INHERIT_SCHED (por omisión) indica que heredará la política del hilo que lo crea.
Solo root puede crear hilos con SCHED_FIFO o SCHED_RR. Un usuario normal que intente crear un hilo de tiempo real recibirá EPERM. Esto es una protección: un hilo de tiempo real con prioridad alta puede monopolizar la CPU.
7. La prioridad
La prioridad de un hilo determina su importancia relativa frente a otros hilos. En Linux, la prioridad depende de la política:
- SCHED_OTHER: la prioridad se expresa como nice (-20 a +19). Se configura con setpriority() o pthread_setschedprio().
- SCHED_FIFO/SCHED_RR: la prioridad es de 1 a 99. Mayor número = mayor prioridad.
La prioridad de un hilo se puede cambiar después de crearlo con pthread_setschedparam() o pthread_setschedprio():
/* Cambiar prioridad (con política) */
struct sched_param param;
param.sched_priority = 10;
pthread_setschedparam(hilo, SCHED_FIFO, ¶m);
/* Cambiar solo la prioridad (sin cambiar política) */
pthread_setschedprio(hilo, 5);
La prioridad se puede consultar con pthread_getschedparam():
int policy;
struct sched_param param;
pthread_getschedparam(hilo, &policy, ¶m);
printf("Política: %d, Prioridad: %d\n", policy, param.sched_priority);
8. Otros atributos
Además de los atributos principales, hay otros que se pueden configurar:
| Atributo | Función | Descripción |
|---|---|---|
| Guard size | pthread_attr_setguardsize() | Tamaño de la página guardia al final de la pila. Por omisión, una página (4096 bytes). Se usa para detectar desbordamiento de pila. |
| Contención de scope | pthread_attr_setscope() | PTHREAD_SCOPE_SYSTEM (el hilo compite con todos los hilos del sistema) o PTHREAD_SCOPE_PROCESS (el hilo compite solo con los hilos del proceso). Por omisión, SYSTEM en Linux. |
| Inherit scheduler | pthread_attr_setinheritsched() | PTHREAD_INHERIT_SCHED (hereda del padre) o PTHREAD_EXPLICIT_SCHED (usa los valores de attr). |
El guard size es una página de memoria marcada como no accesible al final de la pila. Si el hilo desborda la pila, toca la página guardia y genera SIGSEGV. Esto ayuda a detectar desbordamientos de pila.
El scope determina el ámbito de competencia del hilo. PTHREAD_SCOPE_SYSTEM significa que el hilo compite con todos los hilos del sistema (incluso de otros procesos). PTHREAD_SCOPE_PROCESS significa que compite solo con los hilos del mismo proceso. En Linux, solo SYSTEM está soportado.
9. Crear hilos con atributos
El patrón completo para crear un hilo con atributos es:
- Inicializar la estructura de atributos: pthread_attr_init(&attr).
- Configurar los atributos deseados: pthread_attr_setX(&attr, valor).
- Crear el hilo: pthread_create(&hilo, &attr, funcion, arg).
- Destruir la estructura de atributos: pthread_attr_destroy(&attr).
La estructura de atributos se puede reutilizar para crear múltiples hilos con los mismos atributos. Después de crear todos los hilos, se destruye una sola vez.
#define _POSIX_C_SOURCE 200809L
#include
#include
#include
#include
void *trabajo(void *arg)
{
int id = (int)(long) arg;
printf("Hilo %d: pila=%zu bytes\n", id, (size_t) 0);
/* Usar un poco de pila */
char buffer[1024];
buffer[0] = id;
sleep(1);
printf("Hilo %d: terminando\n", id);
return NULL;
}
int main(void)
{
pthread_attr_t attr;
pthread_attr_init(&attr);
/* Configurar: detached, pila de 1MB */
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_attr_setstacksize(&attr, 1024 * 1024);
/* Verificar tamaño de pila */
size_t stack_size;
pthread_attr_getstacksize(&attr, &stack_size);
printf("Tamaño de pila configurado: %zu bytes\n", stack_size);
/* Crear 3 hilos con estos atributos */
for (int i = 0; i < 3; i++) {
pthread_t hilo;
int ret = pthread_create(&hilo, &attr, trabajo, (void *)(long)(i + 1));
if (ret != 0) {
fprintf(stderr, "pthread_create: %s\n", strerror(ret));
return 1;
}
printf("Hilo %d creado (detached)\n", i + 1);
}
pthread_attr_destroy(&attr);
/* Dar tiempo a los hilos para terminar */
sleep(3);
printf("Main: terminando\n");
return 0;
}
$ gcc -std=c17 -Wall -Wextra -pthread -o atributos_completo atributos_completo.c
$ ./atributos_completo
Tamaño de pila configurado: 1048576 bytes
Hilo 1 creado (detached)
Hilo 1: pila=0 bytes
Hilo 2 creado (detached)
Hilo 2: pila=0 bytes
Hilo 3 creado (detached)
Hilo 3: pila=0 bytes
Hilo 1: terminando
Hilo 2: terminando
Hilo 3: terminando
Main: terminando
Los hilos se crean como detached con una pila de 1 MB. Se liberan automáticamente al terminar, sin necesidad de pthread_join.
10. Un programa que demuestra atributos
El siguiente programa crea hilos con diferentes tamaños de pila y verifica que funcionan:
#define _POSIX_C_SOURCE 200809L
#include
#include
#include
#include
void *trabajo(void *arg)
{
int id = (int)(long) arg;
/* Usar un poco de pila */
char buffer[4096];
buffer[0] = id;
printf("Hilo %d: ejecutándose con pila pequeña\n", id);
sleep(1);
printf("Hilo %d: terminando\n", id);
return NULL;
}
int main(void)
{
pthread_attr_t attr;
pthread_attr_init(&attr);
/* Pila pequeña: 64 KB */
size_t small_stack = 64 * 1024;
pthread_attr_setstacksize(&attr, small_stack);
size_t actual;
pthread_attr_getstacksize(&attr, &actual);
printf("Tamaño de pila: %zu bytes (%.0f KB)\n", actual, actual / 1024.0);
/* Crear 3 hilos con pila pequeña */
pthread_t hilos[3];
for (int i = 0; i < 3; i++) {
int ret = pthread_create(&hilos[i], &attr, trabajo, (void *)(long)(i + 1));
if (ret != 0) {
fprintf(stderr, "pthread_create: %s\n", strerror(ret));
return 1;
}
}
for (int i = 0; i < 3; i++)
pthread_join(hilos[i], NULL);
pthread_attr_destroy(&attr);
printf("Todos los hilos terminaron\n");
return 0;
}
$ gcc -std=c17 -Wall -Wextra -pthread -o demo_atributos demo_atributos.c
$ ./demo_atributos
Tamaño de pila: 65536 bytes (64 KB)
Hilo 1: ejecutándose con pila pequeña
Hilo 2: ejecutándose con pila pequeña
Hilo 3: ejecutándose con pila pequeña
Hilo 1: terminando
Hilo 2: terminando
Hilo 3: terminando
Todos los hilos terminaron
Los hilos se crean con una pila de 64 KB (en lugar de 8 MB por omisión). Esto ahorra memoria: 3 hilos usan 192 KB en lugar de 24 MB.
11. Resumen
- Los atributos de un hilo se configuran con pthread_attr_t antes de crearlo.
- pthread_attr_init inicializa la estructura, pthread_attr_destroy la libera.
- El tamaño de la pila se configura con pthread_attr_setstacksize (por omisión, 8 MB).
- Se puede crear un hilo como detached desde el principio con PTHREAD_CREATE_DETACHED.
- La política de planificación puede ser SCHED_OTHER, SCHED_FIFO o SCHED_RR.
- Las políticas de tiempo real requieren privilegios de root.
- La prioridad se configura con pthread_attr_setschedparam.
- El guard size es una página de protección al final de la pila.
- Los atributos se pueden reutilizar para crear múltiples hilos.
12. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. Que es pthread_attr_t?
Una estructura que contiene los atributos de un hilo. Se configura antes de crear el hilo con pthread_create.
2. Como se inicializa pthread_attr_t?
Con pthread_attr_init(&attr). Después de usarla, se destruye con pthread_attr_destroy(&attr).
3. Cual es el tamaño de pila por omisión en Linux?
8 MB en sistemas de 64 bits (x86-64).
4. Como se configura el tamaño de la pila?
Con pthread_attr_setstacksize(&attr, tamaño). El mínimo es PTHREAD_STACK_MIN.
5. Como se crea un hilo detached desde el principio?
Con pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED) antes de pthread_create.
6. Que políticas de planificación existen?
SCHED_OTHER (normal, por omisión), SCHED_FIFO (tiempo real, sin cuanto) y SCHED_RR (tiempo real, con cuanto).
7. Que privilegios se necesitan para políticas de tiempo real?
Privilegios de root. Un usuario normal que intente crear un hilo de tiempo real recibe EPERM.
8. Que es el guard size?
Una página de memoria marcada como no accesible al final de la pila. Detecta desbordamientos de pila.
9. Que es PTHREAD_INHERIT_SCHED?
Un flag que indica que el hilo hereda la política de planificación del hilo que lo crea. Es el valor por omisión.
10. Se puede reutilizar pthread_attr_t para crear varios hilos?
Sí. Se configura una vez y se pasa a múltiples pthread_create. Se destruye después de crear todos los hilos.
Para practicar
- Compila y ejecuta atributos_completo.c. Observa que los hilos se crean como detached.
- Compila y ejecuta demo_atributos.c. Verifica que los hilos funcionan con pila de 64 KB.
- Modifica demo_atributos.c para usar una pila de 4 KB. ¿Funciona? ¿Por qué?
- Escribe un programa que cree un hilo con SCHED_FIFO y prioridad 50 (requiere root).
- Escribe un programa que cree 100 hilos con pila de 64 KB y mida el consumo de memoria.
- Investiga PTHREAD_STACK_MIN en tu sistema.