← Concurrencia y Sincronización Bloque 3 · Operaciones atómicas · Tema 13 de 47

Tema 13

Spinlocks y espera activa

Un spinlock es el candado más simple que existe: un hilo que quiere entrar a la sección crítica gira en un bucle (spin) hasta que el candado se libere. No duerme, no cede la CPU, solo espera. En este tema verás cómo construir un spinlock con una instrucción atómica, cuándo conviene usarlo (y cuándo no), cómo se comporta bajo contención y cómo se mide su costo en un sistema real.

1. Qué es un spinlock

Un spinlock es un candado que, cuando está ocupado, hace que el hilo que quiere adquirirlo gire en un bucle (spin) esperando a que se libere. El hilo no se bloquea ni cede la CPU: consume CPU activamente mientras espera.

El spinlock es el mecanismo de sincronización más simple y el más rápido cuando no hay contención (cuando el lock está libre, se adquiere en una sola instrucción atómica). Pero es el peor cuando hay contención (cuando el lock está ocupado, el hilo desperdicia CPU girando).

El spinlock se construye sobre una instrucción atómica (test-and-set o compare-and-swap). Es la base sobre la que se construyen mutexes y otros mecanismos de sincronización.

Spinlock = candado giratorio

El nombre viene de la espera activa: el hilo «gira» (spin) en un bucle, comprobando repetidamente si el lock se ha liberado. Es como un niño que pregunta «¿ya llegamos?» cada segundo en un viaje largo.

2. Espera activa (busy-waiting)

La espera activa (busy-waiting) es una técnica en la que un hilo consume CPU activamente mientras espera a que se cumpla una condición. En lugar de dormirse y ceder la CPU, el hilo gira en un bucle comprobando la condición.

La espera activa tiene estas características:

La espera activa es útil cuando la espera es muy corta (microsegundos). Si la espera es larga (milisegundos o más), es mejor bloquear el hilo y ceder la CPU.

3. Construir un spinlock con TAS

El spinlock más simple se construye con test-and-set:

spinlock_tas.c
typedef struct {
    volatile int lock;
} spinlock_t;

void spinlock_init(spinlock_t *s) {
    s->lock = 0;  /* 0 = libre, 1 = ocupado */
}

void spinlock_lock(spinlock_t *s) {
    while (test_and_set(&s->lock)) {
        /* espera activa: el lock está ocupado */
    }
}

void spinlock_unlock(spinlock_t *s) {
    s->lock = 0;
}

El spinlock cumple la exclusión mutua: test_and_set es atómico, así que solo un hilo puede adquirir el lock a la vez. El progreso y la espera acotada dependen de la implementación: con test-and-set puro, no hay garantía de espera acotada (puede haber inanición).

El problema del spinlock con TAS es que todos los hilos que esperan ejecutan test_and_set continuamente, lo que genera tráfico en el bus de memoria (cada test-and-set es una operación de escritura atómica que invalida la caché de otros procesadores).

4. Construir un spinlock con CAS

Un spinlock con CAS es más eficiente porque puede usar compare_exchange_weak en un bucle, y la espera se hace leyendo (no escribiendo):

spinlock_cas.c
typedef struct {
    volatile int lock;
} spinlock_t;

void spinlock_lock(spinlock_t *s) {
    int expected = 0;
    while (!atomic_compare_exchange_weak(&s->lock, &expected, 1)) {
        expected = 0;  /* reintentar */
        /* espera activa: leer sin escribir (menos tráfico) */
    }
}

void spinlock_unlock(spinlock_t *s) {
    atomic_store(&s->lock, 0);
}

La ventaja del spinlock con CAS es que la espera se hace leyendo el valor (sin escribir), lo que genera menos tráfico en el bus de memoria. En procesadores modernos, una lectura en caché es muy barata, mientras que una escritura atómica invalida la caché de otros procesadores.

5. Versiones mejoradas

El spinlock básico tiene un problema: los hilos que esperan generan tráfico en el bus de memoria. Hay varias versiones mejoradas:

VersiónCómo funcionaVentajaDesventaja
TAS básicoGira ejecutando test_and_set continuamente.Simple.Mucho tráfico en el bus.
CAS con lecturaGira leyendo (sin escribir) hasta que el lock se libera, luego intenta CAS.Menos tráfico en el bus.Sigue consumiendo CPU.
Backoff exponencialDespués de cada fallo, espera un tiempo que se duplica (1, 2, 4, 8, ... ciclos).Reduce el tráfico en el bus.Más complejo.
Ticket lockCada hilo toma un número y espera a que sea su turno. FIFO justo.Espera acotada garantizada.Más complejo, más memoria.

El ticket lock es la versión más justa: cada hilo toma un número (como en una carnicería) y espera a que su número sea el que se está atendiendo. Esto garantiza la espera acotada: ningún hilo espera indefinidamente.

ticket lock
typedef struct {
    volatile int ticket;    /* próximo número a dar */
    volatile int turn;      /* número que se está atendiendo */
} ticket_lock_t;

