1. Qué es un hilo
Un hilo (thread) es una linea de ejecución dentro de un proceso. Un proceso puede tener uno o más hilos, Que comparten el mismo espacio de direcciónes (Código, datos, heap) pero cada uno tiene su propia pila y su propio estado de la CPU (registros, contador de programa).
Los hilos son la forma de dividir un proceso en multiples táreas que se ejecutan concurrentemente. A diferencia de los procesos (Que se crean con fork y tienen memoria propia), los hilos comparten memoria y se crean con pthread_create (o clone en linux).
La analogia clasica es la de un restaurante: el proceso es el restaurante (con su cocina, ingredientes y recetas), y los hilos son los cocineros que trabajan en la misma cocina. Todos comparten los mismos ingredientes (Memoria) y las mismás recetas (Código), pero cada uno tiene su propia estacion de trabajo (pila) y su propia tárea (contador de programa).
Un hilo es simplemente una secuencia de instrucciones que se ejecuta. Un proceso con un sólo hilo es como los programás que hemos visto hasta ahora: una sola linea de ejecución. Un proceso con varios hilos tiene varias lineas de ejecución que avanzan concurrentemente.
2. Proceso vs hilo
La diferencia fundamental entre un proceso y un hilo es la Memoria:
| Caracteristica | Proceso | Hilo |
|---|---|---|
| Espacio de direcciónes | Propio (independiente) | Compartido con otros hilos del mismo proceso |
| Memoria (Código, datos, heap) | Propia | Compartida |
| Pila | Propia | Propia (cada hilo tiene la suya) |
| Registros y PC | Propios | Propios (cada hilo tiene los suyos) |
| PID | Unico | Compartido (todos los hilos del proceso tienen el mismo PID) |
| TID | N/A | Unico por hilo |
| Creacion | fork() (costosa) | pthread_create() (barata) |
| Cambio de contexto | costoso (cambia tablas de páginas) | Barato (no cambia tablas de páginas) |
| Comunicacion | IPC (pipes, sockets, etc.) | Memoria compartida (directa) |
| Aislamiento | Alto (un proceso no afecta a otros) | Bajo (un hilo puede corromper la memoria de todos) |
Los hilos son más ligeros que los procesos: se crean más rápido, se cambian más rápido, y se comunican más fácilmente (porque comparten memoria). Pero ofrecen menos aislamiento: un error en un hilo puede corromper la memoria de todos los hilos del proceso.
3. Lo que comparten los hilos
Todos los hilos de un proceso comparten:
- Codigo (texto): todos ejecutan el mismo código del programa.
- Datos globales y estaticos: todos acceden a las mismás variables globales.
- Heap (monticulo): todos comparten la memoria dinaminima (malloc/free).
- Descriptores de archivo: todos comparten la tabla de archivos abiertos.
- Directorio de trabajo: todos comparten el cwd.
- Variables de entorno: todos comparten el entorno.
- Senales y manejadores: todos comparten los manejadores de senales (aunque cada hilo tiene su propia máscara de senales bloqueadas).
- PID: todos tienen el mismo PID (el del proceso).
Esta comparticion es lo que hace a los hilos poderosos: pueden comunicarse simplemente leyendo y escribiendo variables compartidas, Sin necesidad de IPC (pipes, sockets, etc.).
4. Lo que cada hilo tiene propio
Cada hilo tiene su propio:
- Pila (stack): cada hilo tiene su propia pila para variables locales y llamadas a funciónes. Esto es esencial: Sí compartieran la pila, las llamadas a funciónes se mezclarian.
- Registros de la CPU: cada hilo tiene sus propios valores de registros (incluyendo el contador de programa y el puntero de pila).
- Estado de la CPU: cada hilo tiene su propio estado (listo, en ejecución, bloqueado).
- TID (Thread ID): cada hilo tiene un identificador único dentro del proceso.
- errno: cada hilo tiene su propia copia de errno (en POSIX). Esto es importante porque errno es una variable global que se usa para reportar errores.
- Mascara de senales bloqueadas: cada hilo puede bloquear senales independientemente.
- Prioridad y planificacion: cada hilo puede tener su propia prioridad.
La pila es el recurso más importante que cada hilo tiene propio. Sin pilas separadas, los hilos no podrian ejecutar funciónes de forma independiente.
5. El modelo de hilos
Los hilos se pueden implementar de dos formas:
- Hilos de usuario (user-level threads): gestionados por una biblioteca en espacio de usuario. El núcleo no sabe que existen. La biblioteca programa los hilos entre sí.
- Hilos de núcleo (kernel-level threads): gestionados por el núcleo. El núcleo programa los hilos como sí fueran procesos ligeros.
Linux usa hilos de núcleo: cada hilo es un proceso ligero (LWP, lightweight process) gestionado por el núcleo. Esto significa que el núcleo programa los hilos directamente, y pueden ejecutarse en paralelo en multiples núcleos.
Los modelos de mapeo entre hilos de usuario y de núcleo son:
| Modelo | Descripcion | Usado por |
|---|---|---|
| 1:1 | Cada hilo de usuario corresponde a un hilo de núcleo. | Linux (pthreads), Windows, macOS. |
| N:1 | Multiples hilos de usuario mapean a un sólo hilo de núcleo. | Sistemás antiguos (Solaris green threads). |
| M:N | Multiples hilos de usuario mapean a un número menor de hilos de núcleo. | Solaris (antiguo), algunos sistemás de tiempo real. |
Linux usa el modelo 1:1: cada hilo de usuario (pthread) corresponde a un hilo de núcleo (LWP). Esto permite que los hilos se ejecuten en paralelo en multiples núcleos y que el núcleo programe cada hilo independientemente.
El tema 29 profundiza en los modelos de hilos y en como linux los implementa con clone.
6. Hilos en el espacio de direcciónes
Todos los hilos de un proceso comparten el mismo espacio de direcciónes. Esto significa que la misma dirección virtual apunta a la misma página fica para todos los hilos. No hay traducción diferente por hilo.
Las pilas de los hilos se colocan en regiones distintas del espacio de direcciónes, tipicamente en la region mapeada (entre el heap y la pila principal). Cada hilo tiene su propia region de pila, de tamano fijo (por omision, 8 MB en linux).
En /proc/PID/maps, se pueden ver las pilas de los hilos:
$ grep stack /proc/PID/maps
7f8a3c000000-7f8a3c021000 rw-p ... [stack:1001]
7f8a3b800000-7f8a3b821000 rw-p ... [stack:1002]
7ffffffde000-7ffffffff000 rw-p ... [stack]
La etiqueta [stack:1001] Indica la pila del hilo con TID 1001. La etiqueta [stack] (Sin número) es la pila del hilo principal (el proceso).
7. El TCB: Thread control block
ASí como un proceso tiene un PCB (Process control block), un hilo tiene un TCB (Thread control block). El TCB guarda la información especÃfica del hilo:
- TID: el identificador del hilo.
- Estado: listo, en ejecución, bloqueado.
- Registros de la CPU: el estado de los registros, incluyendo el contador de programa y el puntero de pila.
- Puntero a la pila: la dirección de la pila del hilo.
- Mascara de senales bloqueadas: las senales que este hilo ha bloqueado.
- errno: la copia privada de errno para este hilo.
- Puntero al PCB del proceso: para acceder a los recursos compartidos.
En linux, el TCB no es una estructura separada: se integra en el task_struct del hilo. El task_struct tiene campos especÃficos para hilos (Como la pila, los registros, la máscara de senales) y campos compartidos con el proceso (Como la memoria, los archivos, etc.).
La biblioteca pthreads (NPTL en linux) mantiene su propia estructura de TCB en espacio de usuario, Que complementa al task_struct del núcleo. Esta estructura guarda información como el estado del hilo (detached, joined), los atributos, y los datos especÃficos del hilo.
8. Hilos en linux: clone
En linux, los hilos se crean con la syscall clone(), Que es una version generalizada de fork. clone permite especificar que recursos se comparten entre el proceso padre y el hijo:
| Flag de clone | Recurso compartido |
|---|---|
| CLONE_VM | Espacio de direcciónes (Memoria). |
| CLONE_FS | Informacion del sistema de archivos (cwd, root, umask). |
| CLONE_FILES | Tabla de descriptores de archivo. |
| CLONE_SIGHAND | Manejadores de senales. |
| CLONE_THREAD | Mismo grupo de hilos (mismo PID/TGID). |
| CLONE_PARENT | Mismo padre. |
| CLONE_VFORK | El padre se suspende hasta que el hijo llame a exec o _exit (Como vfork). |
fork() es equivalente a clone() Sin flags (CLONE_SIGCHLD). Un hilo (pthread) se crea con clone() con todos los flags CLONE_* activados: comparte memoria, archivos, senales, etc., pero tiene su propia pila y su propio TID.
Por eso, en linux, la diferencia entre un proceso y un hilo es sólo que flags se pasan a clone(). Un "proceso" es un clone sin CLONE_VM (Memoria separada). Un "hilo" es un clone con CLONE_VM (Memoria compartida).
En linux, no hay una diferencia fundamental entre procesos e hilos. Ambos son task_struct. La diferencia esta en los flags de clone: Sí se comparte la memoria (CLONE_VM), es un hilo; Sí no, es un proceso. Esto es una de las decisiones de diseno más elegantes de linux.
9. Hilos en otros sistemas
En windows, los hilos son ciudadanos de primera clase: el sistema operativo los gestiona directamente con la API Createthread. Cada hilo tiene su propio TEB (Thread environment block) y su propia pila. Los procesos y los hilos son entidades distintas.
En macOS y otros sistemás unix, los hilos se implementan con pthreads, Que en macOS se basa en el mach kernel. Cada hilo es un "Mach thread" gestionado por el núcleo.
La diferencia principal entre linux y windows es que linux usa clone() para crear tanto procesos como hilos (la diferencia esta en los flags), mientras que windows tiene APIs separadas (Createprocess y createthread).
10. Ventajas de los hilos
- Creacion rápida: crear un hilo es mucho más rápido que crear un proceso (no se copia la memoria).
- Cambio de contexto barato: cambiar entre hilos del mismo proceso no requiere cambiar las tablas de páginas.
- Comunicacion fácil: los hilos comparten memoria, por lo que pueden comunicarse directamente (Sin IPC).
- Paralelismo: los hilos pueden ejecutarse en paralelo en multiples núcleos.
- Responsividad: un hilo puede manejar la interfaz de usuario mientras otro hace trabajo pesado.
11. Desventajas de los hilos
- Sin aislamiento: un error en un hilo puede corromper la memoria de todos los hilos.
- Condiciones de carrera: el acceso concurrente a datos compartidos puede causar resultados incorrectos.
- Depuracion difícil: los bugs de concurrencia son difíciles de reproducir y depurar.
- Sincronizacion: se necesitan mutex, semaforos, etc. para coordinar el acceso a datos compartidos.
- complejidad: el código con hilos es más complejo que el código secuencial.
Los hilos son poderosos pero peligrosos. Compartir memoria fácilita la comunicacion, pero también fácilita la corrupcion de datos. Siempre se debe sincronizar el acceso a datos compartidos con mutex u otras herramientas de sincronizacion.
12. Un programa que demuestra hilos
El siguiente programa crea 3 hilos que comparten una variable global y la incrementan:
#define _POSIX_C_SOURCE 200809L
#include
#include
#include
#include
/* Variable compartida por todos los hilos */
static int contador = 0;
void *trabajo(void *arg)
{
int id = *(int *) arg;
for (int i = 0; i < 5; i++) {
contador++; /* acceso concurrente (Sin sincronizacion) */
printf("Hilo %d: contador = %d\n", id, contador);
usleep(100000);
}
return NULL;
}
int main(void)
{
pthread_t hilos[3];
int ids[3] = {1, 2, 3};
printf("PID: %d (todos los hilos comparten este PID)\n", (int) getpid());
for (int i = 0; i < 3; i++)
pthread_create(&hilos[i], NULL, trabajo, &ids[i]);
for (int i = 0; i < 3; i++)
pthread_join(hilos[i], NULL);
printf("Contador final: %d (esperado: 15)\n", contador);
return 0;
}
$ gcc -std=c17 -Wall -Wextra -pthread -o hilos_demo hilos_demo.c
$ ./hilos_demo
PID: 1090 (todos los hilos comparten este PID)
Hilo 1: contador = 1
Hilo 2: contador = 2
Hilo 3: contador = 3
Hilo 1: contador = 4
...
Contador final: 15 (esperado: 15)
Los 3 hilos comparten el mismo PID y la misma variable contador. Cada hilo incrementa el contador y muestra su valor. El contador final deberia ser 15 (3 hilos x 5 incrementos), pero puede ser menos sí hay una condicion de carrera (tema 34).
Observa el flag -pthread en la compilacion: es necesario para enlazar la biblioteca de hilos.
Tambien se puede verificar que los hilos comparten el mismo PID con ps:
$ ./hilos_demo &
[1] 1090
$ ps -L -p 1090 -o pid,tid,comm
PID TID COMMAND
1090 1090 hilos_demo
1090 1091 hilos_demo
1090 1092 hilos_demo
1090 1093 hilos_demo
Los 4 hilos (1 principal + 3 creados) tienen el mismo PID (1090) pero TIDs distintos (1090-1093). Esto confirma que los hilos comparten el PID del proceso.
13. Resumen
- Un hilo es una linea de ejecución dentro de un proceso.
- Los hilos comparten: Código, datos, heap, descriptores, cwd, entorno, PID.
- Cada hilo tiene su propia: pila, registros, PC, TID, errno, Máscara de senales.
- Los hilos son más ligeros que los procesos: creacion rápida, cambio de contexto barato.
- Linux usa clone() con CLONE_VM para crear hilos (modelo 1:1).
- Los hilos comparten memoria, lo que fácilita la comunicacion pero requiere sincronizacion.
- Un error en un hilo puede corromper la memoria de todos los hilos.
- Se compila con -pthread y se enlaza con libpthread.
14. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. Qué es un hilo?
Una linea de ejecución dentro de un proceso. Comparte el espacio de direcciónes con otros hilos del mismo proceso, pero tiene su propia pila y registros.
2. Qué comparten los hilos de un proceso?
Codigo, datos globales, heap, descriptores de archivo, cwd, entorno, manejadores de senales, y PID.
3. Qué tiene propio cada hilo?
Pila, registros de la CPU, contador de programa, TID, errno, Máscara de senales bloqueadas, y prioridad.
4. Por que los hilos son más rápidos que los procesos?
Porque se crean sin copiar la memoria (comparten el espacio de direcciónes), y el cambio de contexto no requiere cambiar las tablas de páginas.
5. Qué modelo de hilos usa linux?
El modelo 1:1: cada hilo de usuario (pthread) corresponde a un hilo de núcleo (LWP). Esto permite paralelismo real en multiples núcleos.
6. Cómo se crean hilos en linux?
Con la syscall clone() con los flags CLONE_VM, CLONE_FS, CLONE_FILES, CLONE_SIGHAND, CLONE_THREAD, etc. La biblioteca pthreads usa clone internamente.
7. Qué es un TCB?
Thread control block: la estructura que guarda la información de un hilo (TID, estado, registros, pila, Máscara de senales, errno).
8. Por que los hilos necesitan sincronizacion?
Porque comparten memoria. El acceso concurrente a datos compartidos puede causar condiciones de carrera (resultados incorrectos).
9. Cómo se compila un programa con pthreads?
Con el flag -pthread: gcc -std=c17 -Wall -Wextra -pthread programa.c -o programa.
10. Qué diferencia hay entre un hilo y un proceso en linux?
La diferencia esta en los flags de clone(): Sí se comparte la memoria (CLONE_VM), es un hilo; Sí no, es un proceso. Ambos son task_struct.
Para prácticar
- Compila y ejecuta hilos_demo.c. Observa que todos los hilos tienen el mismo PID.
- Mientras se ejecuta, observa los hilos con ps -L -p PID.
- Lee /proc/PID/maps y busca las pilas de los hilos ([stack:TID]).
- Ejecuta hilos_demo varias veces. ¿El contador final siempre es 15? ¿Por que?
- Escribe un programa que cree 10 hilos y muestre sus TIDs.
- Compara el tiempo de creacion de un proceso (fork) con el de un hilo (pthread_create).