← Concurrencia y Sincronización Bloque 9 · Más allá de los candados · Tema 43 de 47

Tema 43

Estructuras sin bloqueos

Las estructuras sin bloqueos (lock-free) usan operaciones atómicas (especialmente CAS) en lugar de mutex. Permiten que los hilos accedan a datos compartidos sin bloquearse, lo que evita deadlocks e inversión de prioridades. En este tema verás cómo implementar pilas y contadores lock-free, el problema ABA y cuándo no vale la pena usarlas.

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:

2. Ventajas y desventajas

VentajaDesventaja
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:

contador lock-free
#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:

pila lock-free
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:

  1. La pila tiene [A, B, C]. El tope es A. inicial
  2. Hilo 1 lee el tope (A) y el siguiente (B). Va a hacer CAS para cambiar el tope de A a B. leer
  3. Hilo 2 saca A y B de la pila. La pila queda [C]. sacar
  4. Hilo 2 mete A de nuevo. La pila queda [A, C]. meter
  5. 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 es real

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ónCómo funcionaVentajaDesventaja
Contador de versiónAñadir un contador que se incrementa en cada operación. CAS compara valor + contador.Simple, efectiva.Espacio adicional.
Hazard pointersLos hilos registran los punteros que usan. Un nodo no se libera hasta que nadie lo use.Segura, sin ABA.Compleja.
RCURead-Copy-Update: los lectores no se bloquean, los escritores crean copias.Muy eficiente para lectura.Compleja, solo Linux.
GCGarbage 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:

La regla práctica es: usar mutex por defecto, y lock-free solo cuando el rendimiento lo justifique y se pueda verificar correctamente.

Lock-free no es siempre más rápido

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:

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

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

  1. Implementa un contador lock-free con atomic_fetch_add.
  2. Implementa una pila lock-free con CAS.
  3. Diseña un escenario donde ocurra el problema ABA.
  4. Implementa la solución con contador de versión.
  5. Compara el rendimiento de una pila lock-free con una protegida por mutex.
  6. Investiga cómo se implementa RCU en el núcleo de Linux.