← Concurrencia y Sincronización Bloque 4 · Mutex · Tema 16 de 47

Tema 16

Granularidad y costo de los bloqueos

Poner un solo mutex alrededor de todo el código protegido es simple pero serializa todo el trabajo. Usar muchos mutex pequeños permite más paralelismo pero complica el código y aumenta el riesgo de interbloqueos. En este tema verás el trade-off entre granularidad gruesa y fina, cómo la contención afecta al rendimiento, y cómo la ley de Amdahl limita la aceleración cuando la sincronización es excesiva.

1. Qué es la granularidad

La granularidad de un bloqueo se refiere a cuánto código protege un solo mutex. Un bloqueo de granularidad gruesa (coarse-grained) protege una gran sección de código o una estructura de datos completa. Un bloqueo de granularidad fina (fine-grained) protege una pequeña sección o un elemento individual.

La granularidad es una decisión de diseño que afecta al rendimiento y a la complejidad del código. No hay una respuesta única: depende de la carga de trabajo, del patrón de acceso y de los requisitos del sistema.

Granularidad = tamaño de la sección crítica

Un mutex de granularidad gruesa protege una sección crítica grande. Uno de granularidad fina protege una sección pequeña. La granularidad afecta a cuánto paralelismo se puede lograr.

2. Granularidad gruesa (coarse-grained)

En la granularidad gruesa, un solo mutex protege toda la estructura de datos o todo el código que accede a ella:

granularidad gruesa
/* Un solo mutex para toda la lista. */
typedef struct {
    nodo_t *lista;
    pthread_mutex_t mutex;
} lista_t;

void insertar(lista_t *l, int valor) {
    pthread_mutex_lock(&l->mutex);
    /* Toda la operación de inserción está protegida. */
    nodo_t *nuevo = malloc(sizeof(nodo_t));
    nuevo->valor = valor;
    nuevo->siguiente = l->lista;
    l->lista = nuevo;
    pthread_mutex_unlock(&l->mutex);
}

int buscar(lista_t *l, int valor) {
    pthread_mutex_lock(&l->mutex);
    /* Toda la búsqueda está protegida. */
    nodo_t *actual = l->lista;
    while (actual) {
        if (actual->valor == valor) {
            pthread_mutex_unlock(&l->mutex);
            return 1;
        }
        actual = actual->siguiente;
    }
    pthread_mutex_unlock(&l->mutex);
    return 0;
}

Ventajas:

Desventajas:

3. Granularidad fina (fine-grained)

En la granularidad fina, cada elemento o grupo de elementos tiene su propio mutex:

granularidad fina
/* Un mutex por nodo. */
typedef struct nodo {
    int valor;
    struct nodo *siguiente;
    pthread_mutex_t mutex;
} nodo_t;

typedef struct {
    nodo_t *lista;
    pthread_mutex_t lista_mutex;  /* protege la cabeza de la lista */
} lista_t;

void insertar(lista_t *l, int valor) {
    nodo_t *nuevo = malloc(sizeof(nodo_t));
    nuevo->valor = valor;
    pthread_mutex_init(&nuevo->mutex, NULL);

    /* Solo bloquear la cabeza de la lista. */
    pthread_mutex_lock(&l->lista_mutex);
    nuevo->siguiente = l->lista;
    l->lista = nuevo;
    pthread_mutex_unlock(&l->lista_mutex);
}

int buscar(lista_t *l, int valor) {
    pthread_mutex_lock(&l->lista_mutex);
    nodo_t *actual = l->lista;
    pthread_mutex_unlock(&l->lista_mutex);

    /* Buscar sin lock (puede ser inseguro si se inserta concurrentemente). */
    while (actual) {
        if (actual->valor == valor) return 1;
        actual = actual->siguiente;
    }
    return 0;
}

Ventajas:

Desventajas:

4. Comparación

AspectoGranularidad gruesaGranularidad fina
ConcurrenciaBaja (todo serializado)Alta (paralelo por elemento)
ContenciónAltaBaja
ComplejidadBajaAlta
Riesgo de deadlockBajo (un lock)Alto (múltiples locks)
Overhead de memoriaBajoAlto
EscalabilidadPobreBuena
VerificabilidadFácilDifícil

5. La contención

La contención (contention) es el grado en que los hilos compiten por el mismo lock. Con alta contención, los hilos pasan más tiempo esperando el lock que haciendo trabajo útil.

La contención se mide como la fracción de tiempo que un hilo pasa esperando el lock vs. ejecutando la sección crítica. Si un hilo pasa el 80 % del tiempo esperando, la contención es alta.

La contención afecta al rendimiento de varias formas:

La solución a la alta contención no es un lock mejor, sino reducir la contención: usar más locks, reducir la sección crítica, o rediseñar el programa.

6. La ley de Amdahl en la práctica

En el tema 1 viste la ley de Amdahl: la aceleración está limitada por la parte serie del programa. La sincronización es una fuente de serialización: cada lock convierte código paralelo en serie.

Si una sección crítica protegida por un lock representa una fracción f del tiempo de ejecución, y el lock se adquiere con frecuencia, la aceleración máxima es:

Aceleración máxima = 1 / f (si la sección crítica es completamente serie).

