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

Tema 42

El mutex por dentro: futex

Un mutex de pthreads parece una llamada al núcleo, pero en el caso común (sin contención) no entra al núcleo en absoluto. El truco es el futex (fast userspace mutex): una primitiva que hace el trabajo en espacio de usuario con una operación atómica y solo llama al núcleo cuando hay contención. En este tema verás cómo funciona por dentro, el fast path y el slow path, y por qué los mutexes son tan rápidos cuando no hay contención.

1. Qué es un futex

Un futex (fast userspace mutex) es una primitiva de sincronización del núcleo de Linux que permite bloquear y despertar hilos eficientemente. La idea es hacer el trabajo en espacio de usuario con operaciones atómicas y solo entrar al núcleo cuando hay contención.

El futex fue introducido en Linux 2.6 (2003) por Hubertus Franke. Es la base de los mutexes, semáforos y variables de condición de glibc.

El futex se basa en una variable entera compartida en espacio de usuario. El núcleo solo interviene cuando un hilo necesita bloquearse o despertar a otro.

Fast userspace mutex

El nombre dice todo: es un mutex que es rápido en espacio de usuario. En el caso común (sin contención), no entra al núcleo. Solo cuando hay contención (un hilo necesita bloquearse) se llama al núcleo.

2. El problema que resuelve

Antes de los futexes, los mutexes de POSIX se implementaban completamente en el núcleo: cada lock y unlock era una syscall. Esto era lento porque cada syscall implica un cambio de modo (usuario → núcleo → usuario) que consume ~1 μs.

El futex resuelve esto haciendo el caso común (sin contención) en espacio de usuario con operaciones atómicas. Solo el caso raro (con contención) entra al núcleo.

El resultado es que un mutex sin contención se adquiere en ~20 ns (una operación atómica), mientras que uno con contención tarda ~1-10 μs (una syscall).

3. El fast path (sin contención)

El fast path es el caso común: el mutex está libre. Se adquiere con una sola operación atómica (CAS) en espacio de usuario:

fast path
/* Pseudocódigo del fast path de pthread_mutex_lock. */
int lock = atomic_load(&mutex->lock);
if (lock == 0) {
    /* Mutex libre: intentar adquirir con CAS. */
    if (atomic_compare_exchange_strong(&mutex->lock, &lock, 1)) {
        return 0;  /* éxito, sin entrar al núcleo */
    }
}
/* Si falla, ir al slow path. */

En el fast path, no se llama al núcleo. La operación es una sola instrucción atómica (LOCK CMPXCHG en x86), que tarda ~20 ns.

El 99 % de las adquisiciones de mutex en programas reales son fast path (sin contención). Por eso, los mutexes son tan rápidos.

4. El slow path (con contención)

El slow path es el caso raro: el mutex está ocupado. El hilo debe bloquearse en el núcleo:

slow path
/* Pseudocódigo del slow path. */
/* El mutex está ocupado. Bloquear el hilo. */
futex_wait(&mutex->lock, 1);  /* esperar a que el valor cambie de 1 */
/* Al despertar, reintentar adquirir. */

El slow path llama a futex(FUTEX_WAIT), que bloquea el hilo en el núcleo. El hilo se duerme y cede la CPU. Cuando el mutex se libera, el núcleo despierta al hilo con futex(FUTEX_WAKE).

El slow path es más lento: implica dos syscalls (FUTEX_WAIT y FUTEX_WAKE) y dos cambios de contexto. Tarda ~1-10 μs.

5. El unlock

El unlock también tiene fast path y slow path:

unlock
/* Pseudocódigo del unlock. */
atomic_store(&mutex->lock, 0);  /* liberar el mutex */
/* Si hay hilos esperando, despertar uno. */
futex_wake(&mutex->lock, 1);

El unlock siempre establece el mutex a 0 con una operación atómica (fast path). Si hay hilos bloqueados, llama a futex(FUTEX_WAKE) para despertar a uno (slow path).

