Concurrencia en Sistemas Operativos: Semáforos, Monitores y Problemas Clásicos de Sincronización
1. Semáforos
wait(S): while(S<=0); S:=S-1 y signal(S): S:=S+1. Un semáforo contador (semaforo=contador) puede tomar cualquier valor entero no negativo, mientras que un semáforo binario (semaforoBinario) solo toma los valores 0 o 1.1.1 Sección crítica
semaforoBinario mutex=1
proceso(){
while(true){ //SNC
wait(mutex) //SC
signal(mutex)
}
}1.2 Problema productor-consumidor
semaforoBinario mutex=1
semaforo elementos=0
semaforo espacios=TAMBUF
productor(){
while(true){
item=producir()
wait(espacios)
wait(mutex)
guardarBuffer(item)
signal(mutex)
signal(elementos)
}
}
consumidor(){
while(true){
wait(elementos)
wait(mutex)
item=quitarBuffer()
signal(mutex)
signal(espacios)
consumir(item)
}
}Clave: el semáforo de condición (elementos/espacios) debe hacer wait antes que el mutex; en caso contrario se produce un interbloqueo.
1.3 Problema de lectores-escritores (prioridad a lectores)
semaforoBinario mutex=1, escritura=1
int lectores=0
lector(){
while(true){
wait(mutex)
lectores++
if(lectores==1) wait(escritura)
signal(mutex) //LEER
wait(mutex)
lectores--
if(lectores==0) signal(escritura)
signal(mutex)
}
}
escritor(){
while(true){
wait(escritura) //ESCRIBIR
signal(escritura)
}
}Nota: esta solución puede provocar inanición de los escritores.
1.4 Barbero dormilón
semaforoBinario mutex=1
semaforo cliente=0, barbero=0
int sillas=NUMSILLAS
barbero(){
while(true){
wait(cliente)
wait(mutex)
sillas++
signal(barbero)
signal(mutex) //CORTAR PELO
}
}
cliente(){
while(true){
wait(mutex)
if(sillas>0){
sillas--
signal(cliente)
signal(mutex)
wait(barbero) //PELARSE
} else {
signal(mutex) //se va
}
}
}1.5 Filósofos comensales (5 filósofos, un tenedor entre cada dos platos)
v1 ingenua (interbloqueo):
semaforo tenedor[5]={1,1,1,1,1}
filosofo(i){
while(true){
pensar()
wait(tenedor[i])
wait(tenedor[(i+1)%5])
comer()
signal(tenedor[i])
signal(tenedor[(i+1)%5])
}
}Fallo: si los cinco filósofos cogen el tenedor izquierdo a la vez y esperan el derecho, se produce una espera circular, es decir, un interbloqueo. Con exclusión mutua se cumple la EM, pero no se evita el interbloqueo.
v2 camarero (solución rápida):
semaforo tenedor[5]={1,1,1,1,1}
semaforo camarero=4 //máximo 4 compitiendo
filosofo(i){
while(true){
pensar()
wait(camarero)
wait(tenedor[i])
wait(tenedor[(i+1)%5])
comer()
signal(tenedor[i])
signal(tenedor[(i+1)%5])
signal(camarero)
}
}Justificación: con camarero=4 nunca compiten los cinco filósofos a la vez, por lo que se rompe el ciclo de espera. Se mantiene la exclusión mutua (los tenedores son binarios) y no hay inanición gracias a la cola FIFO.
2. Monitores (pseudocódigo, Ben-Ari)
Un monitor ofrece exclusión mutua automática (no hace falta un mutex manual). Dispone de variables de condición con waitC() y signalC(), y empty(c) comprueba si la cola de la condición está vacía.
Nota sobre if/while: en esta notación clásica se usa if. En Java con notifyAll es obligatorio while, para volver a comprobar la condición al despertar.
2.1 Productor-consumidor con monitor
monitor PC
bufferType buffer<-empty
condition notEmpty, notFull
operation append(v)
if buffer is full: waitC(notFull)
append(v,buffer)
signalC(notEmpty)
operation take()
if buffer is empty: waitC(notEmpty)
w<-head(buffer)
signalC(notFull)
return w
producer: loop{d<-produce; PC.append(d)}
consumer: loop{d<-PC.take; consume(d)}2.2 Lectores-escritores con monitor
monitor RW
int readers<-0, writers<-0
condition OKtoRead, OKtoWrite
op iniciarLectura
if writers!=0 or not empty(OKtoWrite): waitC(OKtoRead)
readers<-readers+1
signalC(OKtoRead) //cascada siguiente lector
op terminarLectura
readers<-readers-1
if readers=0: signalC(OKtoWrite)
op iniciarEscritura
if writers!=0 or readers!=0: waitC(OKtoWrite)
writers<-writers+1
op terminarEscritura
writers<-writers-1
if empty(OKtoRead) then signalC(OKtoWrite) else signalC(OKtoRead)2.3 Filósofos con monitor (ForkMonitor, sin interbloqueo)
fork[i] indica los tenedores libres del filósofo i (2 significa que puede comer).
monitor ForkMonitor
int array[0..4] fork<-[2,2,2,2,2]
condition array[0..4] OKtoEat
op takeForks(i)
if fork[i]!=2: waitC(OKtoEat[i])
fork[i+1]<-fork[i+1]-1
fork[i-1]<-fork[i-1]-1
op releaseForks(i)
fork[i+1]<-fork[i+1]+1
fork[i-1]<-fork[i-1]+1
if fork[i+1]=2: signalC(OKtoEat[i+1])
if fork[i-1]=2: signalC(OKtoEat[i-1])
filosofo(i): loop{think; takeForks(i); eat; releaseForks(i)}3. Java (monitor)
Hilos: se crean extendiendo Thread o implementando Runnable; el código va en run() y el hilo arranca con start().
Monitor en Java: exclusión mutua con synchronized sobre el mismo objeto. No hay variables condición (existe una única cola), así que se usa while(!cond) wait(); seguido de notifyAll(). wait() siempre debe ir dentro de try/catch(InterruptedException). notify() despierta un hilo aleatorio (posible inanición); notifyAll() despierta a todos.
Estructura: synchronized m1(){while(!cond) wait();} y synchronized m2(){notifyAll();}
3.1 Productor-consumidor en Java
class PCMonitor{
final int N=5
int Oldest=0,Newest=0
volatile int Count=0
int Buffer[]=new int[N]
synchronized void Append(int V){
while(Count==N) try{wait();}catch(InterruptedException e){}
Buffer[Newest]=V
Newest=(Newest+1)%N
Count=Count+1
notifyAll()}
synchronized int Take(){
int temp
while(Count==0) try{wait();}catch(InterruptedException e){}
temp=Buffer[Oldest]
Oldest=(Oldest+1)%N
Count=Count-1
notifyAll()
return temp}}
Hilos: class Productor extends Thread{PCMonitor m; run(){while(true){m.Append(producir());}}}
class Consumidor extends Thread{PCMonitor m; run(){while(true){consumir(m.Take());}}}3.2 Lectores-escritores en Java
class RWMonitor{
volatile int readers=0
volatile boolean writing=false
synchronized void StartRead(){
while(writing) try{wait();}catch(InterruptedException e){}
readers=readers+1
notifyAll()}
synchronized void EndRead(){
readers=readers-1
if(readers==0) notifyAll()}
synchronized void StartWrite(){
while(writing||(readers!=0)) try{wait();}catch(InterruptedException e){}
writing=true}
synchronized void EndWrite(){
writing=false
notifyAll()}}4. Ada: citas (rendezvous) y select
Una cita es una invocación remota, directa y asimétrica. El servidor es una task con entry.
Select general:
select
when guarda1 => accept E1(...) do --cita CORTA--
end E1; --actualizar fuera del do--
or when guarda2 => accept E2(...) do ... end E2;
or delay 5.0; --opcional
or terminate; --opcional
else ... --opcional, no con delay/terminate
end selectSemántica: solo se consideran las guardas abiertas. Si varias tienen llamada pendiente, la elección es indeterminista. Si ninguna tiene llamada y existe else, se ejecuta el else. Si ninguna tiene llamada y no hay else, la tarea se suspende. Si todas las guardas están cerradas, se produce un error.
4.1 Productor-consumidor con task (Buffer)
task Buffer is
entry Leer(x:out ITEM)
entry Escribir(x:in ITEM)
end Buffer;
task body Buffer is
N:constant:=10
B:array(1..N) of ITEM
Inicio,Fin:INTEGER range 1..N:=1
Cont:INTEGER range 0..N:=0
begin
loop
select
when Cont<N =>
accept Escribir(x:in ITEM) do B(Fin):=x end
Fin:=Fin mod N+1; Cont:=Cont+1
or when Cont>0 => --OJO: >0 no >1
accept Leer(x:out ITEM) do x:=B(Inicio) end
Inicio:=Inicio mod N+1; Cont:=Cont-1
end select
end loop
end Buffer;Truco: actualizar índices y contadores fuera del accept ... do ... end (cita corta).
4.2 Productor-consumidor con objeto protegido (barreras entry when)
protected type Buffer_Type is
entry Put(X:in Item)
entry Get(X:out Item)
private
Box:Store
First,Last:Index:=Index'First
Number:Count:=0
end Buffer_Type;
protected body Buffer_Type is
entry Put(X:in Item) when Number<Size is
begin Box(Last):=X; Last:=Last+1; Number:=Number+1 end Put;
entry Get(X:out Item) when Number>0 is
begin X:=Box(First); First:=First+1; Number:=Number-1 end Get;
end Buffer_Type;5. Teoría (2 pts, lo más probable)
5.1 Propiedades
- Seguridad: "nunca pasa algo malo" (exclusión mutua, ausencia de interbloqueo, corrección parcial).
- Vivacidad: "algo bueno pasa" (terminación, equidad y ausencia de inanición; dependen del planificador).
5.2 Sección crítica buena
Debe cumplir: exclusión mutua, ausencia de interbloqueo, ausencia de inanición y ausencia de retrasos innecesarios. Dekker resuelve el caso de 2 procesos; Panadería, el de N procesos.
5.3 Tipos de semáforos
- Espera activa: puede provocar inanición; no es débilmente equitativo.
- Cola de bloqueo: no provoca inanición; es fuertemente equitativo.
- Conjunto de bloqueo: provoca inanición si hay más de 2 procesos.
5.4 Wait/signal en semáforos frente a monitores
- wait: en un semáforo puede bloquear o no; en un monitor siempre bloquea.
- signal: en un semáforo siempre tiene efecto; en un monitor puede no tener efecto si la cola está vacía.
- Despierta a: en un semáforo, cualquier proceso; en un monitor, el primero de la cola.
- Tras signal: en un semáforo, el proceso despertado sigue ya; en un monitor, espera a que el que hizo el signal salga del monitor.
5.5 Monitores
Tienen exclusión mutua por definición y sincronización mediante variables condición. Existen varias semánticas de signal (signal-and-wait, urgent, continue). En Java, al no haber variables condición, se emplea while con notifyAll.
5.6 Lectores-escritores
La solución con semáforos provoca inanición de escritores.
5.7 Ada
- Citas: remotas, directas y asimétricas.
- Select: guardas más indeterminismo (else, delay, terminate).
- Objeto protegido: función concurrente, procedimiento exclusivo y entry con barrera. Las barreras tienen preferencia y no admiten bloqueantes dentro; una barrera sin parámetros provoca requeue.
5.8 Inconvenientes de los semáforos
- Confusión de propósito.
- Escasa capacidad semántica.
- Propensión a errores (un cambio de orden puede causar interbloqueo).
- Acoplamiento innecesario.
- Difíciles de manejar con muchos procesos.
5.9 Sistemas distribuidos (T8 en adelante)
Memoria compartida: variables con exclusión mutua o mensajes en buffers FIFO. Red débilmente acoplada: paso de mensajes, modelo cliente-servidor. Canal síncrono (bloqueante) frente a asíncrono (con buffer). En el modelo cliente-servidor, el servidor queda bloqueado esperando peticiones; el cliente multihilo crea un hilo por petición y vuelve a bloquearse.