Crear y esperar hilos - Procesos e Hilos
Volver a Procesos e Hilos Bloque 7 - Hilos y la biblioteca pthreads - Tema 30 de 43

Tema 30

Crear y esperar hilos

En el tema anterior viste que son los hilos y como se implementan. Ahora toca la práctica: Como crear hilos con pthread_create, Como esperarlos con pthread_join, Como terminarlos con pthread_exit, y como observarlos con ps, top y /proc. Todo lo que necesitas para empezar a programar con hilos.

1. pthread_create: crear un hilo

pthread_create() crea un nuevo hilo en el proceso actual. El hilo empieza ejecutando una función especificada por el programador. Es la syscall clone() por debajo, pero con una interfaz más amigable.

C - firma de pthread_create

#include 

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine)(void *), void *arg);

Los parámetros son:

Devuelve 0 Sí tiene exito, o un código de error (no errno) Sí falla. Los errores más comunes son EAGAIN (Sin recursos para crear el hilo) y EINVAL (atributos inválidos).

El hilo creado empieza ejecutando inmediatamente (o cuándo el planificador lo decide). No hay garantia de que se ejecute antes o después del hilo que lo creó.

2. Los parámetros de pthread_create

Los parámetros de pthread_create son:

ParametroTipoDescripcion
threadpthread_t *Puntero dónde se guarda el ID del hilo. Se usa después para pthread_join, pthread_detach, etc.
attrconst pthread_attr_t *Atributos del hilo (tamano de pila, estado detached, etc.). NULL para valores por omision.
start_routinevoid *(*)(void *)Puntero a la función que el hilo ejecutara. Debe tener la firma void *func(void *).
argvoid *Argumento que se pasa a start_routine. Puede ser NULL.

El tipo pthread_t es un identificador de hilo. En linux (NPTL), es un unsigned long. No se debe imprimir con %d; usar %lu o convertir a unsigned long.

Los atributos (attr) se configuran con pthread_attr_t. El tema 33 profundiza en los atributos. Por ahora, NULL es suficiente.

3. La función del hilo

La función que ejecuta el hilo debe tener la firma:

C - firma de la función del hilo

void *Función(void *arg);

Recibe un void * (el argumento pasado a pthread_create) y devuelve un void * (el resultado, Que se puede obtener con pthread_join).

La función del hilo es como la función main de un proceso: es el punto de entrada del hilo. Cuando la función retorna, el hilo termina (equivalente a pthread_exit con el valor de retorno).

hilo_Simple.c

#define _POSIX_C_SOURCE 200809L
#include 
#include 
#include 
#include 

void *trabajo(void *arg)
{
    int id = *(int *) arg;
    printf("Hilo %d: iniciado (TID %lu)\n", id, (unsigned long) pthread_self());
    sleep(1);
    printf("Hilo %d: terminando\n", id);
    return NULL;  /* equivalente a pthread_exit(NULL) */
}

int main(void)
{
    pthread_t hilo;
    int id = 42;

    printf("Main: creando hilo...\n");
    int ret = pthread_create(&hilo, NULL, trabajo, &id);
    if (ret != 0) {
        fprintf(stderr, "pthread_create: %s\n", strerror(ret));
        return 1;
    }

    printf("Main: hilo creado, esperando...\n");
    pthread_join(hilo, NULL);
    printf("Main: hilo terminado\n");
    return 0;
}

4. pthread_join: esperar a un hilo

pthread_join() bloquea al hilo que la llama hasta que el hilo especificado termina. Es el equivalente de wait() para hilos.

C - firma de pthread_join

#include 

int pthread_join(pthread_t thread, void **retval);

Los parámetros son:

Devuelve 0 Sí tiene exito, o un código de error (EINVAL: hilo no joinable, ESRCH: hilo no existe, EDEADLK: deadlock).

pthread_join también libera los recursos del hilo terminado (su pila, TCB, etc.). Si no se llama a pthread_join ni a pthread_detach, el hilo terminado queda como "zombi" (Similar a un proceso zombi) y sus recursos no se liberan.

Un hilo sólo se puede esperar una vez. Despues de pthread_join, el hilo ya no existe.

5. pthread_exit: terminar un hilo

pthread_exit() termina el hilo que la llama, Sin terminar el proceso. Es el equivalente de exit() para hilos.

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.

pthread_exit se usa cuándo un hilo necesita terminar antes de que su función retorne. Por ejemplo, en un hilo que detecta un error y quiere terminar sin afectar a los demás hilos.