void ticket_lock(ticket_lock_t *s) {
    int my_ticket = atomic_fetch_add(&s->ticket, 1);
    while (s->turn != my_ticket) {
        /* espera activa */
    }
}

void ticket_unlock(ticket_lock_t *s) {
    s->turn++;
}

6. Cuándo conviene usar un spinlock

Un spinlock conviene cuando:

La regla práctica es: si la sección crítica dura menos que el tiempo de un cambio de contexto (1-10 μs), usa un spinlock. Si dura más, usa un mutex.

7. Cuándo NO conviene

Un spinlock NO conviene cuando:

Spinlock en un solo núcleo = deadlock

Si un hilo adquiere un spinlock y otro hilo en el mismo núcleo intenta adquirirlo, el segundo gira indefinidamente (espera activa) y nunca cede la CPU al primero. El primer hilo nunca puede liberar el lock. Esto es un deadlock. Por eso, los spinlocks solo se usan en sistemas multi-núcleo o en el núcleo (donde el planificador no puede preemptar).

8. Contención y rendimiento

La contención es el grado en que los hilos compiten por el mismo lock. Con alta contención, el rendimiento del spinlock se degrada:

ContenciónRendimiento del spinlockAlternativa
Nula (1 hilo)Excelente (1 instrucción atómica).—
Baja (2-4 hilos)Bueno (poca espera).Spinlock o mutex.
Media (5-10 hilos)Regular (mucha espera, tráfico en el bus).Mutex (bloquear es mejor).
Alta (10+ hilos)Pésimo (todos giran, nadie progresa).Mutex o rediseño (reducir contención).

La solución a la alta contención no es un spinlock mejor, sino reducir la contención: usar más locks (uno por dato), usar estructuras lock-free, o rediseñar el programa para que los hilos no compitan.

9. Medir el costo

El costo de un spinlock se mide en ciclos de CPU. Con perf se puede medir el número de cambios de contexto y el tiempo de espera:

terminal · medir spinlock
# Compilar con optimización
$ gcc -std=c17 -Wall -Wextra -O2 -pthread -o spin_test spin_test.c

# Medir con perf
$ perf stat -e cycles,instructions,context-switches ./spin_test

 Performance counter stats for './spin_test':

     1,234,567,890      cycles
       987,654,321      instructions
               123      context-switches

       0.345678901 seconds time elapsed

El número de cambios de contexto (context-switches) indica cuántas veces el planificador cambió de hilo. Con un spinlock, el número de cambios de contexto debería ser bajo (porque los hilos no se bloquean). Con un mutex, el número es mayor (porque los hilos se bloquean y desbloquean).

10. Spinlocks en Linux

El núcleo de Linux usa spinlocks extensivamente para proteger secciones críticas cortas. Los spinlocks del núcleo tienen características especiales:

Los spinlocks del núcleo son la herramienta principal para proteger secciones críticas cortas (microsegundos). Para secciones largas, se usan mutexes o semáforos.

11. Simulador interactivo

Este simulador muestra el comportamiento de un spinlock bajo contención. Varios hilos compiten por un lock y el simulador muestra cuántas veces cada uno adquiere el lock:

Simulador · spinlock bajo contención

HiloAdquisicionesTiempo de espera totalTiempo de espera promedio

12. Resumen

13. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿Qué es un spinlock?

Un candado que espera activamente (gira en un bucle) hasta que se libere.

2. ¿Qué es la espera activa (busy-waiting)?

Consumir CPU activamente mientras se espera a que se cumpla una condición.

3. ¿Sobre qué instrucción atómica se construye un spinlock?

Test-and-set o compare-and-swap.

4. ¿Cuándo conviene usar un spinlock?

Cuando la sección crítica es muy corta y hay poca contención.

5. ¿Cuándo NO conviene usar un spinlock?

Cuando la sección crítica es larga, hay alta contención o el sistema tiene un solo núcleo.

6. ¿Por qué el spinlock puede causar deadlock en un solo núcleo?

Porque el hilo que gira nunca cede la CPU al hilo que tiene el lock.

7. ¿Qué es un ticket lock?

Un spinlock que asigna números (como en una carnicería) y garantiza la espera acotada (FIFO).

8. ¿Qué ventaja tiene el spinlock con CAS sobre el de TAS?

La espera se hace leyendo (sin escribir), lo que genera menos tráfico en el bus.

9. ¿Qué técnica reduce el tráfico en el bus bajo contención?

El backoff exponencial: esperar un tiempo que se duplica después de cada fallo.

10. ¿Qué spinlocks usa Linux en el núcleo?

Ticket locks (no TAS básico) para garantizar la espera acotada.

Para practicar

  1. Implementa un spinlock con TAS y verifica la exclusión mutua.
  2. Implementa un spinlock con CAS y compara el rendimiento.
  3. Implementa un ticket lock y verifica la espera acotada.
  4. Mide el costo de un spinlock con perf (ciclos, cambios de contexto).
  5. Experimenta con alta contención (10+ hilos) y observa la degradación.
  6. Investiga cómo Linux implementa los spinlocks (ticket lock, preempción deshabilitada).