Unidad 3 Memoria común

35
UNIDAD 3 MEMORIA COMÚN El problema de exclusión mutua

Transcript of Unidad 3 Memoria común

Page 1: Unidad 3 Memoria común

UNIDAD 3 MEMORIA

COMÚN

El problema de exclusión mutua

Page 2: Unidad 3 Memoria común

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.

Page 3: Unidad 3 Memoria comú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*/

}

Page 4: Unidad 3 Memoria común

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.

Page 5: Unidad 3 Memoria comú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.

Page 6: Unidad 3 Memoria común

Implementación de la exclusión mutua

• Por procesador

• Por memoria

Page 7: Unidad 3 Memoria común

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.

Page 8: Unidad 3 Memoria común

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.

Page 9: Unidad 3 Memoria común

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.

Page 10: Unidad 3 Memoria común

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*/

}

Page 11: Unidad 3 Memoria común

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

Page 12: Unidad 3 Memoria común

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:

Page 13: Unidad 3 Memoria común

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);

}

}

Page 14: Unidad 3 Memoria común

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.

Page 15: Unidad 3 Memoria común

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;

}

Page 16: Unidad 3 Memoria común

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.

Page 17: Unidad 3 Memoria común

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;

}

Page 18: Unidad 3 Memoria común

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.

Page 19: Unidad 3 Memoria común

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;

}

Page 20: Unidad 3 Memoria común

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.

Page 21: Unidad 3 Memoria común

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);

}

Page 22: Unidad 3 Memoria común

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.

Page 23: Unidad 3 Memoria común

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;

}

Page 24: Unidad 3 Memoria común

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.

Page 25: Unidad 3 Memoria común

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;

}

Page 26: Unidad 3 Memoria común

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;

}

Page 27: Unidad 3 Memoria común

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

}

Page 28: Unidad 3 Memoria común

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.

Page 29: Unidad 3 Memoria común

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.

Page 30: Unidad 3 Memoria común

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.

Page 31: Unidad 3 Memoria común

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.

Page 32: Unidad 3 Memoria común

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();

}

}

Page 33: Unidad 3 Memoria común

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.

Page 34: Unidad 3 Memoria común

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.

Page 35: Unidad 3 Memoria común

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.