Volver a Procesos e Hilos Bloque 7 - Hilos y la biblioteca pthreads - Tema 32 de 43

Tema 32

Terminar, separar y cancelar hilos

En el tema anterior viste cómo pasar datos a un hilo y recibir resultados. Pero ¿qué pasa cuando un hilo termina? ¿Cómo se liberan sus recursos? ¿Se puede cancelar un hilo desde fuera? En este tema verás pthread_exit, pthread_detach y pthread_cancel, y qué recursos se liberan en cada caso.

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).

C - firma de pthread_exit

#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:

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.

pthread_exit vs return

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.

C - firma de pthread_detach

#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:

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.

detach_demo.c

#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.

C - firma de pthread_cancel

#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:

EstadoComportamiento
PTHREAD_CANCEL_ENABLE (por omisión)El hilo puede ser cancelado. Verifica el flag en los puntos de cancelación.
PTHREAD_CANCEL_DISABLEEl hilo no puede ser cancelado. Las solicitudes se quedan pendientes.

Y el tipo de cancelación determina cuándo se cancela:

TipoComportamiento
PTHREAD_CANCEL_DEFERRED (por omisión)El hilo se cancela en el próximo punto de cancelación.
PTHREAD_CANCEL_ASYNCHRONOUSEl 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 es peligrosa

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():

C - configurar cancelación

#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:

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().

cancelar_demo.c

#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;
}
terminal - ejecutar cancelar_demo

$ 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.

C - firma de pthread_cleanup

#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.

cleanup_demo.c

#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;
}
terminal - ejecutar cleanup_demo

$ 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 pop deben estar balanceados

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:

RecursoSe libera?Cuándo
Pila del hiloSíInmediatamente (si detached) o al pthread_join (si joinable).
TCB del hiloSíInmediatamente (si detached) o al pthread_join (si joinable).
Memoria asignada con mallocNo (automáticamente)Solo si se libera explícitamente (en pthread_cleanup o en la función).
Mutex bloqueadosNoSiguen bloqueados. Hay que desbloquearlos en pthread_cleanup.
Descriptores de archivoNoSe comparten con el proceso; no se cierran al terminar el hilo.
Variables thread-localSí (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ónQué haceRecursos liberadosSe puede esperar?
return / pthread_exitTermina el hilo normalmente.Pila y TCB (si detached, inmediatamente; si joinable, al join).Sí (si joinable).
pthread_detachMarca el hilo como detached.Ninguno (solo cambia el estado).No (después de detach).
pthread_cancelSolicita 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:

Siempre limpiar los recursos

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:

terminar_separar_cancelar.c

#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;
}
terminal - ejecutar terminar_separar_cancelar

$ 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

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

  1. Compila y ejecuta cancelar_demo.c. Observa cómo el hilo se cancela en sleep().
  2. Compila y ejecuta cleanup_demo.c. Verifica que la memoria se libera al cancelar.
  3. Compila y ejecuta terminar_separar_cancelar.c. Observa las tres formas de terminar.
  4. Escribe un programa que use pthread_cleanup_push para desbloquear un mutex al cancelar.
  5. Escribe un programa que deshabilite la cancelación en una sección crítica.
  6. Usa pthread_testcancel() en un bucle de cálculo puro para permitir la cancelación.