Programación Concurrente y de Tiempo Real

Transcripción

Programación Concurrente y de Tiempo Real
Programación Concurrente y de Tiempo Real
Guión de prácticas 6: Programación en Java de
algoritmos de control de la Exclusión Mutua con
variables comunes
Natalia Partera Jaime
Alumna colaboradora de la asignatura
Índice
1. Introducción
2
2. Búsqueda de un algoritmo de control de la
2.1. Primer intento . . . . . . . . . . . . . . . .
2.2. Segundo intento . . . . . . . . . . . . . . . .
2.3. Tercer intento . . . . . . . . . . . . . . . . .
2.4. Cuarto intento . . . . . . . . . . . . . . . .
2.5. Algoritmo de Dekker . . . . . . . . . . . . .
exclusión mutua
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
2
2
4
6
7
9
3. Variables volatile
11
4. Ejercicios
11
5. Soluciones de los ejercicios
5.1. Ejercicio 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
12
12
1
1.
Introducción
A veces al ejecutar procesos concurrentemente, puede aparecer el problema de la exclusión mutua. Esto
significa que varios de los procesos pueden pretender acceder a cierto recurso compartido a la misma
vez. Para conseguir que los procesos trabajen como está previsto y aporten el resultado esperado, los
ejecutaremos bajo la exclusión mutua controlada con variables comunes.
Para garantizar el éxito del programa, sus procesos deben satisfacer la condición de exclusión mutua.
Es decir, las instrucciones de cada proceso que hacen referencia al recurso crı́tico compartido (sección
crı́tica) no deben ejecutarse entrelazadas. Por lo que es necesario controlar la exclusión mutua de los
procesos. Para un número N de procesos, es necesario:
Identificar y diferenciar, dentro de cada proceso, entre su sección crı́tica y el resto del código.
Permitir que sólo un proceso pueda estar ejecutando su sección crı́tica a la vez. Para ello, cada
proceso debe ejecutar un pre-protocolo antes de entrar en su sección crı́tica y un post-protocolo al
salir.
Las condiciones para resolver este tipo de problemas son las mismas sean 2 procesos los que intervengan
o sean más. Tan sólo hay que adaptar el algoritmo al número de procesos que intervienen.
Uno de los algoritmos que solucionan el problema de la exclusión mutua es el algoritmo de Dekker. A
continuación, procedemos a explicarlo paso a paso. Para entenderlo mejor, lo explicaremos primero para
2 procesos.
2.
Búsqueda de un algoritmo de control de la exclusión mutua
Para buscar un algoritmo que controle la exclusión mutua, utilizaremos variables comunes a ambos
procesos, que podrán ser consultadas por todos ellos. Supondremos que sólo tenemos 2 procesos, aunque
puede generalizarse para N procesos.
2.1.
Primer intento
Ya hemos mencionado que para controlar la exclusión mutua es necesario que los procesos tengan un
pre-protocolo y un post-protocolo que se ejecuten antes y después de su sección crı́tica. Lo más sencillo
entonces es pensar en una variable global que indique a qué proceso le toca el turno.
Debemos recordar que cada hilo en Java tiene un nombre, o bien un nombre especı́fico proporcionado
por el programador, o bien un nombre genérico proporcionado por Java. Estos nombres proporcionados
por Java siguen la estructura Thread-x, donde x es un número consecutivo y único para cada hilo, por
lo que permiten ordenar los hilos siguiendo un orden definido.
Pues bien, podemos crear una variable entera global turno que indique si es el turno del hilo 0 o del
hilo 1. Cada hilo comprueba en su pre-protocolo si el número de la variable turno coincide con su número
de proceso, el expresado en el nombre generado automáticamente. Si as ası́, continua y ejecuta el código
de su sección crı́tica y el post-protocolo. Si no, entra en un bucle hasta que tenga permiso para ejecutar su
sección crı́tica. Cuando tras ejecutar su sección crı́tica, ejecute su post-protocolo, cambiará en él el valor
de la variable turno para que indique al otro hilo (o al siguiente en el caso de N procesos).
2
Otra opción serı́a distinguir el hilo por su identificador, que es único y no puede cambiar mientras
el hilo exista. Para ello habrı́a que almacenar los identificadores de los hilos en una estructura a la que
puedan acceder todos. Esto habrı́a que hacerlo desde el programa principal antes de lanzarlos. Esta forma
además nos permite cambiar los nombres de los hilos a nuestro antojo.
Veamos cómo quedarı́a su código:
/**
* Programa de prueba del intento 1.
*
* @author Natalia Partera
* @version 1.0
*/
public class PrimerIntento extends Thread
{
public static volatile long turno;
public static volatile long[] turnos;
public static volatile int indice = 0;
public void run() {
for(int i = 0; i < 10; ++i) {
//Antes del pre-protocolo
System.out.println(this.getName() + ": Antes del pre-protocolo.");
//Pre-protocolo
System.out.println(this.getName() + ": Pre-protocolo");
while (turno != this.getId()) {}
//Sección crı́tica
System.out.println(this.getName() + ": Sección crı́tica. Vuelta " + i);
//Post-protocolo
System.out.println(this.getName() + ": Post-protocolo");
if(indice == 1)
indice = 0;
else
++indice;
turno = turnos[indice];
}
}
public static void main(String[] args) throws InterruptedException
{
PrimerIntento t1 = new PrimerIntento();
PrimerIntento t2 = new PrimerIntento();
turnos = new long[2];
turnos[0] = t1.getId();
turnos[1] = t2.getId();
turno = turnos[indice];
3
t1.start();
t2.start();
t1.join();
t2.join();
}
}
Hay un problema en este modelo, y es que obliga a que haya una alternancia entre los 2 hilos, o que
se ejecuten en un cierto orden si son más de 2, antes de que el mismo hilo pueda volver a tener acceso
al recurso crı́tico. Además, si uno de los dos hilos muere o es eliminado, el otro proceso se bloquea en su
sección crı́tica. Estos problemas se deben a que los hilos comprueban y modifican el valor de una misma
variable, y tan sólo dependen de ella.
2.2.
Segundo intento
En esta ocasión, vamos a intentar solucionar el problema del intento anterior añadiendo más variables
de control. Ahora cada hilo tendrá su propia variable de control que reflejará si los demás pueden usar el
recurso crı́tico o no. Las variables de control seguirán siendo enteras, y ahora su valor se sustituirá por 0
y 1, o si lo prefiere, puede usar una variable booleana en su lugar.
Cuando un hilo quiere entrar en su sección crtı́ca, coloca su variable a 0, indicando a los demás hilos
que no intenten obtener el recurso crı́tico, que no pueden. Una vez que el hilo llega al post-protocolo coloca
su variable de control a 1, indicando que el recurso crı́tico está libre. Si un hilo desea ejecutar su sección
crı́tica, comprobará que todas las demás variables de control sean distintas de 0. Si no es ası́, espera.
Veamos cómo quedarı́a su código:
/**
* Programa de prueba del intento 2.
*
* @author Natalia Partera
* @version 1.0
*/
public class SegundoIntento extends Thread
{
public static volatile boolean[] permisos;
public void run() {
for(int i = 0; i < 10; ++i) {
//Antes del pre-protocolo
System.out.println(this.getName() + ": Antes del pre-protocolo.");
//Pre-protocolo
System.out.println(this.getName() + ": Pre-protocolo");
String name = this.getName();
name = name.replace("Thread-", "");
int numThread = Integer.parseInt(name);
int otroThread;
if (numThread == 0)
4
otroThread = 1;
else
otroThread = 0;
System.out.println(this.getName() + ": permisos[otroThread] = " +
permisos[otroThread]);
while (permisos[otroThread] == false) {
System.out.println(this.getName() + ": En el bucle, permisos[" +
otroThread + "] = " + permisos[otroThread]);
}
permisos[numThread] = false;
//Sección crı́tica
System.out.println(this.getName() + ": Sección crı́tica. Vuelta " + i);
//Post-protocolo
System.out.println(this.getName() + ": Post-protocolo");
permisos[numThread] = true;
}
}
public static void main(String[] args) throws InterruptedException
{
SegundoIntento t1 = new SegundoIntento();
SegundoIntento t2 = new SegundoIntento();
permisos = new boolean[2];
for (int i = 0; i < 2; ++i)
permisos[i] = true;
t1.start();
t2.start();
t1.join();
t2.join();
}
}
Desgraciadamente, este intento no cumple con el requerimiento de exclusión mutua. Puede darse el
caso de que los hilos se intercalen al comprobar las demás variables de control y al entrar luego en su
sección crı́tica y que al final acaben los dos hilos ejecutando a la vez su sección crı́tica. Es decir, puede
pasar lo siguiente:
1. P1 comprueba C2 y encuentra que C2 = 1.
2. P2 comprueba C1 y encuentra que C1 = 1.
3. P1 pone C1 a 0.
4. P2 pone C2 a 0.
5. Ambos procesos entran en sus secciones crı́ticas.
5
2.3.
Tercer intento
Para evitar la situación anterior, se puede cambiar el orden en el que se espera en el bucle y se cambia
la variable de control. Para que no obtengan dos hilos el permiso para entrar, lo que se hace ahora es
que primero los hilos declaran su intención (cambiando su variable) y luego esperan a poder entrar en la
sección crı́tica. De este modo se asegura que solamente un hilo entre en su sección crı́tica a la vez.
Cuando un hilo llega a su pre-protocolo anuncia su intención de ejecutar su sección crı́tica cambiando
el valor de su variable de control a 0. Luego el hilo se queda esperando en un bucle hasta que las demás
variables de control valen 1. En ese momento el hilo entra en su sección crı́tica. Después, en su postprotocolo, cambia el valor de su variable de control a 1 para indicar que ha dejado libre el recurso crı́tico.
Veamos cómo quedarı́a su código:
/**
* Programa de prueba del intento 3.
*
* @author Natalia Partera
* @version 1.0
*/
public class TercerIntento extends Thread
{
public static volatile boolean[] permisos;
public void run() {
for(int i = 0; i < 10; ++i) {
//Antes del pre-protocolo
System.out.println(this.getName() + ": Antes del pre-protocolo.");
//Pre-protocolo
System.out.println(this.getName() + ": Pre-protocolo");
String name = this.getName();
name = name.replace("Thread-", "");
int numThread = Integer.parseInt(name);
int otroThread;
if (numThread == 0)
otroThread = 1;
else
otroThread = 0;
permisos[numThread] = false;
while(permisos[otroThread] == false) {}
//Sección crı́tica
System.out.println(this.getName() + ": Sección crı́tica. Vuelta " + i);
//Post-protocolo
System.out.println(this.getName() + ": Post-protocolo");
permisos[numThread] = true;
}
}
6
public static void main(String[] args) throws InterruptedException
{
TercerIntento t1 = new TercerIntento();
TercerIntento t2 = new TercerIntento();
permisos = new boolean[2];
for (int i = 0; i < 2; ++i)
permisos[i] = true;
t1.start();
t2.start();
t1.join();
t2.join();
}
}
Con este código hemos conseguido que sólo un hilo ejecute su sección crı́tica a la vez. Sin embargo, el programa puede bloquearse si algunas instrucciones de distintos hilos se ejecutan entrelazadas y
concurrentemente:
1. P1 pone C1 a 0.
2. P2 pone C2 a 0.
3. P1 comprueba C2 y encuentra que C2 = 0, por lo que permanece en el bucle.
4. P2 comprueba C1 y encuentra que C1 = 0, por lo que permanece en el bucle.
Esta secuencia demuestra cómo pueden bloquearse los hilos tras declarar su intención de ejecutar su
sección crı́tica pero sin poder entrar en ella.
2.4.
Cuarto intento
Este intento continua el razonamiento del anterior, y obliga a un hilo que abandone su intención de
ejecutar su sección crı́tica si descubre que puede existir un estado de contención con otro proceso, donde
se bloquean mutuamente.
Para obligar a un hilo que abandone su intención momentaneamente por si existe contención, se cambia
el valor de su variable de control dos veces mientras que está en el bucle esperando. De este modo si
está esperando y otro hilo desea ejecutar su sección crı́tica también, al poner su variable de control a 1,
el otro ejecuta su sección crı́tica. Luego vuelve a cambiar el valor de su variable de control para volver a
indicar su intención de entrar en su sección crı́tica.
Veamos cómo quedarı́a su código:
/**
* Programa de prueba del intento 4.
*
* @author Natalia Partera
* @version 1.0
*/
7
public class CuartoIntento extends Thread
{
public static volatile boolean[] permisos;
public void run() {
for(int i = 0; i < 10; ++i) {
//Antes del pre-protocolo
System.out.println(this.getName() + ": Antes del pre-protocolo.");
//Pre-protocolo
System.out.println(this.getName() + ": Pre-protocolo");
String name = this.getName();
name = name.replace("Thread-", "");
int numThread = Integer.parseInt(name);
int otroThread;
if (numThread == 0)
otroThread = 1;
else
otroThread = 0;
permisos[numThread] = false;
while(permisos[otroThread] == false) {
permisos[numThread] = true;
permisos[numThread] = false;
}
//Sección crı́tica
System.out.println(this.getName() + ": Sección crı́tica. Vuelta " + i);
//Post-protocolo
System.out.println(this.getName() + ": Post-protocolo");
permisos[numThread] = true;
}
}
public static void main(String[] args) throws InterruptedException
{
CuartoIntento t1 = new CuartoIntento();
CuartoIntento t2 = new CuartoIntento();
permisos = new boolean[2];
for (int i = 0; i < 2; ++i)
permisos[i] = true;
t1.start();
t2.start();
t1.join();
t2.join();
}
}
Con este algoritmo tenemos garantizada la exclusión mutua. Sin embargo puede dar lugar a procesos
ansiosos. Veamos una secuencia en la que esto puede ocurrir:
8
1. P1 pone C1 a 0.
2. P2 pone C2 a 0.
3. P2 comprueba C1 y encuentra que C1 = 0, por lo que permanece en su bucle y cambia C2 a 1.
4. P1 entra en su sección crı́tica y su post-protocolo, donde cambia C1 a 1.
5. P2 sigue en el bucle y cambia C2 a 0.
6. En este punto, si P1 no desea volver a entrar en su sección crı́tica, P2 puede por fin entrar. Pero
si P1 entra en su pre-protocolo y vuelve a solicitar entrar en su sección crı́tica, P2 se encontrarı́a
atrapado en el bucle.
Otro defecto del algoritmo es que puede provocar un livelock. Esto puede ocurrir cuando ambos hilos
se bloqueen y se sigan ejecutando concurrente y entrelazadamente, cambiando ambos los valores de sus
variables de control pero sin llegar a ejecutar su sección crı́tica ninguno de los dos:
1. P1 pone C1 a 0.
2. P2 pone C2 a 0.
3. P2 comprueba C1 y encuentra que C1 = 0, por lo que permanece en el bucle y cambia C2 a 1.
4. P2 sigue en el bucle y cambia C2 a 0.
5. P1 comprueba C2 y encuentra que C2 = 0, por lo que permanece en el bucle y cambia C1 a 1.
6. P1 sigue en el bucle y cambia C1 a 0.
7. P2 comprueba C1 y encuentra que C1 = 0, por lo que permanece en el bucle.
8. P1 comprueba C2 y encuentra que C2 = 0, por lo que permanece en el bucle.
2.5.
Algoritmo de Dekker
El problema del último intento es que por no querer producir un bloqueo, no insiste el hilo lo suficiente
en su derecho a usar el recurso crı́tico. Si lo combinamos con el primer intento, obligará a que alguno de
los hilos entre en su sección crı́tica en el caso de que los dos se estén cediendo el recurso crı́tico entre ellos
mutuamente, es decir, si hay contención.
Primero un hilo anuncia su deseo de entrar en la sección crı́tica, por ejemplo el hilo 1. Pero antes
de entrar en la sección crı́tica comprueba si tiene derecho a insistir en ello, comprobando el valor de la
variable turno. Si no tiene derecho, desactiva el anuncio de entrada en la sección crı́tica, poniendo su
variable de control (C1 ) a 1, y quedando en espara hata que reciba el turno. Cuando el otro hilo (hilo
2) completa su sección crı́tica, el hilo 1 recibe el turno. En ese momento, el hilo 1 vuelve a ajustar su
variable de control a 0 y entra en su sección crı́tica. Al salir de la sección crı́tica, cambia el valor de su
variable de control y le devuelve el turno al otro hilo.
9
Veamos cómo quedarı́a su código:
/**
* Programa de prueba del intento 5: algoritmo de Dekker.
*
* @author Natalia Partera
* @version 1.0
*/
public class Dekker extends Thread
{
public static volatile long turno;
public static volatile long[] turnos;
public static volatile int indice = 0;
public static volatile boolean[] permisos;
public void run() {
for(int i = 0; i < 10; ++i) {
//Antes del pre-protocolo
System.out.println(this.getName() + ": Antes del pre-protocolo.");
//Pre-protocolo
System.out.println(this.getName() + ": Pre-protocolo");
String name = this.getName();
name = name.replace("Thread-", "");
int numThread = Integer.parseInt(name);
int otroThread;
if (numThread == 0)
otroThread = 1;
else
otroThread = 0;
permisos[numThread] = false;
while(permisos[otroThread] == false) {
if(turno != this.getId()) {
permisos[numThread] = true;
while(turno != this.getId()) {}
permisos[numThread] = false;
}
}
//Sección crı́tica
System.out.println(this.getName() + ": Sección crı́tica. Vuelta " + i);
//Post-protocolo
System.out.println(this.getName() + ": Post-protocolo");
permisos[numThread] = true;
if(indice == 1)
indice = 0;
else
++indice;
turno = turnos[indice];
}
10
}
public static void main(String[] args) throws InterruptedException
{
Dekker t1 = new Dekker();
Dekker t2 = new Dekker();
turnos = new long[2];
turnos[0] = t1.getId();
turnos[1] = t2.getId();
turno = turnos[indice];
permisos = new boolean[2];
for (int i = 0; i < 2; ++i)
permisos[i] = true;
t1.start();
t2.start();
t1.join();
t2.join();
}
}
El algoritmo de Dekker es correcto y satisface los requerimientos de exclusión mutua y ausencia de
bloqueos. Ningún hilo puede volverse ansioso y en ausencia de contención, un proceso entra en su sección
crı́tica inmediatamente si lo desea.
3.
Variables volatile
Como habrá observado en los ejemplos anteriores, las variables globales han sido definidas como
volatile. Esta palabra reservada puede usarse en tipos primitivos o en objetos.
Si una variable es volatile el compilador está obligado a leer y escribir su valor en memoria cada
vez que sea accedida, en lugar de hacerlo desde o sobre la memoria caché. Esto permite que una variable
global sea accedida por distintos hilos que modifiquen su valor y que sea sincronizada automáticamente
en cada acceso.
4.
Ejercicios
Ejercicio 1 Implemente el algoritmo de la panaderı́a de Lamport para dos variables. Compı́lelo varias
veces para comprobar que funciona correctamente. Puede comparar su código con el que se encuentra en
5.1.
11
5.
Soluciones de los ejercicios
En esta sección encontrará las soluciones a los ejercicios propuestos a lo largo del guión.
5.1.
Ejercicio 1
A continuación puede ver la implementación del algoritmo de Lamport para 2 hilos:
/**
* Programa de prueba del algoritmo de Lamport.
*
* @author Natalia Partera
* @version 1.0
*/
public class Lamport extends Thread
{
public static volatile int[] turnos;
public void run() {
for(int i = 0; i < 10; ++i) {
//Antes del pre-protocolo
System.out.println(this.getName() + ": Antes del pre-protocolo.");
//Pre-protocolo
System.out.println(this.getName() + ": Pre-protocolo");
String name = this.getName();
name = name.replace("Thread-", "");
int numThread = Integer.parseInt(name);
int otroThread;
if (numThread == 0)
otroThread = 1;
else
otroThread = 0;
turnos[numThread] = 1;
turnos[numThread] = turnos[otroThread] + 1;
while (turnos[otroThread] != 0 && turnos[numThread] > turnos[otroThread]) {}
//Sección crı́tica
System.out.println(this.getName() + ": Sección crı́tica. Vuelta " + i);
//Post-protocolo
System.out.println(this.getName() + ": Post-protocolo");
turnos[numThread] = 0;
}
}
public static void main(String[] args) throws InterruptedException
{
Lamport t1 = new Lamport();
Lamport t2 = new Lamport();
12
turnos = new int[2];
turnos[0] = 0;
turnos[1] = 0;
t1.start();
t2.start();
t1.join();
t2.join();
}
}
13

Documentos relacionados