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.
#include
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine)(void *), void *arg);
Los parámetros son:
thread:puntero dónde se guarda el identificador del hilo creado (pthread_t).attr:atributos del hilo (NULL para valores por omision).start_routine:la función que el hilo ejecutara.arg:el argumento que se pasa a la función.
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:
| Parametro | Tipo | Descripcion |
|---|---|---|
| thread | pthread_t * | Puntero dónde se guarda el ID del hilo. Se usa después para pthread_join, pthread_detach, etc. |
| attr | const pthread_attr_t * | Atributos del hilo (tamano de pila, estado detached, etc.). NULL para valores por omision. |
| start_routine | void *(*)(void *) | Puntero a la función que el hilo ejecutara. Debe tener la firma void *func(void *). |
| arg | void * | 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:
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).
#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.
#include
int pthread_join(pthread_t thread, void **retval);
Los parámetros son:
thread:el hilo a esperar (el valor devuelto por pthread_create).retval:puntero dónde se guarda el valor de retorno del hilo (puede ser NULL Sí no interesa).
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.
#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.
#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.
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:
- Define _REENTRANT: hace que las funciónes de la biblioteca de C sean thread-safe (por ejemplo, errno se convierte en una variable por hilo).
- Enlaza con libpthread: enlaza la biblioteca de hilos.
$ 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.
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.
$ ./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:
$ 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.
$ 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:
$ 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:
$ grep threads /proc/1100/status
Threads: 4
10. Errores comunes
Los errores más comunes al crear y esperar hilos son:
- No comprobar el valor de retorno de pthread_create: Sí falla, el hilo no se crea. Siempre verificar.
- No llamar a pthread_join ni pthread_detach: el hilo terminado queda como "zombi" y sus recursos no se liberan.
- Pasar un puntero a una variable local como argumento: Sí la variable sale de scope antes de que el hilo la use, el hilo accede a memoria invalida.
- No compilar con -pthread: el programa puede fallar en tiempo de ejecución.
- Retornar un puntero a una variable local de la función del hilo: el puntero queda inválido cuándo el hilo termina.
- No manejar la concurrencia: Sí los hilos acceden a datos compartidos sin sincronizacion, hay condiciones de carrera.
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:
#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;
}
$ 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
- pthread_create crea un hilo que ejecuta una función con un argumento.
- pthread_join espera a un hilo y recoge su valor de retorno.
- pthread_exit termina el hilo actual con un valor de retorno.
- Siempre se debe join o detach a cada hilo para liberar recursos.
- Se compila con -pthread.
- ps -L muestra los hilos de un proceso con su TID.
- top -H muestra los hilos como procesos individuales.
- /proc/PID/task/ contiene un directorio por hilo.
- No pasar punteros a variables locales como argumentos o retornos.
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
- Compila y ejecuta hilo_Simple.c. Observa la creacion y espera del hilo.
- Compila y ejecuta pthread_exit_demo.c. Verifica que el resultado se pasa correctamente.
- Compila y ejecuta crear_esperar.c. Observa los resultados de cada hilo.
- Mientras se ejecuta un programa con hilos, observa con ps -L y top -H.
- Lee /proc/PID/task/ para ver los directorios de los hilos.
- Escribe un programa que cree 10 hilos y los espere en orden inverso de creacion.