1. Qué es una estructura sin bloqueos
Una estructura sin bloqueos (lock-free data structure) es una estructura de datos que se puede acceder concurrentemente por múltiples hilos sin usar mutex ni otros mecanismos de bloqueo. Usa operaciones atómicas (especialmente CAS) para garantizar la consistencia.
Las estructuras lock-free son no bloqueantes: un hilo que accede a la estructura nunca se bloquea. Si otro hilo está modificando la estructura, el hilo reintenta la operación hasta que tenga éxito.
Las propiedades de las estructuras lock-free son:
- Lock-free: al menos un hilo progresa en un número finito de pasos.
- Wait-free: todos los hilos progresan en un número finito de pasos (más fuerte que lock-free).
- Obstruction-free: un hilo progresa si ejecuta solo (más débil que lock-free).
2. Ventajas y desventajas
| Ventaja | Desventaja |
|---|---|
| Sin deadlock: no hay candados. | Complejidad: difíciles de diseñar y verificar. |
| Sin inversión de prioridades. | Problema ABA: puede causar corrupción. |
| Progreso garantizado (lock-free). | Rendimiento bajo alta contención (reintentos). |
| Mejor rendimiento bajo baja contención. | No siempre más rápido que un mutex. |
3. Contador lock-free
El contador lock-free más simple usa atomic_fetch_add:
#include <stdatomic.h>
atomic_int counter = 0;
void incrementar(void) {
atomic_fetch_add(&counter, 1); /* atómico, sin lock */
}
int leer(void) {
return atomic_load(&counter);
}
Este contador es lock-free porque usa una operación atómica. No hay mutex, no hay bloqueo, no hay deadlock.
4. Pila lock-free
Una pila lock-free se implementa con CAS. El tope de la pila se actualiza atómicamente:
typedef struct nodo {
int valor;
struct nodo *siguiente;
} nodo_t;
typedef struct {
_Atomic(nodo_t *) tope;
} pila_t;
void pila_push(pila_t *p, int valor) {
nodo_t *nuevo = malloc(sizeof(nodo_t));
nuevo->valor = valor;
nodo_t *viejo;
do {
viejo = atomic_load(&p->tope);
nuevo->siguiente = viejo;
} while (!atomic_compare_exchange_weak(&p->tope, &viejo, nuevo));
}
int pila_pop(pila_t *p) {
nodo_t *viejo;
nodo_t *nuevo;
do {
viejo = atomic_load(&p->tope);
if (viejo == NULL) return -1; /* pila vacía */
nuevo = viejo->siguiente;
} while (!atomic_compare_exchange_weak(&p->tope, &viejo, nuevo));
int valor = viejo->valor;
/* Liberar memoria (con cuidado: problema ABA). */
return valor;
}
La pila usa CAS para actualizar el tope de forma atómica. Si otro hilo modifica el tope entre la lectura y el CAS, el CAS falla y se reintenta.
5. El problema ABA
El problema ABA es el principal problema de las estructuras lock-free. Ocurre cuando un valor cambia de A a B y vuelve a A, engañando a un CAS:
- La pila tiene [A, B, C]. El tope es A. inicial
- Hilo 1 lee el tope (A) y el siguiente (B). Va a hacer CAS para cambiar el tope de A a B. leer
- Hilo 2 saca A y B de la pila. La pila queda [C]. sacar
- Hilo 2 mete A de nuevo. La pila queda [A, C]. meter
- Hilo 1 hace CAS: compara tope con A, es igual, cambia a B. Pero B ya no está en la pila. La pila queda corrupta. corrupción
El problema ABA es sutil y peligroso: el CAS tiene éxito porque el valor es el esperado, pero el estado intermedio se perdió.
El problema ABA no es teórico: ha causado bugs en sistemas reales. Cualquier estructura lock-free que use CAS sobre punteros es vulnerable al problema ABA.
6. Soluciones al ABA
| Solución | Cómo funciona | Ventaja | Desventaja |
|---|---|---|---|
| Contador de versión | Añadir un contador que se incrementa en cada operación. CAS compara valor + contador. | Simple, efectiva. | Espacio adicional. |
| Hazard pointers | Los hilos registran los punteros que usan. Un nodo no se libera hasta que nadie lo use. | Segura, sin ABA. | Compleja. |
| RCU | Read-Copy-Update: los lectores no se bloquean, los escritores crean copias. | Muy eficiente para lectura. | Compleja, solo Linux. |
| GC | Garbage collection: la memoria no se libera manualmente. | Simple (en lenguajes con GC). | No disponible en C. |
La solución más común en C es el contador de versión: se empaqueta el puntero y un contador en una palabra de 64 bits (usando los bits altos para el contador) y se usa CAS sobre la palabra completa.
7. Cuándo no vale la pena
Las estructuras lock-free no siempre son mejores que los mutexes:
- Baja contención: si la contención es baja, un mutex es más rápido (fast path sin entrar al núcleo).
- Alta contención: los reintentos de CAS pueden ser costosos.
- Complejidad: el código lock-free es difícil de escribir y verificar.
- ABA: el problema ABA añade complejidad adicional.
La regla práctica es: usar mutex por defecto, y lock-free solo cuando el rendimiento lo justifique y se pueda verificar correctamente.
Un mutex sin contención se adquiere en ~20 ns. Una operación CAS tarda ~10-50 ns. Bajo baja contención, el mutex es más rápido. Lock-free solo es mejor bajo alta contención o cuando se necesita progreso garantizado.
8. Estructuras lock-free en Linux
Linux usa estructuras lock-free extensivamente:
- Listas lock-free: el núcleo usa listas enlazadas lock-free para la cola de ejecución.
- Contadores atómicos:
atomic_tse usa para contadores de referencia. - RCU: Read-Copy-Update es la técnica lock-free más usada en Linux.
- Hazard pointers: se usan en algunos subsistemas.
RCU es especialmente importante: permite que los lectores no se bloqueen y los escritores creen una copia. Es la técnica que hace que Linux sea escalable en sistemas con muchos núcleos.
9. Simulador interactivo
Este simulador muestra cómo funciona una pila lock-free con CAS:
Simulador · pila lock-free
10. Resumen
- Las estructuras lock-free usan operaciones atómicas (CAS) en lugar de mutex.
- Garantizan que al menos un hilo progresa (lock-free).
- El contador lock-free usa atomic_fetch_add.
- La pila lock-free usa CAS para actualizar el tope.
- El problema ABA ocurre cuando un valor cambia y vuelve al original.
- Las soluciones al ABA incluyen contador de versión, hazard pointers y RCU.
- Lock-free no siempre es más rápido que un mutex.
- Linux usa estructuras lock-free extensivamente (RCU, contadores atómicos).
11. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. ¿Qué es una estructura lock-free?
Una estructura de datos que se accede concurrentemente sin mutex, usando operaciones atómicas.
2. ¿Qué garantiza lock-free?
Que al menos un hilo progresa en un número finito de pasos.
3. ¿Qué es wait-free?
Que todos los hilos progresan en un número finito de pasos (más fuerte que lock-free).
4. ¿Cómo se implementa un contador lock-free?
Con atomic_fetch_add.
5. ¿Cómo se implementa una pila lock-free?
Con CAS para actualizar el tope.
6. ¿Qué es el problema ABA?
Un valor cambia de A a B y vuelve a A, engañando a un CAS.
7. ¿Cómo se resuelve el problema ABA?
Con contador de versión, hazard pointers, RCU o garbage collection.
8. ¿Lock-free siempre es más rápido que un mutex?
No. Bajo baja contención, un mutex es más rápido (fast path).
9. ¿Qué técnica lock-free usa Linux más?
RCU (Read-Copy-Update).
10. ¿Cuándo no vale la pena usar lock-free?
Cuando la contención es baja o la complejidad no se justifica.
Para practicar
- Implementa un contador lock-free con atomic_fetch_add.
- Implementa una pila lock-free con CAS.
- Diseña un escenario donde ocurra el problema ABA.
- Implementa la solución con contador de versión.
- Compara el rendimiento de una pila lock-free con una protegida por mutex.
- Investiga cómo se implementa RCU en el núcleo de Linux.