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
- Cualquier número de lectores puede estar dentro a la vez.
- Un escritor necesita el recurso en exclusiva.
- Se lleva la cuenta de lectores dentro; el primero reserva el recurso y el último lo libera.
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:
- Preferencia al escritor: los lectores nuevos esperan si hay un escritor aguardando.
- Turnos por antigüedad: cola FIFO de peticiones; quien llegó antes pasa antes.
- En C++ moderno,
std::shared_mutexofrece exactamente esta semántica:std::shared_lockpara lectores ystd::unique_lockpara escritores.
#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;
}