Fundamentos de la Concurrencia y Sincronización de Hilos en Sistemas Operativos
Enviado por Chuletator online y clasificado en Informática y Telecomunicaciones
Escrito el en
español con un tamaño de 6,46 KB
Conceptos Fundamentales de Hilos y Procesos
¿Qué es un Hilo (Thread)?
Un hilo representa un **flujo secuencial de instrucciones** dentro de un programa. Los hilos permiten **dividir el trabajo** de un solo programa para aprovechar el **hardware** que tiene **múltiples núcleos** (CPUs).
¿Por qué usar Hilos y no Procesos?
Los procesos no comparten memoria de forma nativa, mientras que los hilos trabajan sobre el mismo espacio de memoria, permitiendo **compartir datos** sin necesidad de copiar información entre ellos. La comunicación entre procesos requiere mecanismos **costosos** como *pipes* o segmentos de memoria compartida (*shared memory segments*).
Estructura de un Hilo
Aunque los hilos comparten la memoria principal, cada hilo posee su propio contexto de ejecución:
- Program Counter (PC): Le dice qué instrucción ejecutar a continuación. Si un hilo está en ejecución, la CPU sigue su PC.
- Conjunto de Registros de CPU.
- Pila propia (Stack): Guarda variables locales y direcciones de retorno de funciones.
Todos los hilos comparten la misma memoria (código, datos globales y *heap*).
Ventajas de la Programación con Hilos
1. Paralelismo
Capacidad de ejecutar muchas tareas de forma concurrente entre múltiples unidades de ejecución (hilos).
2. Evitar Bloqueo por E/S
Las operaciones de **Entrada/Salida (E/S)** son lentas. Con hilos, si un hilo está bloqueado esperando E/S, el **scheduler** (planificador) pausa ese hilo y ejecuta los que sí tengan trabajo pendiente, permitiendo que los otros sigan trabajando.
Creación y Sincronización de Hilos
Para crear hilos en entornos POSIX, se debe incluir la librería <pthread.h>.
El **scheduler** del sistema operativo da el orden de ejecución de los hilos (interleaving):
- El hilo principal (*main*) crea T1 y T2.
- El scheduler decide ejecutar 1ro T2 -> print B.
- Luego T1 -> print A.
- El hilo principal espera (*main wait*).
- Finalmente, el hilo principal imprime end.
La función pthread_join se utiliza para **sincronizar hilos**, haciendo que el hilo principal espere a que los hilos creados terminen su ejecución.
Problemas de Concurrencia: Race Condition
¿Por qué ocurre la pérdida de datos? (Data Share)
Cuando dos hilos acceden o modifican la misma **variable global** al mismo tiempo, se produce un problema.
El Problema: Race Condition
El resultado del programa depende del orden (**interleaving**) en que los hilos acceden o modifican datos compartidos y de cómo el **Sistema Operativo (OS)** decide qué hilo correr en cada instante.
Una operación simple como counter = counter + 1 se traduce a una secuencia de tres instrucciones de máquina separadas:
mov counter, %eax(Carga desde memoria a registro).add $1, %eax(Incrementa el registro).mov %eax, counter(Escribe el registro de vuelta en memoria).
Estas instrucciones pueden **interrumpirse** entre ellas. Por eso, dos hilos que ejecutan la misma secuencia pueden leer el mismo valor antiguo (*old*) y escribir el mismo valor nuevo (*new*), lo que resulta en la **pérdida de incrementos**.
Interleaving en una Sola CPU
No es necesario tener múltiples núcleos para que haya *interleaving*. El **scheduler** del *kernel* puede interrumpir un hilo en cualquier punto y dar ejecución a otro, **multiplexando** la CPU entre los hilos.
Herramienta de Diagnóstico: Usar objdump -d main para ver las instrucciones en ensamblador del programa y verificar que las operaciones sobre el contador no son **atómicas**.
The Wish for Atomicity
La **Atomicidad** es la propiedad que garantiza que una operación se ejecuta **completa** o no se ejecuta en absoluto, sin interrupción de otros hilos. Se busca tener una instrucción de un solo paso atómico (ejemplo conceptual: memory-add 0x313, $0x1).
Soluciones de Sincronización
La meta es garantizar la **Exclusión Mutua** y la **Sincronización Condicional**.
h3>Conceptos Clave de Sincronización
- Sección Crítica (*Critical Section*): Parte del código que accede a un recurso compartido y debe ser protegida.
- Exclusión Mutua (*Mutual Exclusion*): Evita que dos hilos entren a la vez a una sección crítica.
- Sincronización Condicional (*Condition Synchronization*): Hace que un hilo espere a que otro hilo complete una acción antes de continuar.
Mecanismos de Exclusión Mutua
Mutex (pthread_mutex_t)
Protege la sección crítica con una variable de tipo pthread_mutex_t. Garantiza la **exclusión mutua**. Sin embargo, realizar *lock/unlock* a cada incremento es **caro**, ya que implica un costo de bloqueo a nivel de *kernel*.
Spinlock
Implementa un ciclo de espera ocupada: while(!try_lock()) (prueba y gira). Es bueno cuando la sección crítica es **pequeña** y el tiempo de espera es muy reducido. Si el tiempo de espera es largo, **desperdicia ciclos de CPU**.
Operaciones Atómicas (OpAtomica)
Usar primitivas **atómicas** del compilador o la librería <stdatomic.h>. Esto evita el costo de bloqueo del *kernel* asociado a los *mutex*.
LockFree
Basado en **CAS** (*Compare and Swap*). Es complejo de implementar, pero permite una alta concurrencia sin bloqueo.
Construir Sincronización sobre Operaciones Básicas
Se puede usar un pequeño conjunto de instrucciones atómicas básicas (como *test-and-set*, **CAS**, *fetch and add*) para construir mecanismos de sincronización más generales (como *locks* y semáforos). Esto también es asistido por la CPU, que ayuda con el *scheduling* justo y el bloqueo/desbloqueo de hilos cuando esperan por recursos.
h4>Aclaración sobre Volatile
¿Volatile? -> No. La palabra clave volatile evita que el compilador guarde la variable en registros durante mucho tiempo (forzando la lectura desde memoria), pero **no proporciona exclusión mutua ni atomicidad**. No evita que dos hilos ejecuten la secuencia de tres instrucciones de forma intercalada.