Patrones clásicos

Lectores–escritores

Cuando un recurso compartido se usa de dos formas —muchos hilos solo leen y alguno ocasionalmente escribe— un mutex ordinario serializa de más: los lectores podrían entrar a la vez. El patrón lectores–escritores lo permite, y en planta aparece en tablas de configuración y, sorprendentemente, en los pasillos de AGV.

La instalación en acción

Tres AGV cruzan una zona de inspección en paralelo (lectores) mientras una carretilla de mantenimiento la ocupa en exclusiva (escritor). Arranca la simulación y observa cómo los AGV comparten el carril a la vez, pero esperan a que la carretilla termine.

Consejo: mira el valor de sCarril cuando entra el primer lector y cuando sale el último.

El patrón básico

Semaforo sRecurso(1);      // el recurso en sí
Semaforo sLectores(1);     // protege el contador de lectores
int nLectores = 0;

void lector() {
    sLectores.wait();
    nLectores++;
    if (nLectores == 1) sRecurso.wait();   // el primer lector reserva
    sLectores.signal();

    leer();                                // varios lectores a la vez

    sLectores.wait();
    nLectores--;
    if (nLectores == 0) sRecurso.signal(); // el último libera
    sLectores.signal();
}

void escritor() {
    sRecurso.wait();                       // exclusividad total
    escribir();
    sRecurso.signal();
}

Aplicación directa: los parámetros de cinco reguladores PI que llegan por el canal de comunicaciones (escritor) mientras los hilos de control los leen cada período de muestreo (lectores).

El pasillo de AGV: lectores–escritores físico

Del cuaderno SCTR (ej54): diez AGV transportan paquetes entre dos almacenes unidos por un pasillo estrecho. Pueden entrar varios AGV a la vez si todos van en el mismo sentido; los del sentido contrario deben esperar a que el pasillo quede vacío. Es el patrón con dos «bandos»:

Semaforo sIda(1), sVuelta(1);   // protegen sus contadores
Semaforo sPasillo(1);           // el pasillo en sí
int nIda = 0, nVuelta = 0;      // AGV en cada sentido

void cruza(int sentido, int agv) {
    if (sentido == IDA) {
        sIda.wait();
        if (nIda == 0) sPasillo.wait();   // el primero reserva el pasillo
        nIda++;
        sIda.signal();

        cruzaPasillo(1, 2, agv);          // varios pueden ir a la vez

        sIda.wait();
        nIda--;
        if (nIda == 0) sPasillo.signal(); // el último lo libera
        sIda.signal();
    } else {
        // simétrico con sVuelta y nVuelta
    }
}

El mismo esqueleto sirve para: grúas que comparten carril, tuberías con fluidos incompatibles, tramos de vía única, etc.

Inanición y variantes

Con la versión anterior, una corriente continua de lectores (o de AGV en un sentido) puede dejar esperando indefinidamente al escritor (o al otro sentido). Soluciones:

#include <shared_mutex>

std::shared_mutex mtxConfig;

double leeConsigna(int regulador) {
    std::shared_lock lk(mtxConfig);       // varios lectores a la vez
    return consigna[regulador];
}

void escribeConsigna(int regulador, double v) {
    std::unique_lock lk(mtxConfig);       // escritor en exclusiva
    consigna[regulador] = v;
}