pthread_exit_demo.c

#define _POSIX_C_SOURCE 200809L
#include 
#include 
#include 

void *trabajo(void *arg)
{
    int *resultado = malloc(Sizeof *resultado);
    *resultado = 42;
    printf("Hilo: calculando resultado...\n");
    pthread_exit(resultado);  /* terminar con resultado */
    /* Este código no se ejecuta */
    printf("Esto no se imprime\n");
    return NULL;
}

int main(void)
{
    pthread_t hilo;
    pthread_create(&hilo, NULL, trabajo, NULL);

    void *retval;
    pthread_join(hilo, &retval);
    int *resultado = retval;
    printf("Main: resultado = %d\n", *resultado);
    free(resultado);
    return 0;
}

El hilo calcula un resultado, lo asigna a un puntero, y termina con pthread_exit. El main recoge el resultado con pthread_join.

No retornar un puntero a una variable local

Si el hilo retorna un puntero a una variable local (en la pila del hilo), el puntero queda inválido cuándo el hilo termina (porque la pila se libera). Siempre hay que usar malloc o memoria estatica para los resultados que se pasan a pthread_join.

6. Compilar con -pthread

Para compilar un programa con pthreads, se usa el flag -pthread en gcc. Este flag hace dos cosas:

terminal - compilar con -pthread

$ gcc -std=c17 -Wall -Wextra -pthread -o programa programa.c
$ gcc -std=c17 -Wall -Wextra -pthread -g -o programa programa.c  # con debug

En versiones antiguas de gcc, se usaba -lpthread en lugar de -pthread. Hoy en dia, -pthread es la forma recomendada porque también define _REENTRANT.

No olvidar -pthread

Si se olvida -pthread, el programa puede compilar pero fallar en tiempo de ejecución (por ejemplo, pthread_create puede devolver un error, o el programa puede tener comportamiento indefinido). Siempre usar -pthread.

7. Ver los hilos con ps -L

ps -L muestra los hilos de un proceso, con su TID (Thread ID). Es la forma más directa de ver los hilos de un proceso.

terminal - ps -L

$ ./hilos_demo &
[1] 1100
$ ps -L -p 1100 -o pid,tid,stat,comm
    PID     TID STAT COMMAND
   1100    1100 Sl+  hilos_demo
   1100    1101 Sl+  hilos_demo
   1100    1102 Sl+  hilos_demo
   1100    1103 Sl+  hilos_demo

Los 4 hilos (1 principal + 3 creados) tienen el mismo PID (1100) pero TIDs distintos. La columna STAT muestra el estado de cada hilo (Sl+ = dormido, multihilo, en primer plano).

Para ver todos los hilos del sistema:

terminal - todos los hilos del sistema

$ ps -eL | head -20
    PID     TID    PPID ...  COMMAND
      1       1       0 ...  systemd
     44     44       1 ...  systemd-journal
     44     45       1 ...  systemd-journal
     ...

8. Ver los hilos con top -H

top -H muestra los hilos como sí fueran procesos individuales, con su uso de CPU y memoria. Es útil para ver que hilo esta consumiendo más recursos.

terminal - top -H

$ top -H -p 1100
  PID USER  ...  %CPU %MEM  TIME+  COMMAND
 1101 ana   ...   0.3  0.1   0:00.01 hilos_demo
 1102 ana   ...   0.3  0.1   0:00.01 hilos_demo
 1103 ana   ...   0.3  0.1   0:00.01 hilos_demo
 1100 ana   ...   0.0  0.1   0:00.00 hilos_demo

En top, la columna PID muestra el TID del hilo (no el PID del proceso). La columna COMMAND muestra el nombre del programa para todos los hilos.

Dentro de top, se puede pulsar H para alternar entre mostrar hilos y procesos.

9. Hilos en /proc

Los hilos de un proceso se exponen en /proc/PID/task/, Dónde cada subdirectorio tiene el TID del hilo:

terminal - hilos en /proc

$ ls /proc/1100/task/
1100  1101  1102  1103
$ cat /proc/1100/task/1101/comm
hilos_demo
$ grep state /proc/1100/task/1101/status
State:  S (sleeping)

Cada hilo tiene su propio directorio en /proc/PID/task/TID, con los mismos archivos que un proceso (status, stat, stack, etc.). Esto permite inspeccionar cada hilo individualmente.

El número de hilos se muestra en /proc/PID/status en el campo threads:

terminal - Número de hilos

$ grep threads /proc/1100/status
Threads: 4