Por ejemplo, si el 10 % del tiempo se pasa en la sección crítica (adquiriendo y liberando el lock), la aceleración máxima es 10×, sin importar cuántos núcleos haya.

La granularidad fina reduce f (la fracción serie) al acortar la sección crítica y reducir la contención. Pero añade overhead (más locks, más código).

7. Un ejemplo desarrollado

Un programa que mantiene un array de contadores. Varios hilos incrementan contadores aleatorios. Se compara la granularidad gruesa (un lock para todo el array) con la fina (un lock por contador):

contadores.c
#define _POSIX_C_SOURCE 200809L
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define NUM_CONTADORES 1000
#define INCREMENTOS 100000

/* Granularidad gruesa: un lock para todo. */
typedef struct {
    int contadores[NUM_CONTADORES];
    pthread_mutex_t mutex;
} array_grueso_t;

/* Granularidad fina: un lock por contador. */
typedef struct {
    int valor;
    pthread_mutex_t mutex;
} contador_fino_t;

typedef struct {
    contador_fino_t contadores[NUM_CONTADORES];
} array_fino_t;

/* Función de incremento con granularidad gruesa. */
void incrementar_grueso(array_grueso_t *a, int idx) {
    pthread_mutex_lock(&a->mutex);
    a->contadores[idx]++;
    pthread_mutex_unlock(&a->mutex);
}

/* Función de incremento con granularidad fina. */
void incrementar_fino(array_fino_t *a, int idx) {
    pthread_mutex_lock(&a->contadores[idx].mutex);
    a->contadores[idx].valor++;
    pthread_mutex_unlock(&a->contadores[idx].mutex);
}

Con granularidad gruesa, todos los incrementos se serializan: un solo hilo a la vez puede incrementar cualquier contador. Con granularidad fina, los hilos pueden incrementar contadores distintos simultáneamente.

La diferencia de rendimiento es significativa: con 4 hilos y 1000 contadores, la granularidad fina puede ser 4 veces más rápida que la gruesa (si los hilos acceden a contadores distintos).

7. El trade-off

La elección de granularidad es un trade-off entre:

FactorGranularidad gruesaGranularidad fina
RendimientoBajo (serialización)Alto (paralelismo)
ComplejidadBajaAlta
Riesgo de deadlockBajoAlto
Overhead de memoriaBajoAlto
VerificabilidadFácilDifícil

La regla práctica es: empezar con granularidad gruesa y refinar solo donde la contención sea un problema. Premature optimization es la raíz de todos los males.

Empezar grueso, refinar después

La práctica recomendada es empezar con un solo mutex (granularidad gruesa) y, si la contención es un problema, refinar a granularidad fina solo donde sea necesario. Esto minimiza la complejidad y el riesgo de deadlock.

8. Medir el costo

Para medir el costo de la contención, se puede usar un benchmark que mida el tiempo de ejecución con distintos números de hilos y distintas granularidades:

terminal · medir contención
$ gcc -std=c17 -Wall -Wextra -O2 -pthread -o contadores contadores.c
$ for n in 1 2 4 8; do echo -n "$n hilos: "; ./contadores $n; done
1 hilos: 0.05 s
2 hilos: 0.08 s (grueso) / 0.03 s (fino)
4 hilos: 0.15 s (grueso) / 0.02 s (fino)
8 hilos: 0.30 s (grueso) / 0.015 s (fino)

Con granularidad gruesa, el tiempo aumenta con el número de hilos (contención). Con granularidad fina, el tiempo disminuye (paralelismo).

9. Simulador interactivo

Este simulador compara la granularidad gruesa con la fina para un array de contadores:

Simulador · granularidad de bloqueos

MedidaValor
Tiempo total (unidades)
Contención (%)
Paralelismo efectivo

10. Resumen

11. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿Qué es la granularidad de un bloqueo?

Cuánto código protege un solo mutex.

2. ¿Qué es la granularidad gruesa?

Un solo mutex para toda la estructura de datos o todo el código.

3. ¿Qué es la granularidad fina?

Un mutex por elemento o por sección pequeña.

4. ¿Cuál es la ventaja de la granularidad gruesa?

Simplicidad, sin deadlock, fácil de verificar.

5. ¿Cuál es la ventaja de la granularidad fina?

Alta concurrencia, baja contención, mejor escalabilidad.

6. ¿Qué es la contención?

El grado en que los hilos compiten por el mismo lock.

7. ¿Cómo afecta la contención al rendimiento?

Los hilos pasan más tiempo esperando que haciendo trabajo útil.

8. ¿Cómo se relaciona la sincronización con la ley de Amdahl?

La sincronización serializa el código, limitando la aceleración.

9. ¿Cuál es la práctica recomendada?

Empezar con granularidad gruesa y refinar donde la contención sea un problema.

10. ¿Cuál es la solución a la alta contención?

Reducir la contención (más locks, secciones más cortas), no un lock mejor.

Para practicar

  1. Implementa el programa de contadores con granularidad gruesa y fina.
  2. Mide el rendimiento con 1, 2, 4 y 8 hilos para ambas granularidades.
  3. Calcula la contención para cada caso.
  4. Explica por qué la granularidad fina puede ser más lenta con pocos hilos.
  5. Diseña un esquema de granularidad intermedia (grupos de contadores).
  6. Investiga cómo se implementa la granularidad fina en bases de datos (row-level locking).