1. pthread_exit: terminar un hilo
pthread_exit() termina el hilo que la llama, sin terminar el proceso. Es el equivalente de exit() para hilos: el hilo se detiene, se ejecutan las funciones registradas con pthread_cleanup_push, y los recursos se liberan (si el hilo es joinable, hasta que alguien lo espera; si es detached, inmediatamente).
#include
void pthread_exit(void *retval);
El parámetro retval es el valor de retorno del hilo, que se puede obtener con pthread_join(). Es un puntero void *, por lo que puede apuntar a cualquier tipo de dato.
pthread_exit se usa cuando un hilo necesita terminar antes de que su función retorne. Por ejemplo:
- El hilo detecta un error: termina con pthread_exit y un código de error.
- El hilo completó su tarea: termina con pthread_exit y el resultado.
- El hilo es cancelado: pthread_cancel llama internamente a pthread_exit.
Después de pthread_exit, el hilo no ejecuta más código. Las funciones registradas con pthread_cleanup_push se ejecutan en orden inverso (LIFO). Luego, si el hilo es joinable, se queda como "zombi" hasta que alguien llama a pthread_join. Si es detached, se liberan los recursos inmediatamente.
Retornar de la función del hilo es equivalente a llamar a pthread_exit con el valor de retorno. La diferencia es que pthread_exit se puede llamar desde cualquier función (no solo desde la función principal del hilo), lo que permite terminar el hilo desde una función auxiliar.
2. pthread_detach: separar un hilo
pthread_detach() marca un hilo como "detached" (separado). Un hilo detached se libera automáticamente cuando termina, sin necesidad de que nadie llame a pthread_join. Es el equivalente de "fire and forget": el hilo se ejecuta y se limpia solo.
#include
int pthread_detach(pthread_t thread);
Devuelve 0 si tiene éxito, o un código de error (EINVAL: hilo ya detached o no existe, ESRCH: hilo no existe).
Un hilo se puede marcar como detached en dos momentos:
- Al crearlo: con el atributo PTHREAD_CREATE_DETACHED.
- Después de crearlo: con pthread_detach(hilo).
Una vez detached, no se puede esperar con pthread_join. Si se intenta, pthread_join 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 ni esperarlo.
#define _POSIX_C_SOURCE 200809L
#include
#include
#include
#include
void *trabajo(void *arg)
{
int id = (int)(long) arg;
printf("Hilo %d: trabajando...\n", id);
sleep(2);
printf("Hilo %d: terminando (se libera automáticamente)\n", id);
return NULL;
}
int main(void)
{
pthread_t hilo;
pthread_create(&hilo, NULL, trabajo, (void *)(long) 1);
/* Separar el hilo: no necesitamos esperarlo */
pthread_detach(hilo);
printf("Main: hilo separado, continuando...\n");
sleep(3); /* dar tiempo al hilo para terminar */
printf("Main: terminando\n");
return 0;
}
El hilo se separa inmediatamente después de crearlo. El main no espera al hilo; simplemente continúa. El hilo se libera automáticamente cuando termina.
3. pthread_cancel: cancelar un hilo
pthread_cancel() solicita la cancelación de un hilo. Es el equivalente de enviar una señal a un proceso: pide al hilo que termine, pero no lo fuerza inmediatamente.
#include
int pthread_cancel(pthread_t thread);
Devuelve 0 si la solicitud se envió correctamente, o un código de error (ESRCH: hilo no existe).
pthread_cancel no mata al hilo inmediatamente: establece un flag de cancelación en el hilo. El hilo verifica este flag en los puntos de cancelación (cancellation points) y termina cuando lo detecta.
El comportamiento de pthread_cancel depende del estado de cancelación del hilo:
| Estado | Comportamiento |
|---|---|
| PTHREAD_CANCEL_ENABLE (por omisión) | El hilo puede ser cancelado. Verifica el flag en los puntos de cancelación. |
| PTHREAD_CANCEL_DISABLE | El hilo no puede ser cancelado. Las solicitudes se quedan pendientes. |
Y el tipo de cancelación determina cuándo se cancela:
| Tipo | Comportamiento |
|---|---|
| PTHREAD_CANCEL_DEFERRED (por omisión) | El hilo se cancela en el próximo punto de cancelación. |
| PTHREAD_CANCEL_ASYNCHRONOUS | El hilo se cancela inmediatamente (en cualquier instrucción). |
4. Cancelación diferida vs asíncrona
La cancelación diferida (deferred) es la por omisión y la recomendada. El hilo solo se cancela en los puntos de cancelación (funciones como sleep, read, wait, etc.). Esto es seguro porque el hilo está en un estado conocido.
La cancelación asíncrona (asynchronous) cancela al hilo inmediatamente, en cualquier instrucción. Esto es peligroso porque el hilo puede estar en medio de una operación (como malloc o escribir en un archivo) y dejar los datos en un estado inconsistente.
La cancelación asíncrona puede dejar mutex bloqueados, memoria sin liberar, y archivos en estado inconsistente. Solo se debe usar en casos muy específicos (por ejemplo, hilos que solo hacen cálculos puros sin recursos compartidos). En la mayoría de los casos, usar cancelación diferida.
El tipo y el estado de cancelación se configuran con pthread_setcancelstate() y pthread_setcanceltype():
#include
/* Cambiar el estado de cancelación */
int old_state;
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &old_state);
/* Cambiar el tipo de cancelación */
int old_type;
pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, &old_type);
Estas funciones se usan para proteger secciones críticas: deshabilitar la cancelación mientras se manipulan datos compartidos, y reactivarla después.
5. Puntos de cancelación
Los puntos de cancelación son funciones que verifican si el hilo tiene una solicitud de cancelación pendiente. Si la tiene, el hilo termina (llamando a pthread_exit). Las funciones que son puntos de cancelación incluyen:
sleep(),usleep(),nanosleep()read(),write(),open(),close()wait(),waitpid()pthread_cond_wait(),pthread_cond_timedwait()pthread_join(),pthread_testcancel()accept(),connect(),recv(),send()poll(),select(),epoll_wait()
Si un hilo no llama a ninguna de estas funciones (por ejemplo, un bucle de cálculo puro), nunca se cancelará con cancelación diferida. Para forzar un punto de cancelación, se llama a pthread_testcancel().
#define _POSIX_C_SOURCE 200809L
#include
#include
#include
#include
void *trabajo(void *arg)
{
(void) arg;
printf("Hilo: iniciando trabajo largo...\n");
for (int i = 0; i < 10; i++) {
printf("Hilo: paso %d\n", i);
sleep(1); /* punto de cancelación */
}
printf("Hilo: terminado normalmente\n");
return NULL;
}
int main(void)
{
pthread_t hilo;
pthread_create(&hilo, NULL, trabajo, NULL);
sleep(3); /* esperar 3 segundos */
printf("Main: cancelando el hilo...\n");
pthread_cancel(hilo);
void *retval;
pthread_join(hilo, &retval);
if (retval == PTHREAD_CANCELED)
printf("Main: hilo fue cancelado\n");
else
printf("Main: hilo terminó normalmente\n");
return 0;
}
$ gcc -std=c17 -Wall -Wextra -pthread -o cancelar_demo cancelar_demo.c
$ ./cancelar_demo
Hilo: iniciando trabajo largo...
Hilo: paso 0
Hilo: paso 1
Hilo: paso 2
Main: cancelando el hilo...
Main: hilo fue cancelado
El hilo se cancela en el sleep(1) del paso 2 (después de 3 segundos). El main recoge el resultado con pthread_join, que devuelve PTHREAD_CANCELED.
6. Limpieza con pthread_cleanup
Cuando un hilo se cancela, puede estar en medio de una operación (como tener un mutex bloqueado o memoria asignada). Para asegurar que los recursos se liberan, se usan las funciones pthread_cleanup_push() y pthread_cleanup_pop().
Estas funciones registran una función de limpieza que se ejecuta cuando el hilo termina (por pthread_exit, pthread_cancel, o return). Es el equivalente de atexit para hilos.
#include
void pthread_cleanup_push(void (*routine)(void *), void *arg);
void pthread_cleanup_pop(int execute);
pthread_cleanup_push registra una función de limpieza. pthread_cleanup_pop la desregistra. Si execute es distinto de 0, la función se ejecuta al desregistrar.
Las funciones de limpieza se ejecutan en orden inverso (LIFO), como atexit. Se ejecutan cuando el hilo termina por pthread_exit, pthread_cancel, o return.
#define _POSIX_C_SOURCE 200809L
#include
#include
#include
#include
void limpiar(void *arg)
{
int *p = (int *) arg;
printf("Limpieza: liberando memoria (%d)\n", *p);
free(p);
}
void *trabajo(void *arg)
{
int *datos = malloc(sizeof *datos);
*datos = 42;
/* Registrar limpieza */
pthread_cleanup_push(limpiar, datos);
printf("Hilo: trabajando con datos=%d\n", *datos);
sleep(5); /* punto de cancelación */
/* Si llegamos aquí, no fue cancelado */
pthread_cleanup_pop(1); /* ejecutar limpieza */
printf("Hilo: terminó normalmente\n");
return NULL;
}
int main(void)
{
pthread_t hilo;
pthread_create(&hilo, NULL, trabajo, NULL);
sleep(2);
printf("Main: cancelando el hilo...\n");
pthread_cancel(hilo);
pthread_join(hilo, NULL);
printf("Main: hilo cancelado, memoria liberada\n");
return 0;
}
$ gcc -std=c17 -Wall -Wextra -pthread -o cleanup_demo cleanup_demo.c
$ ./cleanup_demo
Hilo: trabajando con datos=42
Main: cancelando el hilo...
Limpieza: liberando memoria (42)
Main: hilo cancelado, memoria liberada
Cuando el hilo es cancelado, se ejecuta la función de limpieza (limpiar), que libera la memoria. Sin pthread_cleanup_push, la memoria se perdería (fuga de memoria).
pthread_cleanup_push y pthread_cleanup_pop deben aparecer en pares en la misma función. No se puede hacer push en una función y pop en otra. Esto es una restricción de la implementación (en glibc, push es una macro que abre un bloque).
7. Qué recursos se liberan
Cuando un hilo termina (por return, pthread_exit, o pthread_cancel), se liberan los siguientes recursos:
| Recurso | Se libera? | Cuándo |
|---|---|---|
| Pila del hilo | Sí | Inmediatamente (si detached) o al pthread_join (si joinable). |
| TCB del hilo | Sí | Inmediatamente (si detached) o al pthread_join (si joinable). |
| Memoria asignada con malloc | No (automáticamente) | Solo si se libera explícitamente (en pthread_cleanup o en la función). |
| Mutex bloqueados | No | Siguen bloqueados. Hay que desbloquearlos en pthread_cleanup. |
| Descriptores de archivo | No | Se comparten con el proceso; no se cierran al terminar el hilo. |
| Variables thread-local | Sí (si se registraron destructores) | Los destructores se ejecutan al terminar el hilo. |
La memoria asignada con malloc NO se libera automáticamente. Si el hilo no la libera antes de terminar, hay una fuga de memoria. Por eso, siempre se debe usar pthread_cleanup_push para liberar recursos en hilos que pueden ser cancelados.
Los mutex bloqueados por un hilo cancelado siguen bloqueados. Esto puede causar deadlock si otro hilo intenta bloquear el mismo mutex. La solución es desbloquear el mutex en la función de limpieza.
8. Comparación: exit, detach, cancel
| Operación | Qué hace | Recursos liberados | Se puede esperar? |
|---|---|---|---|
| return / pthread_exit | Termina el hilo normalmente. | Pila y TCB (si detached, inmediatamente; si joinable, al join). | Sí (si joinable). |
| pthread_detach | Marca el hilo como detached. | Ninguno (solo cambia el estado). | No (después de detach). |
| pthread_cancel | Solicita cancelación del hilo. | Pila y TCB (cuando el hilo termina). | Sí (con pthread_join, que devuelve PTHREAD_CANCELED). |
La elección depende del caso:
- return/pthread_exit: cuando el hilo termina su trabajo normalmente.
- pthread_detach: cuando no se necesita esperar al hilo ni su resultado.
- pthread_cancel: cuando se necesita terminar un hilo desde fuera.
Independientemente de cómo termine un hilo, los recursos que asignó (memoria, mutex, archivos) deben liberarse. Usar pthread_cleanup_push para asegurar que la limpieza se ejecuta incluso si el hilo es cancelado.
9. Un programa que demuestra terminar, separar y cancelar
El siguiente programa demuestra las tres formas de terminar un hilo:
#define _POSIX_C_SOURCE 200809L
#include
#include
#include
#include
void *trabajo_normal(void *arg)
{
int *datos = (int *) arg;
printf("Hilo normal: trabajando con %d\n", *datos);
sleep(1);
printf("Hilo normal: terminando\n");
return (void *)(long) (*datos * 2); /* devolver resultado */
}
void *trabajo_cancelable(void *arg)
{
(void) arg;
printf("Hilo cancelable: iniciando...\n");
for (int i = 0; i < 10; i++) {
printf("Hilo cancelable: paso %d\n", i);
sleep(1);
}
printf("Hilo cancelable: terminado normalmente\n");
return NULL;
}
int main(void)
{
pthread_t hilo1, hilo2, hilo3;
int valor = 42;
/* 1. Hilo normal: termina y se espera */
printf("=== Hilo normal ===\n");
pthread_create(&hilo1, NULL, trabajo_normal, &valor);
void *res;
pthread_join(hilo1, &res);
printf("Main: resultado = %d\n", (int)(long) res);
/* 2. Hilo detached: se separa y no se espera */
printf("\n=== Hilo detached ===\n");
pthread_create(&hilo2, NULL, trabajo_normal, &valor);
pthread_detach(hilo2);
printf("Main: hilo separado, continuando...\n");
sleep(2);
/* 3. Hilo cancelable: se cancela desde fuera */
printf("\n=== Hilo cancelable ===\n");
pthread_create(&hilo3, NULL, trabajo_cancelable, NULL);
sleep(3);
printf("Main: cancelando...\n");
pthread_cancel(hilo3);
pthread_join(hilo3, &res);
if (res == PTHREAD_CANCELED)
printf("Main: hilo fue cancelado\n");
printf("\nTodos los hilos terminaron\n");
return 0;
}
$ gcc -std=c17 -Wall -Wextra -pthread -o terminar_separar_cancelar terminar_separar_cancelar.c
$ ./terminar_separar_cancelar
=== Hilo normal ===
Hilo normal: trabajando con 42
Hilo normal: terminando
Main: resultado = 84
=== Hilo detached ===
Hilo normal: trabajando con 42
Hilo normal: terminando
Main: hilo separado, continuando...
=== Hilo cancelable ===
Hilo cancelable: iniciando...
Hilo cancelable: paso 0
Hilo cancelable: paso 1
Hilo cancelable: paso 2
Main: cancelando...
Main: hilo fue cancelado
Todos los hilos terminaron
El programa demuestra las tres formas: el hilo normal termina y devuelve un resultado, el hilo detached se separa y no se espera, y el hilo cancelable se cancela desde fuera.
10. Resumen
- pthread_exit termina el hilo con un valor de retorno.
- pthread_detach marca el hilo como detached: se libera automáticamente al terminar.
- pthread_cancel solicita la cancelación del hilo (diferida o asíncrona).
- La cancelación diferida (por omisión) solo cancela en puntos de cancelación.
- La cancelación asíncrona es peligrosa: puede dejar recursos en estado inconsistente.
- pthread_cleanup_push/pop registran funciones de limpieza que se ejecutan al terminar.
- Los recursos (memoria, mutex) no se liberan automáticamente: hay que liberarlos explícitamente.
- Los mutex bloqueados por un hilo cancelado siguen bloqueados.
11. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. Que hace pthread_exit?
Termina el hilo actual con un valor de retorno. Es el equivalente de exit() para hilos.
2. Que hace pthread_detach?
Marca un hilo como detached: se libera automáticamente al terminar, sin necesidad de pthread_join.
3. Que hace pthread_cancel?
Solicita la cancelación de un hilo. El hilo se cancela en el próximo punto de cancelación (cancelación diferida).
4. Que es la cancelación diferida?
El hilo solo se cancela cuando llama a una función que es punto de cancelación (sleep, read, wait, etc.). Es la por omisión y la recomendada.
5. Que es la cancelación asíncrona?
El hilo se cancela inmediatamente, en cualquier instrucción. Es peligrosa y solo se usa en casos específicos.
6. Que es un punto de cancelación?
Una función que verifica si hay una solicitud de cancelación pendiente y, si la hay, termina el hilo. Incluye sleep, read, write, wait, pthread_cond_wait, etc.
7. Que hace pthread_cleanup_push?
Registra una función de limpieza que se ejecuta cuando el hilo termina (por pthread_exit, pthread_cancel, o return).
8. Que recursos se liberan automáticamente al terminar un hilo?
Solo la pila y el TCB. La memoria (malloc), los mutex y los descriptores no se liberan automáticamente.
9. Que pasa con los mutex bloqueados por un hilo cancelado?
Siguen bloqueados. Hay que desbloquearlos en la función de limpieza (pthread_cleanup_push).
10. Que devuelve pthread_join cuando el hilo fue cancelado?
PTHREAD_CANCELED. Se puede verificar con: if (retval == PTHREAD_CANCELED).
Para practicar
- Compila y ejecuta cancelar_demo.c. Observa cómo el hilo se cancela en sleep().
- Compila y ejecuta cleanup_demo.c. Verifica que la memoria se libera al cancelar.
- Compila y ejecuta terminar_separar_cancelar.c. Observa las tres formas de terminar.
- Escribe un programa que use pthread_cleanup_push para desbloquear un mutex al cancelar.
- Escribe un programa que deshabilite la cancelación en una sección crítica.
- Usa pthread_testcancel() en un bucle de cálculo puro para permitir la cancelación.