En el caso sin contención, futex_wake retorna inmediatamente (no hay hilos bloqueados). En el caso con contención, despierta al primer hilo bloqueado.

6. Implementación en glibc

La implementación de pthread_mutex en glibc usa futexes. El código (simplificado) es:

glibc mutex (simplificado)
/* Estructura del mutex. */
typedef struct {
    int lock;       /* 0 = libre, 1 = ocupado, 2 = ocupado con waiters */
} pthread_mutex_t;

int pthread_mutex_lock(pthread_mutex_t *mutex) {
    int c;
    /* Fast path: intentar adquirir con CAS. */
    c = atomic_load(&mutex->lock);
    if (c == 0 && atomic_compare_exchange_strong(&mutex->lock, &c, 1)) {
        return 0;  /* éxito */
    }
    /* Slow path: bloquear en el núcleo. */
    return __pthread_mutex_lock_slow(mutex);
}

int pthread_mutex_unlock(pthread_mutex_t *mutex) {
    /* Establecer a libre. */
    if (atomic_exchange(&mutex->lock, 0) == 1) {
        return 0;  /* no había waiters */
    }
    /* Hay waiters: despertar uno. */
    futex_wake(&mutex->lock, 1);
    return 0;
}

El fast path usa atomic_compare_exchange_strong (CAS). Si tiene éxito, retorna sin entrar al núcleo. Si falla, va al slow path.

El valor 2 en el lock indica que hay hilos esperando (waiters). Esto permite al unlock decidir si necesita llamar a futex_wake.

7. El costo del futex

EscenarioCostoNotas
Lock sin contención (fast path)~20 nsUna instrucción atómica (CAS).
Unlock sin contención (fast path)~20 nsUna instrucción atómica (store).
Lock con contención (slow path)~1-10 μsSyscall FUTEX_WAIT + cambio de contexto.
Unlock con contención (slow path)~1-10 μsSyscall FUTEX_WAKE + cambio de contexto.

El fast path es tan rápido como una operación atómica. El slow path es 50-500× más lento. Por eso, los mutexes son eficientes cuando no hay contención.

8. Cómo lo usa Linux

Linux usa futexes extensivamente:

El futex es la primitiva de sincronización fundamental de Linux. Todo lo demás se construye sobre él.

9. Simulador interactivo

Este simulador muestra el fast path y el slow path del mutex:

Simulador · futex (fast path vs. slow path)

10. Resumen

11. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿Qué es un futex?

Fast userspace mutex: una primitiva que hace el trabajo en espacio de usuario y solo entra al núcleo cuando hay contención.

2. ¿Qué es el fast path?

Adquirir el mutex con una operación atómica sin entrar al núcleo (cuando no hay contención).

3. ¿Qué es el slow path?

Bloquear el hilo en el núcleo con futex(FUTEX_WAIT) (cuando hay contención).

4. ¿Qué syscall usa el fast path?

Ninguna. Usa una operación atómica en espacio de usuario.

5. ¿Qué syscall usa el slow path?

futex(FUTEX_WAIT) y futex(FUTEX_WAKE).

6. ¿Cuál es el costo del fast path?

~20 ns (una instrucción atómica).

7. ¿Cuál es el costo del slow path?

~1-10 μs (syscall + cambio de contexto).

8. ¿Qué implementa glibc con futexes?

pthread_mutex, pthread_cond, sem_t, pthread_rwlock.

9. ¿Por qué los mutexes son rápidos sin contención?

Porque el fast path no entra al núcleo: es una sola instrucción atómica.

10. ¿Quién introdujo los futexes en Linux?

Hubertus Franke, en Linux 2.6 (2003).

Para practicar

  1. Usa strace para observar las syscalls de futex con y sin contención.
  2. Mide el costo del mutex con perf (ciclos) sin contención.
  3. Mide el costo del mutex con perf con contención.
  4. Explica por qué el fast path no entra al núcleo.
  5. Investiga cómo se implementa el futex en el núcleo de Linux.
  6. Explica por qué el valor 2 en el lock indica waiters.