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.
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:
- Consume CPU: el hilo usa el 100 % de un núcleo mientras espera.
- Respuesta rápida: cuando la condición se cumple, el hilo la detecta inmediatamente (en el siguiente ciclo del bucle).
- Sin cambios de contexto: no hay overhead de bloquear/desbloquear.
- Peligrosa en un solo núcleo: si el hilo que tiene el lock está en el mismo núcleo, el spinlock puede causar deadlock (el hilo que gira nunca cede la CPU al que tiene el lock).
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:
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):
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ón | Cómo funciona | Ventaja | Desventaja |
|---|---|---|---|
| TAS básico | Gira ejecutando test_and_set continuamente. | Simple. | Mucho tráfico en el bus. |
| CAS con lectura | Gira leyendo (sin escribir) hasta que el lock se libera, luego intenta CAS. | Menos tráfico en el bus. | Sigue consumiendo CPU. |
| Backoff exponencial | Despué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 lock | Cada 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.
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 sección crítica es muy corta: microsegundos o menos. El tiempo de espera es menor que el costo de bloquear/desbloquear un mutex.
- Hay poca contención: los hilos rara vez compiten por el lock.
- El sistema tiene varios núcleos: los hilos pueden girar en núcleos distintos sin bloquearse mutuamente.
- El código es del núcleo: en el núcleo de Linux, los spinlocks son la herramienta principal para proteger secciones críticas cortas.
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:
- La sección crítica es larga: milisegundos o más. El hilo desperdicia CPU girando.
- Hay alta contención: muchos hilos compiten por el lock, generando mucho tráfico en el bus.
- El sistema tiene un solo núcleo: si el hilo que tiene el lock está en el mismo núcleo que el que gira, el spinlock causa deadlock (el que gira nunca cede la CPU).
- El código es de usuario: en el espacio de usuario, los mutexes son mejores porque bloquean el hilo y ceden la CPU.
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ón | Rendimiento del spinlock | Alternativa |
|---|---|---|
| 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:
# 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:
- Preempción deshabilitada: mientras un hilo tiene un spinlock, no puede ser preemptado. Esto evita el deadlock en un solo núcleo.
- Interrupciones deshabilitadas: en algunos casos, las interrupciones se deshabilitan para evitar que un manejador de interrupción adquiera el mismo lock.
- Versiones:
spin_lock(),spin_lock_irqsave(),spin_lock_bh(), etc., según qué se debe deshabilitar. - Ticket lock: Linux usa ticket locks (no TAS básico) para garantizar la espera acotada.
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
| Hilo | Adquisiciones | Tiempo de espera total | Tiempo de espera promedio |
|---|
12. Resumen
- Un spinlock es un candado que espera activamente (gira en un bucle).
- Se construye con test-and-set o compare-and-swap.
- Conviene cuando la sección crítica es muy corta y hay poca contención.
- No conviene cuando la sección crítica es larga o hay alta contención.
- En un solo núcleo, el spinlock puede causar deadlock.
- El ticket lock garantiza la espera acotada (FIFO justo).
- Linux usa spinlocks (ticket locks) para proteger secciones críticas cortas en el núcleo.
- El costo se mide con perf (ciclos, cambios de contexto).
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
- Implementa un spinlock con TAS y verifica la exclusión mutua.
- Implementa un spinlock con CAS y compara el rendimiento.
- Implementa un ticket lock y verifica la espera acotada.
- Mide el costo de un spinlock con perf (ciclos, cambios de contexto).
- Experimenta con alta contención (10+ hilos) y observa la degradación.
- Investiga cómo Linux implementa los spinlocks (ticket lock, preempción deshabilitada).