Unidad 3 Memoria común
Transcript of Unidad 3 Memoria común
UNIDAD 3 MEMORIA
COMÚN
El problema de exclusión mutua
Exclusión mutua
• Exclusión mutua significa asegurar que un recurso compartido (variable, estructura de datos, etc.) sea accedida por un solo hilo a la vez, el acceso se refiere a ejecutar la sección crítica correspondiente del hilo.
• Esto significa a su vez establecer un preprotocolo y un postprotocolo para evitar que 2 o más hilos estén ejecutando sus secciones críticas al mismo tiempo.
• Este modelo basado en secciones críticas permite además que los procesos concurrentes se comuniquen a través de ellas, utilizando variables comunes protegidas por secciones críticas como medio de comunicación.
Pre y Post protocolos para exclusión
mutua. • De acuerdo con ello, la notación para representar a un proceso que
ejecuta una sección crítica bajo exclusión mutua será el que se presenta en la Figura 2.1.
hilos Ti, i=0, 1, 2, …
while (true){
…
código de sección no crítica
…
wantToEnterCS(i); /* preprotocolo*/
…
código de la sección crítica
…
finishedCS(i); /*postprotocolo*/
}
Propiedades que se deben cumplir
• Seguridad: No deben ocurrir deadlocks, los abrazos mortales ocurren cuando dos hilos intentaron entrar a sus secciones críticas al mismo tiempo y ninguno tuvo éxito (ambos se bloquearon o se quedan en espera ocupada indefinidamente) aún cuando ningún otro hilo intente entrar a su región crítica.
• Existe una forma de deadlock denominada livelock en la cual los hilos no están bloqueados o en espera sino que ejecutan su código de preprotocolo indefinidamente.
• Ausencia de espera innecesaria: Ningún hilo fuera de su SC debe bloquear o estorbar a otros hilos que están fuera de sus SC y que quieran entrar.
• Entrada asegurada: ningún hilo deberá esperar por siempre para poder entrar a su SC, o ser bloqueado indefinidamente cuando intente entrar; i.e., debe asegurarse ausencias de inanición.
Formas de inanición
1. En ausencia de contención: ocurre cuando un hilo T
intentó entrar a su SC y aún cuando ningún otro hilo
está en su correspondiente SC, T no puede entrar.
2. En presencia de contención: cuando 2 o más hilos
intentaron entrar a sus SC pero uno nunca puede
mientras que el otro entra una y otra vez.
• Se asume que los hilos no se detienen o colisionan en
sus pre-post protocolos, ni en sus SC ni fuera de ellas.
• Las secciones críticas, por tanto, deben ejecutarse de
forma tal que parecieran ser operaciones atómicas.
Implementación de la exclusión mutua
• Por procesador
• Por memoria
Implementación de la exclusión mutua
1. Por procesador:
• Cuando un proceso está ejecutando su sección crítica,
ejecuta un protocolo de entrada que desactiva las
interrupciones, y uno de salida que las activa, lo cual
basta para asegurar la exclusión mutua.
• Es un sistema de alto riesgo, porque deja el control de las
interrupciones a cargo del usuario.
• En un gran proyecto, con varios programadores
involucrados, alguien puede simplemente olvidarse de
activar las interrupciones que otra persona desactivó
previamente, derivándose efectos indeseables.
Implementación de la exclusión mutua
2. por memoria:
• Se define una variable común a todos los procesos
concurrentes que controla el acceso a la sección crítica.
• El protocolo de entrada consultará ahora su valor y en
función de él dejará entrar o bloqueará al proceso que
desea entrar en su sección crítica.
• El protocolo de salida modificará el valor de la variable
para indicar que el recurso ya está libre.
• La variable común se utiliza como cauce de
comunicación entre los distintos procesos, al objeto de
acceder éstos a sus secciones críticas con cierto orden.
Clasificación de algoritmos para resolver
el problema de la exclusión mutua
1. Centralizados: utilizan variables compartidas entre los
procesos que expresan el estado de los procesos ( se
suele acceder a estas variables en los protocolos de
E/S)
2. Distribuidos: no utilizan variables compartidas entre
los procesos. Suelen utilizar paso de mensajes.
Propuestas para resolver el problema de
exclusión mutua: • Esperas ocupadas vs. bloqueos ( con soporte del SO),
• soluciones por hardware,
• etc.
hilos Ti, i=0, 1, 2, …
while (true){
…
código de sección no crítica
…
wantToEnterCS(i); /* preprotocolo*/
…
código de la sección crítica
…
finishedCS(i); /*postprotocolo*/
}
Soluciones (en software) para 2 hilos, por
espera ocupada
• Primera aproximación
• Segunda aproximación
• Tercera aproximación
• Cuarta aproximación
• Solución Dekker
• Solución Peterson
Soluciones (en software) para 2 hilos, por
espera ocupada • Primera aproximación
• La primera aproximación (clase att1.java) consiste en
alternar una variable que indica el “turno”, así los dos
hilos involucrados alternan la ejecución de sus SC:
class Arbitrator {
private static final int NUM_NODES = 2; // solo 2 hilos
private volatile int turn = 0; // 1er. Intento:alternado
//estricto
public Arbitrator(int numNodes) {
if (numNodes != NUM_NODES) {
System.err.println("Arbitrator: numNodes=" + numNodes
+ " which is != " + NUM_NODES);
System.exit(1);
}
}
private int other(int i) { return (i + 1) % NUM_NODES; }
public void wantToEnterCS(int i) { // pre-protocol
while (turn != i) /* busy wait */
Thread.currentThread().yield();
}
public void finishedInCS(int i) { // post-protocol
turn = other(i);
}
}
Primera aproximación, problema
• Este intento padece de inanición en ausencia de
contención: puede ocurrir que un hilo quiera entrar a su
SC pero no sea su turno y deba por tanto esperar.
• Inanición en ausencia de contención: ocurre cuando un
hilo T intentó entrar a su SC y aún cuando ningún otro
hilo está en su correspondiente SC, T no puede entrar.
Segunda aproximación
• El segundo intento (clase att2.java) utiliza una variable
como bandera para indicar el deseo del hilo por entrar a
su SC:
class Flag { public volatile boolean value = false; }
...
// Second attempt: check other's flag.
private Flag[] desiresCS = new Flag[NUM_NODES];
...
for (int i = 0; i < NUM_NODES; i++) desiresCS[i] = new Flag();
...
public void wantToEnterCS(int i) { // pre-protocol
while (desiresCS[other(i)].value) // busy wait
Thread.currentThread().yield();
desiresCS[i].value = true;
}
public void finishedInCS(int i) { // post-protocol
desiresCS[i].value = false;
}
Segunda aproximación, problema
• Este tampoco funciona del todo: dos hilos entran a sus
SC a la vez si ambos ejecutan sus ciclos de espera
ocupada al verificar el estado de la bandera del otro hilo.
Cada uno observa la bandera en falso y entran.
Tercera aproximación
• El tercer intento (clase att3.java) intenta corregir el 2do
invirtiendo el orden de la verificación de la bandera
asociada al vecino y señalando su propio deseo por
entrar a su SC:
...
// Third attempt: set own flag.
...
public void wantToEnterCS(int i) { // pre-protocol
desiresCS[i].value = true;
while (desiresCS[other(i)].value) // busy wait
Thread.currentThread().yield();
}
public void finishedInCS(int i) { // post-protocol
desiresCS[i].value = false;
}
Tercera aproximación, problema
• Esta modificación asegura exclusión mutua pero permite
la posibilidad de deadlock, ya que ambos hilos se
quedan indefinidamente en sus ciclos de espera si ambos
intentan entrar casi al mismo tiempo.
• Cada uno observa que la bandera del otro es verdadera
entonces esperan por siempre.
Cuarta aproximación
• El cuarto intento (clase att4.java) intenta corregir al
anterior haciendo que los hilos se retrocedan de entrar a
sus SC si detectan que ambos están tratando de entrar a
la vez: ...
// Fourth attempt: back off.
...
public void wantToEnterCS(int i) { // pre-protocol
desiresCS[i].value = true;
while (desiresCS[other(i)].value) {
desiresCS[i].value = false; // back off
Thread.currentThread().yield();
desiresCS[i].value = true;
}
}
public void finishedInCS(int i) { // post-protocol
desiresCS[i].value = false;
}
Cuarta aproximación, problema
• Desafortunadamente modifica el problema de deadlock a
uno relacionado denominado livelock en el cual los hilos
retroceden indefinidamente cuando coinciden al intentar
entrar a sus SC.
• livelock en el cual los hilos no están bloqueados o en espera sino
que ejecutan su código de preprotocolo indefinidamente.
Solución Dekker • Esta propuesta fue desarrollada por Dekker en 1960:
corrige el cuarto intento considerando que los hilos tomen
turnos para retroceder (clase attd.java).
…
// Dekker's solution: take turns backing off.
public void wantToEnterCS(int i) { // pre-protocol
desiresCS[i].value = true;
while (desiresCS[other(i)].value) {
if (turn != i) {
desiresCS[i].value = false; // back off
while (turn != i) /* busy wait */
Thread.currentThread().yield();
desiresCS[i].value = true;
}
}
}
public void finishedInCS(int i) { // post-protocol
desiresCS[i].value = false;
turn = other(i);
}
Solución Dekker
• Asegura exclusión mutua, evita deadlocks y no permite
inanición en ausencia de contención, pero permite
inanición en presencia de contención.
• Inanición en presencia de contención: cuando 2 o
más hilos intentaron entrar a sus SC pero uno nunca
puede mientras que el otro entra una y otra vez.
Solución Peterson
• En 1981 Peterson descubrió una solución mejor: (clase
attp.java) la variable compartida last registra qué hilo
intentó entrar a su SC, a éste hilo se le retarda si ambos
hilos intentan entrar a la vez. Esta propuesta elimina la
inanición en presencia de contención:
... private volatile int last = 0; // Peterson's solution.
...
public void wantToEnterCS(int i) { // pre-protocol
desiresCS[i].value = true;
last = i;
while (desiresCS[other(i)].value && last == i) // busy wait
Thread.currentThread().yield();
}
public void finishedInCS(int i) { // post-protocol
desiresCS[i].value = false;
}
Algoritmo de la panadería de Lamport
• Cuando hay más de 2 hilos involucrados, ninguno de los
algoritmos anteriores funcionan sin complicadas
modificaciones.
• El algoritmo de la panadería de Lamport está diseñado
para soportar múltiples hilos: un hilo que desea entrar a
su SC calcula el número del siguiente boleto que indica el
turno y espera, de manera análoga a los clientes cuando
entran a una panadería.
• Cada hilo usa el valor máximo de los boletos anteriores y
le suma 1. Dado que 2 hilos podrían calcular el mismo
número, para asegurar unicidad se le identifica a cada
hilo por un número y permitirá evitar empates.
Algoritmo de Lamport para 2 hilos class Ticket{public volatile int value =0;}
...
// Lamport's bakery ticket algorithm.
private Ticket[] ticket = new Ticket[NUM_NODES];
...
for (int i = 0; i < NUM_NODES; i++) ticket[i] = new Ticket();
...
public void wantToEnterCS(int i) { // pre-protocol
ticket[i].value = 1;
ticket[i].value = ticket[other(i)].value + 1; // compute next ticket
while (!(ticket[other(i)].value == 0
|| ticket[i].value < ticket[other(i)].value
|| (ticket[i].value == ticket[other(i)].value // break a tie
&& i == 0))) /* busy wait */
Thread.currentThread().yield();
}
public void finishedInCS(int i) { // post-protocol
ticket[i].value = 0;
}
Algoritmo de Lamport para n hilos private int numNodes = 0; // Lamport's bakery ticket algorithm.
private Ticket[] ticket = null;
private int maxx(Ticket[] ticket) {
int mx = ticket[0].value;
for (int i = 1; i < ticket.length; i++)
if (ticket[i].value > mx) mx = ticket[i].value;
return mx;
}
public void wantToEnterCS(int i) { // pre-protocol
ticket[i].value = 1;
ticket[i].value = 1 + maxx(ticket); // compute next ticket
for (int j = 0; j < numNodes; j++) if (j != i)
while (!(ticket[j].value == 0 || ticket[i].value < ticket[j].value
|| // break a tie
(ticket[i].value == ticket[j].value && i < j))) // busy wait
Thread.currentThread().yield();
}
public void finishedInCS(int i) { // post-protocol
ticket[i].value = 0;
}
Soluciones (en hardware )
• Con el apoyo de hardware, se pueden deshabilitar las
interrupciones de tal forma que ningún otro proceso o hilo
pueda ejecutarse hasta que las mismas interrupciones
sean rehabilitadas. Una posible propuesta de solución al
problema de exclusión mutua utilizando esta idea es:
wantToEnterCS(int i){ //pre-protocolo
inhabilitar interrupciones
}
finishedInCS(int i){ //post-protocolo
habilitar interrupciones
}
Soluciones (en hardware )
• No resulta aceptable que cualquier hilo deshabilite las
interrupciones o el bus interno del CPU, ya que en
principio nada garantiza que los procesos de usuario
estén bien programados, de tal forma que pueden
ocasionar una catástrofe si no se restauran
convenientemente.
• Esta estrategia debe restringirse a nivel del kernel
exclusivamente, ya que en ese nivel se pueden
deshabilitar las interrupciones por períodos cortos de
tiempo.
Soluciones mediante bloqueo
• Las propuestas de solución en software realizan una forma de espera ocupada: el hilo cede su tiempo de procesador restante y el despachador lo devuelve a la lista de hilos elegibles para ser ejecutados; sin embargo, esto tiene la desventaja de desperdiciar ciclos de procesador.
• En lugar de ello, en vista de lograr mejor desempeño, al hilo que deba esperar su turno para entrar a su SC se le puede colocar en una lista de espera en donde no sea elegible para usar el procesador pero pueda ser rehabilitado una vez que el recurso compartido haya sido liberado.
Soluciones mediante bloqueo
• Para ello pueden incorporarse una pareja de llamados al
sistema delay() y wakeup(): cuando un hilo invoque a
delay() el despachador lo colocará al final de la lista de
espera y por tanto su estado se cambiará de “listo” a
“bloqueado”.
• Cuando un hilo invoque wakeup() el hilo (si hay alguno)
que se encuentre al inicio de la lista de espera será
transferido a la lista de hilos elegibles por ser ejecutados
y su estado se cambiará a “listo”; si la lista se encuentra
vacía, wakeup() no tendrá efecto.
Soluciones mediante bloqueo
• Desafortunadamente, este enfoque es susceptible a padecer
condiciones de contención: si tenemos los siguientes
fragmentos de hilos
A: while (cond) delay(); B:wakeup();
• Supongamos que el hilo A se ejecuta y encuentra que el valor
de la condición es verdadero, en ese momento ocurre un
cambio de contexto (justamente antes que A invocara al
delay()) y el hilo B ejecuta su invocación a wakeup() la cual no
tiene efecto en A pues el cambio de contexto ocurrió
desafortunadamente antes, al continuar su ejecución A invoca
al delay() y entonces se queda esperando indefinidamente.
Soluciones mediante bloqueo
• Productor (P) y un consumidor (C) que trabajan sobre un buffer de tamaño finito.
//variables compartidas por el consumidor y el productor: int N, count=0;
producer(){
0: while(true){
1: produce_Elemento();
2: if (count == N) delay(); //el buffer esta lleno
3: introduce_en_buffer();
4: count++;
5: if (count == 1) wakeup();
}
}
consumer(){
0: while(true){
1: if (count == 0) delay(); //el buffer esta vacio
2: quita_Elemento();
3: introduce_en_buffer();
4: count--;
5: if (count == N-1) wakeup();
}
}
Soluciones mediante bloqueo
• Como puede apreciarse, si el buffer está vacío el
consumidor no puede consumir y se espera hasta que el
productor le avise, si hay al menos una ranura libre donde
coloque un elemento el productor, el consumidor se lo
indica (i.e. lo despierta). Por su parte, el productor se
bloquea cuando el buffer está lleno, y despierta al
consumidor si hay al menos un elemento en el buffer.
Soluciones mediante bloqueo
• No obstante, este código es susceptible a condiciones de contención: un hilo podría decidir bloquearse pero la señal que lo despierte se ha perdido debido a un desafortunado cambio de contexto: supongamos que las instrucciones que están a punto de ejecutar P y C son respectivamente 4 y 5, estando el valor de count=N.
• Supongamos que le toca el turno a ejecutarse a C, y ejecuta los enunciados 3 y 4 completos: decrementa count y emite una señal wakeup() la cual no tiene efecto pues ninguno está bloqueado aún; ocurre un cambio de contexto y P ejecuta los enunciados 4, (la cual modifica count y ahora tiene valor de N), 5, y le da tiempo de llegar hasta la 2: ahí se cumple la condición y queda bloqueado.
• C continúa consumiendo elementos hasta vaciar el buffer, por lo cual se bloquea (count =0). Por tanto, ocurre un deadlock.
Conclusión
• Podemos afirmar que para evitar el problema de deadlock
que potencialmente presentan las soluciones apoyadas
en bloqueos, se deben almacenar las señales wakeup();
para que no se pierdan.
• Justamente ese es el propósito de los siguientes
mecanismos de sincronización.