10. Errores comunes

Los errores más comunes al crear y esperar hilos son:

Siempre join o detach

Todo hilo creado debe ser esperado (pthread_join) o separado (pthread_detach). Si no, sus recursos no se liberan y se acumulan "hilos zombis". Es el equivalente de los procesos zombis para hilos.

11. Un programa que crea y espera hilos

El siguiente programa crea 5 hilos, cada uno calcula un resultado, y el main recoge los resultados:

crear_esperar.c

#define _POSIX_C_SOURCE 200809L
#include 
#include 
#include 
#include 

void *calcular(void *arg)
{
    int n = *(int *) arg;
    int *resultado = malloc(Sizeof *resultado);
    *resultado = n * n;  /* cuadrado */
    printf("Hilo %d: %d^2 = %d\n", n, n, *resultado);
    sleep(1);
    return resultado;
}

int main(void)
{
    pthread_t hilos[5];
    int valores[5] = {1, 2, 3, 4, 5};
    void *resultados[5];

    /* Crear hilos */
    for (int i = 0; i < 5; i++) {
        int ret = pthread_create(&hilos[i], NULL, calcular, &valores[i]);
        if (ret != 0) {
            fprintf(stderr, "pthread_create: %s\n", strerror(ret));
            return 1;
        }
    }

    /* Esperar a todos y recoger resultados */
    for (int i = 0; i < 5; i++) {
        pthread_join(hilos[i], &resultados[i]);
        int *res = resultados[i];
        printf("Main: resultado del hilo %d = %d\n", i, *res);
        free(res);
    }

    printf("Todos los hilos terminaron\n");
    return 0;
}
terminal - ejecutar crear_esperar

$ gcc -std=c17 -Wall -Wextra -pthread -o crear_esperar crear_esperar.c
$ ./crear_esperar
Hilo 1: 1^2 = 1
Hilo 2: 2^2 = 4
Hilo 3: 3^2 = 9
Hilo 4: 4^2 = 16
Hilo 5: 5^2 = 25
Main: resultado del hilo 0 = 1
Main: resultado del hilo 1 = 4
Main: resultado del hilo 2 = 9
Main: resultado del hilo 3 = 16
Main: resultado del hilo 4 = 25
Todos los hilos terminaron

El programa crea 5 hilos, cada uno calcula el cuadrado de un número. El main espera a todos con pthread_join y recoge los resultados. Los resultados se asignan con malloc para que sobrevivan a la terminación del hilo.

12. Resumen

13. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. Qué hace pthread_create?

Crea un nuevo hilo que ejecuta una función con un argumento. Devuelve 0 Sí tiene exito.

2. Qué hace pthread_join?

Bloquea al hilo que la llama hasta que el hilo especificado termina, y recoge su valor de retorno.

3. Qué hace pthread_exit?

Termina el hilo actual con un valor de retorno. Es el equivalente de exit() para hilos.

4. Por que se debe usar -pthread al compilar?

Porque define _REENTRANT (hace que errno sea por hilo) y enlaza la biblioteca de hilos.

5. Cómo se ven los hilos con ps?

Con ps -L -p PID, Que muestra el TID de cada hilo.

6. Cómo se ven los hilos con top?

Con top -H, Que muestra los hilos como procesos individuales con su TID.

7. Donde se exponen los hilos en /proc?

En /proc/PID/task/TID, con los mismos archivos que un proceso (status, stat, stack, etc.).

8. Por que no se debe retornar un puntero a una variable local?

Porque la variable local esta en la pila del hilo, Que se libera cuándo el hilo termina. El puntero queda inválido.

9. Qué pasa sí no se llama a pthread_join ni pthread_detach?

El hilo terminado queda como "zombi" y sus recursos no se liberan. Es el equivalente de los procesos zombis para hilos.

10. Qué es pthread_self()?

Devuelve el pthread_t del hilo que la llama. Se usa para identificar al hilo actual.

Para prácticar

  1. Compila y ejecuta hilo_Simple.c. Observa la creacion y espera del hilo.
  2. Compila y ejecuta pthread_exit_demo.c. Verifica que el resultado se pasa correctamente.
  3. Compila y ejecuta crear_esperar.c. Observa los resultados de cada hilo.
  4. Mientras se ejecuta un programa con hilos, observa con ps -L y top -H.
  5. Lee /proc/PID/task/ para ver los directorios de los hilos.
  6. Escribe un programa que cree 10 hilos y los espere en orden inverso de creacion.