FCC Exclusión Mutua

Anuncio
Programación Concurrente
CAPITULO 4. EXCLUSION MUTUA
A continuación se presentan soluciones al problema de la exclusión mutua de
procesos en sistemas con memoria común. Como es sabido, en estos sistemas
dos o más procesos ejecutándose concurrentemente pueden pretender acceder a
un mismo recurso compartido al mismo tiempo, situación que se conoce como
problema de la exclusión mutua. De este intento de acceso pueden derivarse
problemas indeseables.
Con objeto de evitar estos problemas, definimos el segmento de código de un
proceso donde este pide un recurso compartido como una sección crítica (ver
Figura 4.1).
Figura 4.1 Modelo de proceso concurrente.
El objetivo puede ahora reformularse: se pretende evitar que dos o más procesos
concurrentes se encuentren ejecutando su sección crítica al mismo tiempo.
Una de las soluciones al problema de la exclusión mutua propuestas en la
literatura en el conocido algoritmo de Dekker. Se analizará este algoritmo
proponiendo previamente al mismo varias soluciones que demostrarán ser
incorrectas, hasta llegar a él. En ocasiones se insistirá en la demostración formal
de la incorrección de los algoritmos. Igualmente veremos la extensión de la
exclusión mutua a N procesos en base a los algoritmos de Lamport y Peterson.
BUAP FCC.
Mary carmen Cerón Garnica
53
Programación Concurrente
4.1. El problema de la exclusión mutua para N procesos
Puede ser expuesto mediante la siguiente relación de condiciones, que nos
proporcionan una abstracción teórica sobre la cual comenzar a construir nuestras
propuestas de solución y realizar el análisis de las mismas.
1. N procesos se encuentran ejecutando un lazo idealmente infinito de
instrucciones, que podemos considerar dividido en dos partes: la sección
crítica y el resto. Los programas deben satisfacer la condición de exclusión
mutua: las instrucciones incluidas en las secciones críticas de dos o más
procesos no deben ejecutarse entrelazadas.
2. Evidentemente, la única manera de conseguir esto es permitir que sólo
un proceso se encuentre ejecutando su sección crítica, mientras los demás
esperan para ejecutarla a que éste termine. Nuestras soluciones irán
encaminadas a conseguir este objetivo. Todo proceso que desee ejecutar
su sección crítica deberá antes de entrar en ella ejecutar un pre-protocolo
que impide a cualquier otro proceso ejecutar la suya, y un post-protocolo al
salir que libera a otro proceso para ejecutarla. La implantación de estos
protocolos requerirá en general variables adicionales en el código
Además:
1. Un proceso puede pararse por cualquier motivo en zona no crítica de
instrucciones, pero no le estará permitido parar durante la ejecución de los
protocolos o de la sección crítica1.
2. El programa tampoco debe bloquearse. Si varios procesos del programa
están intentando entrar en sus secciones críticas, no se producirá un
bloqueo, sino que alguno de ellos conseguirá entrar en ella eventualmente.
No obstante, si ninguno de los procesos tiene éxito en pasar del preprotocolo a la sección crítica, diremos que el programa se ha bloqueado.
3. Tampoco debe haber permanencia indefinida de ninguno de los procesos
en su pre-protocolo, sino que en algún instante cada proceso tendrá éxito
en el acceso a su región crítica. Es decir, los procesos no estarán ansiosos.
BUAP FCC.
Mary carmen Cerón Garnica
54
Programación Concurrente
4. En ausencia de contenciones (circunstancias que lo retengan), un único
proceso que desee entrar en su sección crítica tendrá éxito y entrará en
ella.
Un punto muy importante a tener en cuenta es el hecho de que las
soluciones que propondremos asumen que las instrucciones de lectura y
escritura en la memoria común son atómicas2. Sintácticamente indicaremos
la lectura de una variable común poniendo la variable en la parte derecha
de una sentencia de asignación o en una sentencia condicional.
4.1.1. Primer Intento de solución
Se define una variable global turno que puede tomar dos valores, uno y dos,
puesto que vamos a tener dos procesos en ejecución concurrente. Inicialmente
damos a la variable el valor uno. El objetivo es indicar con esta variable qué
proceso tiene derecho a entrar en su sección crítica, y qué proceso no puede
hacerlo. Los procesos ejecutarán un pre-protocolo donde comprobarán si el valor
de la variable de turno coincide con su número de proceso.
Si es así pasarán a ejecutar su sección crítica y tras ella un post-protocolo donde
modificarán la variable de turno para dar acceso a su sección crítica a otro
proceso.
En caso contrario, entrarán en un lazo y continuarán en él hasta que tengan
derecho a entrar en su sección crítica.
BUAP FCC.
Mary carmen Cerón Garnica
55
Programación Concurrente
Analizamos a continuación la bondad de la solución propuesta mediante el
establecimiento y prueba de varios teoremas.
Proposición 1. Esta solución satisface el requerimiento de exclusión mutua.
Demostración: Supongamos que no es así, y tratemos de derivar una
contradicción.
En este caso, en algún momento ambos procesos estarán al mismo tiempo en sus
secciones críticas. Supongamos que P1 entró antes4 que P2. Esto es, P1 entró en
tiempo t1,P2 entró en tiempo t2 y t1< t2. Si ello es así, en tiempo t1 se tuvo que
turno = 1 e igualmente en tiempo t2, turno = 2. Pero durante el intervalo de t1 a t2
el proceso P1 permaneció en su sección crítica y no ejecutó el post-protocolo, por
lo cual la variable de turno no pudo cambiar su valor a dos, y en consecuencia el
segundo proceso no está en su sección crítica. Esto es una contradicción, luego la
suposición inicial era errónea.¤
Si estudiamos detenidamente la demostración efectuada, ¿Qué ocurre con las
diferentes aserciones teóricas del modelo de concurrencia que tenemos
propuesto? ¿Rompe el entrelazado de instrucciones la exclusión mutua? Hemos
probado que no.
Proposición 2. La solución no da lugar al bloqueo del programa.
Demostración: Para que haya bloqueo, ambos procesos deben ejecutar el test
sobre la variable de turno y permanecer en el lazo del pre-protocolo por ser la
condición de acceso falsa. Pero si el primer proceso no puede entrar, es que turno
= 2 y si el segundo tampoco puede hacerlo, es que turno = 1 lo cual no es
posible.¤
Proposición 3. La solución no da lugar a procesos ansiosos.
Demostración: Para que exista esta situación, algún proceso como P1 debe entrar
en su sección crítica mientras los otros permanecen estancados en el preprotocolo de acceso a esta. Pero si esto es así, P1 modificará el valor de turno en
su post-protocolo, liberando del pre-protocolo a cualquier otro proceso, que tendrá
por tanto éxito en el acceso a su sección crítica.
Proposición 4. La solución falla en ausencia de contención.
Demostración: Si el proceso P2 para en su zona no crítica de código, P1 podrá
ejecutar su sección crítica como máximo una vez. Después de ella, ajustará el
valor de turno a dos, y no podrá volver a ejecutarla.
Este último teorema muestra que no hemos resuelto el problema de la exclusión
mutua, puesto que una de las condiciones requeridas no se da. Incluso aunque
BUAP FCC.
Mary carmen Cerón Garnica
56
Programación Concurrente
ninguno de los procesos se parase, si un proceso entra varias veces por segundo
en su sección crítica, y otra una vez cada hora, la solución sigue sin ser práctica.
Ello es así porque la hemos basado en una técnica de programación conocida
como co-rutinas, que pasa el derecho de ejecución de la sección crítica de un
proceso en forma explícita, y que funciona bien si un problema sencillo es resuelto
por varios módulos trabajando juntos. Los problemas en que trabajamos pueden
involucrar a procesos trabajando en problemas independientes o sobre diferentes
aspectos de un mismo problema, y por tanto con un nivel de acoplamiento débil
4.1.2 Segundo intento de solución
La solución previa falla porque a ambos procesos se les permite comprobar y
ajustar el valor de la misma variable. Además, si uno de los procesos muere, o es
eliminado del sistema por alguna circunstancia, el otro proceso se bloquea en su
sección crítica. Vamos a corregir la situación dando a cada proceso su propia
variable Ci, con la siguiente interpretación: si el proceso Pi desea entrar en su
sección crítica, hace Ci = 0 la ejecuta y hace Ci = 1 cuando sale de ella. Si
mientras tanto algún otro proceso desea entrar en su sección crítica comprueba
que las variables de los demás procesos sean distintas de cero, y espera en caso
de que no ocurra así. Sin embargo, la solución propuesta no satisface el
requerimiento de exclusión mutua.
La solución en código es la siguiente:
BUAP FCC.
Mary carmen Cerón Garnica
57
Programación Concurrente
Veamos por qué esta solución no satisface el requerimiento de exclusión mutua.
Proposición 5. P1 y P2 pueden ejecutar sus secciones críticas simultáneamente,
quebrantando el requerimiento de exclusión mutua.
Demostración: La siguiente secuencia de instrucciones entrelazadas de ambos
procesos lleva a la rotura de la exclusión mutua:
1. P1 comprueba C2 y encuentra que C2 = 1.
2. P2 comprueba C1 y encuentra que C1 = 1.
3. P1 pone C1 a cero.
4. P2 pone C2 a cero.
5. Ambos procesos entran en sus secciones críticas.¤
Es muy importante estudiar las diferencias entre esta demostración y las del
primer intento. Puesto que aquí pretendemos demostrar un requerimiento
fundamental, se requeriría demostrar la corrección de todas las posibles
secuencias entrelazadas de instrucciones. Como son infinitas, necesitaríamos
argumentos matemáticos. Aquí, como pretendemos probar la falsedad del
requerimiento, basta dar una secuencia concreta que sirva como contraejemplo.
BUAP FCC.
Mary carmen Cerón Garnica
58
Programación Concurrente
4.1.3. Tercer intento de solución
En el segundo intento de solución se introducían las variables Ci las cuales
intentaban indicar a los procesos en ejecución concurrente que un proceso
concreto Pi estaba en su sección crítica. No obstante, una vez que un proceso
había completado con éxito su lazo de control de entrada, no podía de ninguna
forma ser prevenido de que otro proceso había ya entrado en su sección crítica.
Esto, como vimos, llevaba a violar el requerimiento de exclusión mutua.
Ello ocurría porque el lazo debería ejecutarse habiendo sido avisado previamente
por cualquier otro proceso de su intención de entrar a sección crítica. En este
intento conseguimos esto desplazando la sentencia de asignación de valor a Ci
donde se indica esto fuera del lazo.
BUAP FCC.
Mary carmen Cerón Garnica
59
Programación Concurrente
Sin embargo, el programa puede bloquearse después de la ejecución concurrente
y entrelazada de algunas instrucciones.
Proposición 6. La solución puede bloquearse.
Demostración: Basta considerar para probarlo la siguiente secuencia de
instrucciones:
1. P1 asigna 0 a C1.
2. P2 asigna 0 a C2.
3. P1 comprueba C2 y permanece en el bucle.
4. P2 comprueba C1 y permanece en el bucle.
Nos encontramos pues en una situación en que dos procesos desean entrar en
una sección crítica pero ninguno tiene éxito en conseguirlo.
4.1.4. Cuarto intento de solución
En el intento anterior, cuando un proceso Pi ajusta a cero la variable Ci, no solo
indicaba su intención de entrar en la sección crítica, sino que insistía en su
derecho para hacerlo. Si dos procesos insistían simultáneamente en este derecho,
llegábamos a un bloqueo.
El cuarto intento pretende remediar el problema requiriendo a un proceso que
abandone su intención de entrar en su sección crítica si descubre que puede
existir un estado de contención con otro proceso.
La secuencia de asignaciones {C1:= 1;C1:= 0} es significativa en un programa
concurrente, pero indiferente en un programa secuencial.
Por ello, el proceso P2 puede ejecutar ahora un número arbitrario de instrucciones
entrelazadas con las dos asignaciones de C1. En este caso, cuando C1 abandona
el intento de entrar en su sección crítica ajustando C1 a 1, el proceso P2 puede
ejecutar el bucle y entrar con éxito en su sección crítica.
Esta solución satisface claramente el requerimiento de exclusión mutua, por lo
mismo que la anterior también lo cumplía. Sin embargo, puede dar lugar a
procesos ansiosos, y a bloqueos livelock, que se producen cuando existen
secuencias de computación que pueden tener éxito, pero existen otras que llevan
a uno o más procesos a no entrar en su sección crítica. Veamos primeramente su
código, para luego pasar a un análisis del mismo.
BUAP FCC.
Mary carmen Cerón Garnica
60
Programación Concurrente
Proposición 7. Un proceso puede estar ansioso.
Demostración: puesto que la ejecución entrelazada de instrucciones es posible,
entre dos asignaciones a la variable C2, el proceso P1 puede completar un ciclo
completo de su lazo externo, y hacer un intento de entrar en su sección crítica
justo cuando P2 también lo hace.
1. P1 asigna 0 a C1.
2. P2 asigna 0 a C2.
3. P2 comprueba C1 y entonces ajusta C1 a 1.
4. P1 efectúa un ciclo completo.
5. P2 ajusta C2 a 0.
BUAP FCC.
Mary carmen Cerón Garnica
61
Programación Concurrente
Ello lleva a una situación en la que P1 entra un número infinito de veces en su
sección crítica, mientras que P2 permanece siempre ejecutando su pre-protocolo.
Proposición 9. La solución puede llevar a un livelock.
Demostración: Podemos considerar una secuencia de ejecución que alterna
perfectamente instrucciones de ambos procesos, que lleva a un bloqueo
(deadlock). Sin embargo, la más ligera desviación de esa secuencia permitiría que
un proceso entrase en la sección crítica, y por ello se halla presente un livelock.
Búsquese como ejercicio tal secuencia.
De lo visto hasta ahora debería quedar claro que en un sistemade cómputo real:
a) la conservación de la exclusión mutua y la libertad de bloqueos son
requerimientos irrenunciables.
b) Los procesos ansiosos y los bloqueos livelock son tolerables si su
probabilidad de aparición es despreciable.
Por tanto, el diseñador de un sistema deberá evaluar cómo de probables son la
ansiedad y la aparición de bloqueos livelock en el sistema, validando una solución
si esta probabilidad es muy pequeña pero a cambio cumple de manera estricta la
primera de las condiciones expuestas.
4.1.5. El algoritmo de Dekker
Este algoritmo es una combinación del primer y el cuarto intento de solución.
Mientras que el primer intento pasaba en forma explícita el derecho a entrar en
la sección crítica mediante el uso de la variable de turno, pero fallaba en ausencia
de contenciones, en el cuarto cada proceso tenía su propia variable para evitar los
problemas en ausencia de contención, pero en presencia de ella ningún proceso
tenía derecho a insistir para entrar en su sección crítica.
El algoritmo de Dekker funciona igual que la cuarta solución, excepto que
incorpora el derecho explícito de insistir para entrar en la sección crítica. Las
variables individuales de los procesos permitirán conseguir la exclusión mutua,
pero se usará una variable de turno común a ambos procesos, de manera que en
presencia de contención entre en su sección crítica el proceso que tenga el turno.
Un análisis cuidadoso del pseudocódigo del algoritmo de Dekker muestra
claramente la situación. El proceso P1 anuncia su deseo de entrar en sección
crítica. Sin embargo, antes de hacerlo comprueba si tiene derecho a insistir en
ello, comprobando el valor de la variable de turno. Si no tiene derecho, desactiva
el anuncio de entrada en sección crítica haciendo C1:=1 y queda en espera hasta
que recibe el turno, cosa que ocurre cuando P2 completa su sección crítica. En
ese momento, vuelve a ajustar la variable a cero y entra en la sección crítica. Al
salir de ella hace C1:=1 y devuelve el turno al proceso P2.
BUAP FCC.
Mary carmen Cerón Garnica
62
Programación Concurrente
El algoritmo de Dekker es correcto, y satisface los requerimientos de exclusión
mutua y ausencia de bloqueos. Ningún proceso puede volverse ansioso, y en
ausencia de contención un proceso entra en su sección inmediatamente si lo
desea.
BUAP FCC.
Mary carmen Cerón Garnica
63
Programación Concurrente
4.2. Exclusión mutua de N procesos
Como hemos visto, el algoritmo de Dekker resuelve el problema de la exclusión
mutua para dos procesos. No obstante, es posible diseñar métodos que la
generalicen a N procesos en ejecución concurrente. Estas soluciones no son
utilizadas en la práctica debido a su relativa complejidad, pero sobre todo a la
existencia de soluciones más simples basadas en el hardware. No obstante
damos un ejemplo de solución utilizando el algoritmo de la panadería de Lamport.
Tal y como lo presentamos, el algoritmo no es muy práctico. No obstante, las
ideas subyacentes han sido utilizadas en algoritmos más avanzados.
En este algoritmo cada proceso que desea ejecutar su sección crítica toma un
número. El proceso que tiene el número menor es el que accede a su sección
crítica. De ahí el nombre del algoritmo.
Se presenta a continuación como paso previo el algoritmo de la panadería para
dos procesos.
BUAP FCC.
Mary carmen Cerón Garnica
64
Programación Concurrente
Proposición 10. El algoritmo de la panadería satisface el requerimiento de
exclusión mutua.
Demostración: Inténtela el lector trabajando sobre las variables.
Una extensión de este algoritmo a N procesos puede construirse fácilmente. Cada
proceso escoge un número mayor que todos los números que ya han sido
escogidos.
Se utiliza un array choosing donde se indica a otros procesos que un proceso
concreto está escogiendo su número. Al igual que antes, un proceso entra en su
sección crítica cuando su número es el más bajo. Se utiliza un segundo array
number donde se mantienen los números escogidos por los procesos. El esquema
del proceso genérico Pi es
Proposición 11. El algoritmo de la panadería para N procesos satisface el
requerimiento de exclusión mutua.
BUAP FCC.
Mary carmen Cerón Garnica
65
Programación Concurrente
Demostración: Se deja como ejercicio al lector, bastando para efectuarla el utilizar
un argumento de reducción al absurdo y suponiendo a dos procesos dentro de sus
secciones críticas, tratando entonces de derivar una contradicción.
4.2.1 El algoritmo de Peterson
Otro medio de conseguir la exclusión mutua generalizada a N procesos es utilizar
el algoritmo propuesto por Peterson, que utiliza variables comunes a los procesos
concurrentes que solo pueden tomar un número acotado de valores. El algoritmo
de Peterson desarrollado se especifica a continuación.
No procederemos a desarrollar el conjunto de teoremas que prueben la bondad
del algoritmo de Peterson, prefiriendo dejar esta tarea al lector, por cuanto todos
ellos siguen esquemas parecidos a resultados previamente probados con otros
algoritmos.
BUAP FCC.
Mary carmen Cerón Garnica
66
Programación Concurrente
Nótese no obstante que la prueba de la exclusión mutua en el algoritmo pasaría
por un esquema de reducción al absurdo, resultado de suponer a ambos procesos
en sus secciones críticas al mismo tiempo (exclusión mutua rota), y tratar de
derivar una contradicción entre variables.
4.2.2. Exclusión mutua mediante hardware
Como ya hemos visto, bajo un modelo en el cual las instrucciones individuales
pueden entrelazarse individualmente, el conseguir la exclusión mutua es difícil. La
dificultad desaparece cuando las instrucciones de acceso a memoria son
atómicas.
Un ejemplo de instrucción de esta clase es la instrucción Test and Set, cuya
ejecución es equivalente a la de las dos siguientes instrucciones, sin permitir
entrelazado entre ellas.
Li:=C;
C:=1;
Aquí C es una variable global que inicialmente tiene valor 0. Cualquier proceso
puede acceder a ella. Li es una variable local a un proceso Pi. El problema de la
exclusión mutua en la ejecución de las secciones críticas de N procesos puede
ahora ser resuelto a entera satisfacción con el siguiente programa:
Otra instrucción válida para implementar un protocolo de exclusión mutua válido
es Exchange (A,B) que intercambia el contenido de las variables A y B, y cuya
ejecución es equivalente a la realización en forma atómica de la siguiente
secuencia de instrucciones:
Temp := A
A := B;
B := Temp;
BUAP FCC.
Mary carmen Cerón Garnica
67
Programación Concurrente
Un ejemplo de algoritmo que proporciona la exclusión mutua sobre N procesos
utilizando este mecanismo es el siguiente:
El lector puede por sí mismo comprobar con toda facilidad que ambos métodos
logran la exclusión mutua requerida sobre las secciones críticas.
4. 3. Semáforos
Los algoritmos de las secciones previas se ejecutaban sobre una máquina
desnuda, en el sentido de que usaban sólo las instrucciones máquina
proporcionadas por la computadora.
Si bien hemos conseguido implementar con ellos soluciones al problema de la
exclusión mutua, son de muy bajo nivel para ser eficientes y rentables.
Estudiaremos ahora los semáforos, que son mecanismos que proporcionan
primitivas de control de la concurrencia a un nivel superior al de las instrucciones
máquina. En general, estas estructuras aparecen implementadas como llamadas
al sistema operativo.
Un semáforo es una variable S entera que toma valores no negativos, y sobre la
cual aparecen definidas dos operaciones:
1. Wait(S) Si S > 0 entonces S := S − 1. En caso contrario, se suspende la
ejecución del proceso y éste queda bloqueado en el semáforo S.
2. Signal(S) Si hay algún proceso que ha sido suspendido en el semáforo,
se le despierta y puede continuar su ejecución. En caso contrario, S := S
+1.
BUAP FCC.
Mary carmen Cerón Garnica
68
Programación Concurrente
Los semáforos tienen las siguientes propiedades:
1. Wait y Signal son instrucciones atómicas. Ninguna instrucción puede ir
entrelazada entre la comprobación de si S > 0 y el decremento de la
variable o la suspensión del proceso.
2. El valor inicial de un semáforo es un número no negativo.
3. La operación Signal debe despertar a uno de los procesos suspendidos
en el semáforo, pero la definición no especifica a cuál. Esto podría llevar a
pensar que los semáforos no garantizan la vivacidad. La siguiente hipótesis
nos permite asegurarla.
4. Hipótesis de corrección de los semáforos: si un proceso P está
bloqueado en un semáforo S y este toma un valor estrictamente mayor que
cero con una frecuencia infinita, el proceso P eventualmente será
desbloqueado.
Los semáforos serán generales si pueden tomar cualquier valor no negativo, y
binarios si solo pueden tomar los valores 0 y 1. En este último caso, la instrucción
de incremento de la operación Signal se sustituye por S := 1.
En la literatura la notación habitual para las operaciones sobre semáforos es la
siguiente:
Wait(S) P(S)
Signal(S) V(S)
4.3.1. Ecuaciones de invariancia de un semáforo
Son las siguientes, y se verifican para cualquier semáforo S:
S ≥ =0
S= S0 + |Signals| − |Wai
(1)
donde S0 es el valor inicial no negativo del semáforo, |Signals| es el número de
operaciones de Signal ejecutadas sobre él, y |Waits| el número de Wait
completados.
Los invariantes se siguen directamente de la definición de semáforo. Si un
programa rompe estos invariantes, se ha realizado una mala implementación del
semáforo.
Presentaremos en lo que sigue una solución al problema de la exclusión mutua
caracterizada por estar basada en el uso exclusivo de semáforos.
BUAP FCC.
Mary carmen Cerón Garnica
69
Programación Concurrente
4.3.2. Implementación de un semáforo
Es posible realizar varias implementaciones del concepto de semáforo. La más
simple asocia a la variable que representa el semáforo una cola donde quedan
situados los procesos que quedan bloqueados en una operación Wait sobre el
semáforo. Los procesos que efectúan una señalización Signal sobre el semáforo
desbloquean a uno de lo procesos situados en la cola si los hay. A nivel de código,
la sintaxis de definición de un semáforo sería algo parecido a lo siguiente
Aquí, la variable S debe entenderse que almacena el valor concreto (un entero no
negativo) del semáforo en un instante dado, mientras que L es una lista que
almacena los descriptores de proceso actualmente bloqueados en el semáforo.
4.3.3. Exclusión mutua utilizando semáforos
El siguiente programa muestra un ejemplo de control del acceso de dos procesos
concurrentes a sus secciones críticas mediante semáforos, solucionando el
problema de la exclusión mutua.
Si un proceso como P1 desea entrar en su sección crítica, ejecuta un preprotocolo compuesto de una operación de Wait (S). Si el semáforo vale uno, se
decrementa y el proceso entra en su sección crítica, y la abandona ejecutando un
post-protocolo Signal (S) que restaura el valor del semáforo a uno. Si mientras el
primer proceso ejecuta su sección crítica un segundo proceso desea entrar en
BUAP FCC.
Mary carmen Cerón Garnica
70
Programación Concurrente
ella, hará una operación de Wait sobre el semáforo, y quedará bloqueado en él
hasta que sea liberado por el primer proceso cuando salga de ella.
La solución aquí propuesta es similar al segundo intento de solución que se dió,
salvo que la implementación atómica de las operaciones de los semáforos
previene el entrelazado entre la comprobación del valor de S y la asignación.
La diferencia con una instrucción de Test and Set es que el proceso detenido no
tiene que ejecutar un bucle de espera ocupada, sino que es situado en una cola
de procesos suspendidos asociada a S. Esto disminuye la sobrecarga (overhead)
del sistema.
Proposición 12.
mutua.
La solución propuesta satisface la propiedad de exclusión
Demostración: Sea |CS| el número de procesos concurrentes que están
ejecutando sus secciones críticas. Hemos de probar que
|CS| + S = 1
(2)
es invariante, lo cual equivale a decir que hay un sólo proceso en su sección
crítica.
De la ecuación 1 sabemos que S ≥ 0, de lo cual se deduce que |CS| ≤ 1 como
queríamos, probando ello la exclusión mutua. Veamos que además la relación es
invariante.
Es claro que |CS| será la diferencia entre el número de procesos bloqueados en el
semáforo, y el número de los que ya han salido de su sección crítica o lo que es lo
mismo:
|CS| = |Waits| − |Signals|
Luego utilizando la invariante de semáforos S = S0 +|Signals|−|Waits|, obtenemos
que S = 1− |CS| , de lo cual |CS| + S = 1 como se pretendía.
Proposición 13. El programa no puede bloquearse.
Demostración: Para ello sería necesario que ambos procesos quedaran
suspendidos en la ejecución de Wait (S). En este caso, será porque S = 0.
Ninguno de los procesos estará ejecutando su sección crítica, luego CS = 0. En
este caso se rompe el invariante 2 lo cual no es posible. En consecuencia los
procesos no pueden bloquearse entre sí.
BUAP FCC.
Mary carmen Cerón Garnica
71
Programación Concurrente
Proposición 14. No existen procesos ansiosos.
Demostración: Supongamos que P1 es suspendido, y que por tanto el semáforo
vale 0. Si ello es así, y por el invariante 2, el proceso P2 está ejecutando su
sección crítica, y cuando acabe hará una operación de Signal sobre el semáforo
liberando al único proceso bloqueado en él. Por tanto, no hay procesos ansiosos y
todos acceden a sus secciones críticas
4.4. Definiciones de semáforos
La bibliografía da varias definiciones de semáforos. Debemos distinguir entre ellas,
ya que la corrección de nuestros programas en un sistema dado dependerá de los
semáforos que implemente el sistema. Veremos aquí tres definiciones, si bien son
posibles algunas más.
a) Semáforos de conjunto bloqueado. El proceso señalador (que ejecuta Signal)
libera a uno de los procesos suspendidos, pero no sabemos a cual. Es el
semáforo que hemos definido y utilizado.
b) Semáforos de cola bloqueada. Los procesos suspendidos son encolados en
una estructura FIFO y liberados según el orden en que entraron en ella.
c) Semáforos de espera ocupada. El valor de S es comprobado en un bucle de
espera ocupada. La sentencia if completa es ejecutada en una sola operación
atómica, pudiendo haber entrelazados entre los ciclos del bucle.
El semáforo de espera ocupada se define en términos de una implementación, (el
código que lo define), y es ineficiente, debiendo ser considerado sólo en sistemas
BUAP FCC.
Mary carmen Cerón Garnica
72
Programación Concurrente
distribuidos donde la sobrecarga añadida por un bucle de espera ocupada es
despreciable.
La corrección de todas las soluciones puede comprobarse, ya que todas las
definiciones satisfacen los invariantes definidos para los semáforos en general.
Con los procesos ansiosos no ocurre igual, y en el caso de semáforos de espera
ocupada es posible la aparición de procesos de esta clase.
Proposición 15. Para un semáforo de espera ocupada, los procesos ansiosos son
posibles.
Demostración: Basta considerar la siguiente secuencia de instrucciones
ejecutadas:
1. El proceso P1 ejecuta Wait (S) y entra en su sección crítica.
2. P2 encuentra S a cero y entra en el lazo de espera ocupada.
3. P1 realiza un ciclo completo de pos-protocolo, pre-protocolo y reentrada
en sección crítica.
4. P2 vuelve a encontrar S a cero y sigue en espera ocupada.
Como podía haber entrelazado entre intrucciones del ciclo, esta actúa de modo
que el segundo proceso comprueba S antes de que el primero cambie su valor
con Signal (S) dando lugar a que el segundo proceso nunca entre en su sección
crítica.
Proposición 16. Para un semáforo de cola bloqueada, los procesos ansiosos no
son posibles.
Demostración: Si P1 está bloqueado en el semáforo, y hay como mucho N −1
procesos delante de él en la cola, todos ellos irán ejecutando Signal al salir de sus
secciones críticas, momento en que P1 entrará en la suya.
4.5. El problema del productor-consumidor
El problema de la exclusión mutua puede verse como una abstracción de la
necesidad de sincronizar la ejecución de procesos comunicados en sistemas
reales. Veremos como realizar la sincronización de procesos utilizando dos clases
de ellos, y los semáforos como primitiva de sincronización.
a) Procesos productores, que ejecutan un procedimiento interno producir y que
crean elementos de datos que pueden ser enviados a los procesos consumidores.
BUAP FCC.
Mary carmen Cerón Garnica
73
Programación Concurrente
b) Procesos consumidores, que tras recibir un elemento de datos lo procesan
utilizando un procedimiento interno consumir.
Un ejemplo de ello sería una aplicación produciendo listados de impresión que son
enviados al demonio de impresión del sistema. Cuando un proceso debe enviar un
dato a otro, una solución elemental a la comunicación es la comunicación
síncrona: cuando un proceso está listo para enviar y el otro para recibir, se envía
el dato.
Si necesitamos más flexibilidad, utilizamos un buffer de datos, y conseguimos
comunicaciones asíncronas. Aquí el productor va introduciendo datos en el buffer
de donde los toma el consumidor. Cada proceso va a su ritmo, y sólo hay que
controlar que el productor no inserte en un buffer lleno, y que el consumidor no
retire de un buffer vacío (ver Figura 2.2).
Figura 2.2 Productor-consumidor con buffer.
En la práctica podemos encontrar esta situación en un sistema interactivo donde el
usuario envía varios comandos a la computadora sin esperar al fin de la ejecución
del primero. Los comandos van a un buffer de memoria donde son recogidos por
la computadora y ejecutados.
Debe no obstante hacerse una precisión. Si las velocidades de los procesos
comunicados son muy diferentes no debe usarse un buffer, ya no que aporta
ninguna ventaja, pudiendo dar lugar a inconvenientes graves.
4.6. Productor-consumidor con buffers infinitos
Solucionaremos el problema del productor-consumidor utilizando inicialmente
buffers infinitos. Si bien la solución es inaplicable en la práctica, nos permitirá
ponerde manifiesto algunos principios de diseño aplicables a situaciones prácticas,
como es el caso de buffers finitos.
BUAP FCC.
Mary carmen Cerón Garnica
74
Programación Concurrente
Hay dos variables de índice In_Ptr, Out_Ptr, indicando la siguiente posición del
buffer donde hay que insertar y la posición más antigua de la cual se eliminó un
elemento.
Cuentan por lo tanto el número de elementos insertados en el buffer y el de
elementos eliminados respectivamente. Luego |E| = In_Ptr − Out_Ptr es el número
de elementos actualmente en el buffer. Las siguientes ecuaciones son claramente
invariantes:
Sin embargo, son estas ecuaciones las que definen el comportamiento de un
semáforo, luego el problema del productor-consumidor puede ser resuelto
utilizando un semáforo Elements que represente el número de elementos que hay
en el buffer.
BUAP FCC.
Mary carmen Cerón Garnica
75
Programación Concurrente
El semáforo Elements cuenta explícitamente el número de elementos que hay en
el buffer, con lo cual inicialmente tiene valor 0. El consumidor ejecuta una
operación Wait sobre este semáforo, con lo cual queda suspendido si el buffer
está vacío.
El productor ejecuta un Signal sobre este semáforo para desbloquear al
consumidor.
Por tanto, en esta implementación del problema del productor-consumidor, se
dispone de información física para indicar al consumidor cuando el buffer está
vacío o no utilizando la señalización proporcionada por el semáforo. Se consigue
así la tan necesaria sincronización sobre el consumidor al objeto de prever una
extracción de un buffer eventualmente vacío.
BUAP FCC.
Mary carmen Cerón Garnica
76
Programación Concurrente
4.7. Productor-consumidor con buffer acotado
En la práctica un buffer debe ser finito, y existen dos métodos de manejar un
buffer acotado. La primera es considerar un buffer circular, donde los índices se
toman módulo la longitud del buffer. Ahora, además de la sincronización, hemos
de asegurar que el consumidor no intente tomar un elemento de un buffer vacío, y
que el productor no intente poner nada en un buffer lleno.
Otro método es utilizar un grupo de buffers finitos. Al productor se le da un buffer
para que lo llene, tras lo cual se lo pasa al consumidor. Este consume los
elementos y devuelve el buffer al grupo. La sincronización vuelve a ser tan
necesaria como antes.
Utilizaremos un buffer circular por ser el más utilizado en la mayoría de los casos
y tener una implementación sencilla y eficiente mediante una cola circular.
Para controlar el buffer circular se crea un semáforo Spaces que cuenta los
lugares vacíos en el buffer. Cuando no hay más espacio, el productor al intentar
insertar en el buffer realiza una operación Wait sobre el semáforo y queda
suspendido. Mientras, el consumidor extrae un elemento del buffer y ejecuta un
Signal sobre el semáforo desbloqueando al productor. Los índices son
computados módulo N, que es la longitud del buffer. Haciendo uso del ya conocido
semáforo Elements prevemos la condición crítica de buffer vacío sobre el
consumidor.
BUAP FCC.
Mary carmen Cerón Garnica
77
Programación Concurrente
Analizamos a continuación la corrección de nuestra solución:
el número de elementos en el buffer. Las siguientes
Proposición 17. Sea
fórmulas son invariantes al comienzo del ciclo del lazo de control.
Demostración: Elements es un semáforo que es incrementado cuando el productor
inserta en el buffer y decrementado cuando el consumidor retira. Luego indica el
número de elementos que hay que en el buffer. Como
N = Elements + Spaces lo cual es cierto.
= Elements, resulta que
Proposición 18. El programa nunca elimina un elemento de un buffer vacío ni
inserta uno en un buffer lleno.
Demostración: Eliminar de un buffer vacío supone ejecutar un ciclo del proceso
consumidor con |E| = 0. Pero por la invariancia de |E|, resultará que Elements es
igual a 0, quedando el consumidor suspendido en el semáforo sin posibilidad de
consumir del buffer vacío. La demostración para la inserción en un buffer lleno es
enteramente similar, y el lector puede efectuarla como ejercicio.
Proposición 19. El programa no puede bloquearse.
Demostración: Para que el programa se bloquease sería necesario que tanto
Elements como Spaces valiesen cero. Pero entonces, por la invariancia |E| =
N−Spaces resultaría que N = 0. Luego estaríamos tratando con un buffer de
tamaño cero, lo cual no tiene sentido.
Proposición 20. No hay procesos ansiosos.
Demostración: No pueden darse procesos ansiosos porque tras acceder al buffer
tanto el productor como el consumidor señalizan al semáforo que teóricamente
podría bloquear a un proceso indefinidamente.
4.8. Productor-consumidor con semáforos binarios
En la solución anterior al problema del productor-consumidor con buffer acotado
utilizamos semáforos generales que podían tomar cualquier valor no negativo. A
continuación se propone una solución basada exclusivamente en semáforos
BUAP FCC.
Mary carmen Cerón Garnica
78
Programación Concurrente
binarios, ya que en ocasiones existen sistemas que sólo proporcionan esta
modalidad.
La solución lógica es utilizar un contador Count para conocer en todo momento
el número de elementos que hay en el buffer. El contador será modificado por los
procesos productor y consumidor pero en condiciones de exclusión mutua
controladas por un semáforo binario S. Si el productor encuentra que ese contador
es igual a N, se bloqueará en un semáforo binario Not_Full y será desbloqueado
por el consumidor cuando este realice una operación de extracción, señalizando a
ese semáforo. El consumidor será suspendido en igual forma en caso de buffer
vacío sobre el semáforo Not_Empty.
Las operaciones de incremento de los apuntadores pueden hacerse fuera de la
sección crítica porque aparecen como tareas simples.
Se introducen además las variables locales a los procesos Local_Count para
permitir que un proceso compruebe el valor de Count la última vez que cambió su
valor. Esto es necesario porque los semáforos tienen memoria. Esto es, para
Signal debe haber habido el correspondiente Wait. Sin las variables locales, se
darían secuencias de ejecución como la siguiente:
1. El proceso productor añade un elemento, ajusta Count a 1 y ejecuta
Signal (Not_Empty). de modo que Not_Empty=1 .
2. El consumidor mira el valor Count, detecta que no es cero y en
consecuencia no
procede a ejecutar Wait (Not_Empty). El consumidor extrae un elemento del
buffer y ajusta el valor de Count a 0. El estado actual de la computación es
tal que el buffer está vacío, Count=0 y Not_Empty=1 .
3. El consumidor entra de nuevo en su lazo y como Count=0, ejecutará Wait
(Not_Empty). Puesto que Not_Empty=1 , eliminará un elemento del buffer,
lo cual no es posible. Hemos llegado por tanto a una situación incorrecta.
El uso de la variable local hará que el consumidor ejecute Wait (Not_Empty)
solucionando el problema.
BUAP FCC.
Mary carmen Cerón Garnica
79
Programación Concurrente
Nota: en todas las versiones dadas, el acceso a las variables comunes (punteros y
buffer) queda protegido mediante un semáforo de exclusión mutua. Aquí el
semáforo sem es sustituido por el semáforo S, que realiza exactamente la misma
función.
BUAP FCC.
Mary carmen Cerón Garnica
80
Programación Concurrente
4.9. Desventajas de los semáforos
Son varias. Un semáforo puede ser utilizado tanto para exclusión mutua como
para sincronización. Si el programa es grande, es muy difícil saber cual es la
función que realiza. Además, la responsabilidad queda enteramente en manos del
programador. Si olvida hacer un Signal, el programa puede degenerar en una
situación de interbloqueo (deadlock).
Por otra parte, un uso profuso de semáforos en un código relativamente complejo
tiende a desestructurar el mismo, dificultando su legibilidad, y sobre todo su
interpretación semántica.
4.10 Regiones críticas
Son primitivas que permiten controlar la exclusión mutua y la sincronización de
procesos concurrentes de manera más estructurada, buscando las variables
compartidas por los distintos procesos y evitando el acceso simultáneo a las
mismas mediante la región crítica. Sólo se permite que un proceso acceda a la
región crítica, con lo cual se consigue la exclusión mutua.
4.10.1 Inconvientes de las regiones críticas.
Fundamentalmente son dos: en primer lugar no permiten que varios procesos lean
simultáneamente una variable, aún en el caso de que este acceso pueda llevarse
a cabo. En segundo lugar, no preveen la sincronización.
4.10.2. Regiones críticas condicionales
Hoare y Hansen proponen una solución a el problema de la sincronización
introduciendo las regiones críticas condicionales, caracterizadas por la primitiva de
sincronización Await(X), que bloquea a el proceso que la ejecuta, y permitiendo
por tanto sincronizar a distintos procesos. Intenta expresar el hecho de que el
proceso que llama a Await debe esperar hasta que se cumpla alguna condición
sobre las variables compartidas.
Así REGION X AWAIT(B) DO bloquearía al proceso llamante hasta que se
cumpliera la condición B.
BUAP FCC.
Mary carmen Cerón Garnica
81
Programación Concurrente
Figura 3: Procesos accediendo a un monitor.
4.11. Monitores
Como se ha visto, el semáforo es una primitiva de sincronización que no
requiere espera ocupada y que nos ha permitido solucionar con éxito los
problemas habituales presentes en los sistemas con múltiples procesos en
ejecución concurrente. Sin embargo, es un mecanismo de bajo nivel, ya que no es
estructurado. Si deseáramos construir un gran sistema concurrente basado en
semáforos, la responsabilidad de controlar los mismos sería algo difuso entre los
programadores del sistema. Bastaría que alguien se olvidase de hacer una
operación Signal cuando fuera necesario, para provocar un fallo total, y muy difícil
de aislar y subsanar.
Los monitores proporcionan una alternativa estructurada como primitiva de
programación concurrente, introducida por Hoare. No son más potentes ni útiles
que los semáforos, pero sí más estructurados y legibles. Un monitor es una
construcción sintáctica de un lenguaje de programación concurrente que
encapsula una estructura de datos o recurso que se ha de proteger mediante
exclusión mutua, junto con un conjunto de procedimientos que manejan ese
recurso (ver Figura 2.3).
La interface del monitor con el exterior es el conjunto de llamadas a
procedimientosdel monitor especificadas en su definición. Los procesos requieren
los servicios de los monitores mediante llamadas a sus procedimientos. Si un
mismo monitor es llamado por dos procesos, la implementación del monitor
asegura que las llamadas son procesadas secuencialmente para asegurar la
exclusión mutua en el acceso al monitor. Si un mismo proceso llama a monitores
diferentes, sus ejecuciones pueden ser entrelazadas y discurrir concurrentemente.
BUAP FCC.
Mary carmen Cerón Garnica
82
Programación Concurrente
En general, podemos decir que un monitor está caracterizado por encapsular en
un único módulo datos y procedimientos, teniendo lugar por tanto:
1. Centralización de recursos. Los monitores centralizan una serie de
recursos, garantizando que el acceso a ellos tiene lugar bajo exclusión
mutua.
2. Estructuración de los datos. Los monitores protegen un conjunto de
datos críticos comunes a varios procesos. El monitor puede verse como (y
de hecho su implementación es muy parecida a) un TDA.
3. Elevación del nivel de abstracción. Los programadores de procesos que
comparten recursos no han de preocuparse de proteger la exclusión mutua
de sus regiones críticas. Basta con llamar al monitor que maneja esos
recursos. Será este quien asegure la exclusión mutua.
Como ejemplo se muestra a continuación el problema del productor-consumidor
con buffer acotado resuelto mediante un monitor.
Una vez que disponemos de este código, que encapsula datos y procedimientos, y
que asegura la exclusión mutua por definición, la construcción de los
BUAP FCC.
Mary carmen Cerón Garnica
83
Programación Concurrente
procedimientos Productor y Consumidor es un asunto trivial que únicamente exige
la realización de llamadas a la interface de procedimientos proporcionada por el
monitor.
Como vemos, tanto el buffer como las variables de puntero que lo gestionan son
recursos críticos, y por tanto aparecen definidos dentro del monitor. Esto quiere
decir que cualquier proceso externo que desea hacer uso ellos debe hacerlo
pasando necesariamente por la interface de procedimientos proporcionada por el
monitor. Si la implementación de este es tal que solo admite a un proceso dentro
del mismo, tenemos conseguida la deseada exclusión mutua. Sin embargo, queda
aún por sincronizar a los procesos que manejan el buffer.
Para conseguirlo, hacemos uso de de las variables de condición, tal y como se
explica más adelante. Supuesto que el monitor proporciona la exclusión mutua tal
y como hemos visto, el código de los procesos Productor y Consumidor se reduce
a algo tan simple como lo siguiente:
La semántica de cualquier monitor es tal que sólo a un proceso se le permite
ejecutar uno de los procedimientos del monitor en un instante dado, con lo que
aseguramos la exclusión mutua sobre las variables y recursos globales.
La solución es mucho más estructurada que la basada en semáforos. Los
procesos productor y consumidor sólo ven llamadas abstractas a los
procedimientos Añadir y Coger, y no necesitan saber como están definidos ni
como es manejado el buffer.
BUAP FCC.
Mary carmen Cerón Garnica
84
Programación Concurrente
Al igual que un semáforo, un monitor tiene asociada una cola, FIFO en este caso,
donde permanecen los procesos bloqueados en una llamada a unos de los
procedimientos del monitor.
Como hemos visto, el problema de la exclusión mutua de los datos, queda
automáticamente resuelto por el hecho de usar un monitor. Sin embargo, dentro
del monitor puede ser necesarios en ocasiones sincronizar a los procedimientos
que lo componen.
Así, en el caso del monitor productor-consumidor, hay que sincronizar a los
procedimientos añadir y coger, ya que si no lo hacemos, el proceso productor
podría llamar al procedimiento añadir cuando el buffer estuviera lleno, causando
problemas.
La sincronización a nivel interno en los monitores se consigue utilizando las
variables de condición o señales, que son variables enteras no negativas sobre las
que se pueden efectuar tres operaciones:
1. Wait(C): El proceso que llamó a el procedimiento del monitor que
contiene esta sentencia es suspendido en una cola FIFO asociada a C. La
exclusión mutua sobre el monitor es liberada, de forma que otros procesos
puedan entrar en el monitor (y quizás señalizar la condición C).
2. Send(C): Se despierta al proceso situado en la cabeza de la cola si hay
alguno. Si no lo hay, la operación es simplemente ignorada.
3. Awaited(C): Devuelve true si la cola de C es no vacía. En el monitor
productor-consumidor se utilizaron dos señales:
1. Not_Empty: que suspende al consumidor hasta que el buffer esté
no vacio”, y contenga por tanto elementos para ser consumidos.
2. Not_Full: que suspende al productor hasta que el buffer esté ”no
lleno”, y se disponga de huecos para insertar elementos.
Las señales definidas son llamadas restrictivas, debido a que una operación
Send debe ser la última del procedimiento que la ejecuta. Esto es lógico, ya
que de no ser así, tendríamos dos procesos dentro del monitor, rompiendo
el requerimiento de exclusión mutua. Las señales restrictivas se
implementan en la práctica como una norma de programación que debe
cumplirse por parte de quien diseña el monitor. La técnica es conocida
como reasunción inmediata, ya que al efectuar Send sobre una variable de
condición un proceso bloqueado en ella entra inmediatamente en acción.
BUAP FCC.
Mary carmen Cerón Garnica
85
Programación Concurrente
Existen también las señales no restrictivas, que no imponen normas de
programación, pudiendo efectuar un Send donde y cuantas veces
queramos, si bien nosotros no las trataremos aquí.
Tal y como se ha venido comentando, los monitores proporcionan unos
servicios exactamente iguales a los de los semáforos, excepto que el nivel
de abstracción de estos es mucho menor. Como prueba de ella, se muestra
a continuación como simular semáforos utilizando monitores y viceversa.
4.11. 1 Emulación de semáforos vía monitores
Mediante esta emulación demostraremos no solo que las dos primitivas
proporcionan una potencia y expresividad similares, sino que también
ilustraremos por qué el monitor presenta un mayor nivel de abstracción que
el semáforo, viendo que la emulación es mucho más simple en un sentido
que en el otro. La emulación sirve para disponer de una primitiva cuando un
sistema proporciona una pero no la otra. A continuación se muestra el
código de la emulación:
La variable S mantiene el valor del semáforo y es inicializada a algún valor no
negativo S0. La variable de condición Not_Zero mantiene el encolamiento de
procesos que esperan a que el semáforo tenga un valor no cero. A continuación
mostramos que el monitor definido se comporta como un semáforo.
BUAP FCC.
Mary carmen Cerón Garnica
86
Programación Concurrente
Proposición 21. El invariante de semáforo se verifica.
Demostración: Puede hacerse sobre la secuencia de ejecución. Puesto que
cada procedimiento del monitor se ejecuta bajo exclusión mutua por
definición sin posibilidad de entrelazado de instrucciones, es posible
simplicar la demostración. Bastará probar que el invariante se mantiene en
cualquier entrelazado donde cada ejecución del procedimiento del monitor
es considerada como una instrucción atómica.
Asumiendo que S0 no es negativa, la primera desigualdad de verifica
inicialmente. Posteriormente se mantiene ya que la única posibilidad de que
S fuera menor que cero sería comenzando con valor S0 igual a uno y
ejecutando Wait. Pero si ello ocurre el proceso llamante queda bloqueado
en la condición Not_Zero y nunca llega a decrementar S, por lo cual se
mantiene la desigualdad.
La segunda desigualdad de deja como ejercicio al lector
4.11.2. Emulación de monitores vía semáforos
Esta segunda emulación es más compleja, ya que tenemos que utilizar a los
semáforos para construir un monitor. Para ello, usaremos un semáforo S para
asegurar la exclusión mutua sobre los procedimientos del monitor y un semáforo
C_Semáforo para cada variable de condición necesaria. Asumiremos que los
semáforos son del tipo de cola bloqueada, para poder implementar el
requerimiento de cola FIFO de los monitores. En otro caso, se podría programar
una cola de procesos bloqueados en forma explícita.
Asociado a cada variable de condición se tendrá un contador, ya que el significado
de Signal(C) depende de si la cola está o no vacía, y un semáforo no tiene modo
de descubrir esto, luego el contador ha de programarse explícitamente.
Cada uno de los procedimientos del monitor efectuará Wait(S) como primera
instrucción y Signal(S) como la última. Esto emula el requerimiento de exclusión
mutua de los monitores.
Cada llamada a Wait(C) sobre una variable de condición C del monitor se traduce
en
BUAP FCC.
Mary carmen Cerón Garnica
87
Programación Concurrente
Debe aclararse que esta implementación corresponde a señales restrictivas, que
como sabemos pueden ejecutarse como máximo una vez dentro de un
procedimiento del monitor y siempre como última instrucción de este. Ello implica
que una operación Send(C) sobre una variable de condición C da lugar a la toma
del control por un proceso que estuviera bloqueado en la condición.
4.11.3. Corrección en Monitores
La prueba de corrección de un monitor pasa por asociar al mismo una relación
invariante que debe ser cierta antes y después de cualquier llamada a un
procedimiento del monitor, así como en su inicialización. Un ejemplo de tal
relación, para el monitor Productor_Consumidor podría ser:
donde m es el número de elementos que hay en el buffer en un momento dado.
También podemos asegurar la correción de la sincronización implantada con el
monitor estableciendo para cada variable de condición una condición que ha de
cumplirse junto con el invariante inmediatamente después de operación Wait e
inmediatamente antes de la operación Send.
4.12. Solución a otros problemas clásicos
En esta sección y como colofón al capítulo, se comentarán dos problemas clásicos
dentro de la literatura, como son los problemas de los lectores y escritores, y el de
los filósofos, proponiendo para ambos soluciones basadas en semáforos y
monitores.
4.12.1. El problema de los lectores y escritores
Como se verá a continuación es un problema similar al más general, y ya resuelto,
problema de la exclusión mutua, donde los procesos competían por el acceso a
sus secciones críticas. En esta ocasión, los procesos se dividen en dos clases
bien definidas:
Procesos lectores: que no necesitan excluirse unos a otros.
Procesos escritores: que deben excluirse entre ellos, y entre los lectores.
BUAP FCC.
Mary carmen Cerón Garnica
88
Programación Concurrente
El problema es común a los sistemas de bases de datos, donde no hay
inconveniente en que varios procesos de lectura accedan concurrentemente a los
mismos datos, pero donde es necesario a toda costa garantizar que los procesos
que realizan operaciones de escritura ejecuten éstas en exclusión mutua.
Plantearemos la solución al problema mediante el uso de monitores, y dejaremos
al lector la solución a través de semáforos.
Para ello, propondremos un monitor con una interfaz de llamadas compuesta de
cuatro procedimientos: Start_Read y Start_Write, que el lector (escritor) llamará
cuando comience a leer (escribir). Igualmente habrá procedimientos End_Read y
End_Write, mediante los cuales el lector (escritor) indica al monitor que ha
terminado de leer (escribir). Como es natural, los procedimientos Start_ del
monitor se han diseñado para que el proceso que los llama quede bloqueado
cuando sea necesario.
Para ello, el monitor dispondrá de dos variables de estado internas, que
notaremos por:
• OK_to_Read para suspender a los lectores y,
• OK_to_Write para suspender a los escritores.
Mantendremos además un contador del número de lectores que están
actualmente haciendo una lectura en Readers, y una variable lógica, Writing, que
valdrá True cuando un escritor esté en acción.
La forma del código del monitor es sencilla: las variables Reader y Writing serán
incrementadas por los procedimientos Start_ y decrementadas por los
procedimientos End_.
Un lector es suspendido si algún proceso está actualmente escribiendo, es decir
Writing=True.
También lo es si hay algún escritor esperando para escribir, y por tanto se tiene
que Non_Empty(OK_to_Write). La primera condición la impone la definición del
problema, mientras que la segunda da la prioridad a un proceso escritor
suspendido sobre los lectores, y podría haberse especificado de otra forma.
Además un escritor será suspendido si y solo si hay procesos leyendo (Readers
6= 0) o escribiendo.
BUAP FCC.
Mary carmen Cerón Garnica
89
Programación Concurrente
BUAP FCC.
Mary carmen Cerón Garnica
90
Programación Concurrente
Como puede verse, el procedimiento End_Read ejecuta Signal (OK_to_Write) si
no hay más lectores. Si hay escritores suspendidos, uno de ellos será activado y
se le permitirá completar el procedimiento Start_Write, y los demás seguirán
suspendidos.
Finalmente, los procesos lector y escritor que hacen uso del monitor propuesto
son:
4.12.2 El problema de los filósofos
Es este otro de los problemas clásicos en el campo de la programación
concurrente, mediante el cual podemos comparar soluciones basadas en las
distintas estrategias propuestas para conseguir la exclusión mutua. El problema se
establece en términos de una comunidad de cinco filósofos, cuya vida transcurre
entre períodos de comer y de pensar. Podemos por tanto abstraer a un filósofo
mediante el siguiente código:
BUAP FCC.
Mary carmen Cerón Garnica
91
Programación Concurrente
Las comidas tienen lugar en una mesa donde hay cinco tenedores y un plato
central desde el cual los fiósofos se sirven cuando comienzan su período de
alimentación. Para poder comer, un filósofo necesita dos tenedores, que tomará
de su derecha y de su izquierda. El problema es diseñar pre y post-protocolos
tales que un filósofo sólo pueda comer si ha conseguido dos tenedores. La
solución, además, debería garantizar las propiedades de corrección ya descritas y
conocidas.
En lo que sigue, plantearemos una solución al problema basada en semáforos, y
otra en monitores, comprobando que ambas son legítimas desde el punto de vista
de su corrección formal.
4.12.3. Solución basada en semáforos
La solución que se propone viene implementada mediante el siguiente segmento
de código, donde los tenedores son simulados mediante semáforos. Un filósofo
debe completar dos operaciones de Wait sobre los tenedores situados a derecha e
izquierda antes de poder comer. Se supone que se lanza un proceso por filósofo
inicializado con un índice I que indica el número del filósofo.
Veamos a continuación si la solución propuesta es correcta. Comencemos para
ello probando el siguiente:
Proposición 22. Dos filósofos no pueden coger el mismo tenedor.
Demostración: Si consideramos la actividad de comer como la sección crítica, sólo
podrá haber un filósofo realizándola. De acuerdo con esto, y si #Pi es el número
de filósofos que tienen el tenedor i, tendremos que:
BUAP FCC.
Mary carmen Cerón Garnica
92
Programación Concurrente
Como además el invariante de un semáforo nos dice que su valor no puede ser
negativo, concluimos que #Pi ≤ 1 como queríamos.
No obstante, la solución aún no es adecuada. Considérese la secuencia de ínter
foliación que lleva a todos los filósofos a coger el tenedor de su izquierda,
mediante la ejecución de Wait(Tenedores(I)). Es obvio que llegamos a un
interbloqueo no recuperable, ya que ningún proceso señalizará sobre un
semáforo.
Una manera de asegurar la vivacidad de la solución es limitar el número de
filósofos que pueden sentarse a la mesa a cuatro, mediante la adición de un nuevo
semáforo
Acceso:
Proposición 23. No se dan procesos ansiosos.
Demostración: Supondremos que el semáforo Acceso es de cola bloqueada de
forma que culaquier filósofo esperando acceder a la mesa pueda eventualmente
hacerlo. Los semáforos Tenedores, en cambio, bastará con que sean de conjunto
bloqueado. Supongamos ahora que el filósofo i está ansioso. En estas condiciones
estará bloqueado para siempre en algún semáforo, y podremos distinguir entre
tres posibilidades:
1. Bloqueado en Acceso: como hemos supuesto que el semáforo es FIFO, i
está bloqueada sobre Acceso sólo si este vale 0 indefinidamente. Esto
sucederá únicamente si los otros cuatro filósofos están bloqueados en sus
tenedores porque uno de ellos ha cogido dos. Como eventualmente ese
BUAP FCC.
Mary carmen Cerón Garnica
93
Programación Concurrente
filósofo en concreto terminará y señalizará sobre Acceso, este caso es
consecuencia de los dos siguientes.
2. i está bloqueado en el tenedor de su izquierda. Entonces, el filósofo i-1
ha código el tenedor i, habiendo por tanto ejecutado con éxito su última
operación Wait. En consecuencia, terminará dejando i y deshaciendo el
problema.
3. i está bloqueado en el tenedor de su derecha, luego el fiósofo i+1 ha
tomado el
tenedor de su izquierda.
Proposición 24. La solución propuesta está libre de interbloqueos.
Demostración: Es trivial desde el momento en que se introduce el semáforo
Acceso.
4.12.4 Solución basada en monitores
La dificultad principal en la búsqueda de una solución basada en semáforos al
problema de los filósofos radica en el hecho de que la ejecución de una operación
Wait sobre un semáforo es irrevocable, y por tanto no hay manera de comprobar
el estado de dos tenedores simultáneamente. Si utilizáramos como solución un
monitor, no habría problemas para dejar a un filósofo esperando hasta que los
tenedores necesarios estuviesen libres.
A continuación se muestra el monitor que resuelve el problema:
BUAP FCC.
Mary carmen Cerón Garnica
94
Programación Concurrente
Como puede verse, el monitor mantiene un array Tenedores que indica el número
de tenedores libres disponibles para cada filósofo. El procedimiento
Coger_Tenedor espera sobre su propia variable de condición hasta que haya dos
tenedores disponibles. Antes de salir, el procedimiento decrementa el número de
tenedores disponibles para los vecinos. Por otra parte, tras haber comido, un
filósofo llama a Dejar_Tenedor, que libera los tenedores y manda una señal sobre
la variable de condición adecuada por si algún otro semáforo estuviera detenido
en ella. El código del proceso que abstrae a un filósofo, tras lo expuesto, es
absolutamente trivial:
Veamos ahora la corrección de la solución.
Teorema 25: Un filósofo solo come si tiene dos tenedores.
Demostración: Es claro, ya que el monitor garantiza que un filósofo no queda
bloqueado en la variable de condición OK_to_Eat sólo cuando el número de
tenedores que tiene vale dos.
BUAP FCC.
Mary carmen Cerón Garnica
95
Programación Concurrente
Programas en Java con monitores
1. Ejercicio de Filósofos implementados con monitores
Filosofo.java
public class Filosofo implements Runnable {
Mesa mesa;
int id;
public Filosofo (Mesa _mesa, int _id) {
mesa = _mesa;
id = _id;
}
public void run () {
for (int i=0;i<5;i++) {
System.out.println ("Filosofo "+id+" pensando");
try {Thread.sleep (1000);}
catch (Exception e) {}
mesa.coger (id);
System.out.println ("Filosofo "+id+" comiendo");
try {Thread.sleep (1000);}
catch (Exception e) {}
mesa.poner (id);
}
}
}
Mesa. java
import ConditionVariable.CV2;
public class Mesa {
int tenedores[] = {2,2,2,2,2};
CV2 okParaComer[] = new CV2[5];
public Mesa () {
for (int i=0;i<5;i++) {
okParaComer[i]=new CV2("okParaComer"+i);
}
}
public synchronized void coger (int i) {
if (tenedores [i] != 2) {
BUAP FCC.
Mary carmen Cerón Garnica
96
Programación Concurrente
System.out.println ("Filosofo "+i+" esperando");
okParaComer[i].DELAY (this);
}
tenedores[(i+1) % 5] = tenedores[(i+1) % 5] - 1;
tenedores[(i+4) % 5] = tenedores[(i+4) % 5] - 1;
}
public synchronized void poner (int i) {
System.out.println ("Filosofo "+i+" termina");
tenedores[(i+1) % 5] = tenedores[(i+1) % 5] + 1;
tenedores[(i+4) % 5] = tenedores[(i+4) % 5] + 1;
if (tenedores[(i+1) % 5] == 2)
okParaComer[(i+1) % 5].RESUME (this);
if (tenedores[(i+4) % 5] == 2)
okParaComer[(i+4) % 5].RESUME (this);
}
}
Programa Principal Filosofos con monitores
MonFilos.java
public class MonFilos {
public static void main (String args[]) {
Mesa mesa = new Mesa();
for (int i = 0;i<5;i++) {
new Thread (new Filosofo (mesa,i)).start();
}
}
}
Paquete ConditionVariable esta compuesto de:
•
•
•
CondVarSem.java
CondVarSem2.java
CV2.java
package ConditionVariable;
import Semaforo.*;
public class CondVarSem {
SemaforoBinario mutex;
BUAP FCC.
Mary carmen Cerón Garnica
97
Programación Concurrente
SemaforoBinario miCondicion;
int bloqueados;
public CondVarSem (SemaforoBinario _mutex) {
mutex = _mutex;
miCondicion = new SemaforoBinario (0);
bloqueados = 0;
}
public void DELAY () {
bloqueados++;
mutex.SIGNAL ();
miCondicion.WAIT ();
mutex.WAIT ();
}
public void RESUME () {
if (bloqueados>0) {
bloqueados--;
miCondicion.SIGNAL ();
}
}
public boolean emptyCV () {
if (bloqueados > 0) return false;
else return true;
}
}
package ConditionVariable;
import Semaforo.*;
public class CondVarSem2 {
SemaforoBinario mutex;
SemaforoBinario miCondicion;
int bloqueados;
public CondVarSem2 (SemaforoBinario _mutex) {
mutex = _mutex;
miCondicion = new SemaforoBinario (0);
bloqueados = 0;
BUAP FCC.
Mary carmen Cerón Garnica
98
Programación Concurrente
}
public void DELAY () {
bloqueados++;
mutex.SIGNAL ();
miCondicion.WAIT ();
bloqueados--;
}
public void RESUME () {
if (bloqueados>0) {
miCondicion.SIGNAL ();
mutex.WAIT();
}
}
public boolean EMPTY () {
if (bloqueados > 0) return false;
else return true;
}
}
package ConditionVariable;
import java.util.Vector;
public class CV2 {
Vector bloqueados;
// threads bloqueados en esta variable condición
boolean condSalida;
// condición para salir del DELAY
String nombre;
// nombre de la variable condición
static Vector condicion;
/* condiciones sobre las que se ha hecho resume
y
hay algún bloqueado pero todavía no se
desbloqueó.
Debe ser static para ser compartido por todas las variables condición. */
public CV2 (String _nombre) {
nombre = _nombre;
condSalida = false;
bloqueados = new Vector (50);
condicion = new Vector (10);
}
public void DELAY (Object monitor) {
synchronized (monitor) { // adquirimos el cerrojo sobre el monitor
BUAP FCC.
Mary carmen Cerón Garnica
99
Programación Concurrente
try {
bloqueados.addElement (Thread.currentThread());
do {
monitor.wait();
//System.out.println (".");
/* la condición de salida es que este thread sea el que más tiempo lleva
esperando en la variable condición y que además esté esperando por una
condición sobre la que se ha hecho un resume.*/
condSalida = bloqueados.firstElement().equals
(Thread.currentThread()) && condicion.contains
(nombre);
// si este thread no puede despertarse, se despierta a otro
if (!condSalida) monitor.notify ();
}
while (!condSalida);
condSalida = false;
bloqueados.removeElement (Thread.currentThread());
condicion.removeElement (nombre);
} catch (Exception e) {e.printStackTrace();}
}
}
public void RESUME (Object monitor) {
synchronized (monitor) {
if (bloqueados.size () > 0) { // Sólo si hay bloqueados
tiene sentido entrar
condSalida = false;
if (!condicion.contains (nombre))
condicion.addElement (nombre);
monitor.notify (); // Sólo despertamos a uno
return;
}
}
}
public boolean EMPTY (Object monitor) {
synchronized (monitor) {
if (bloqueados.size() > 0) return false;
else return true;
}
}
}
BUAP FCC.
Mary carmen Cerón Garnica
100
Programación Concurrente
2. Ejercicio de Fumadores
import ConditionVariable.CV2;
public class Mesa {
boolean hayIngredientes =false;
int ingred1=0, ingred2=0;
CV2 nuevos[] = new CV2[3];
CV2 tablaVacia = new CV2("tablaVacia");
public Mesa () {
for (int i=0;i<3;i++)
nuevos[i]=new CV2("nuevos"+i);
}
public synchronized void poner (int i, int j) {
if (hayIngredientes) {
System.out.println ("el agente se bloquea");
tablaVacia.DELAY (this);
}
System.out.println ("El agente ha dejado los ingredientes "+i+" y "+j);
ingred1 = i;
ingred2 = j;
hayIngredientes = true;
nuevos[3-(i+j)].RESUME (this);
}
public synchronized void coger (int i) {
if ((!hayIngredientes) || (i == ingred1) || (i == ingred2)) {
System.out.println ("se bloquea el fumador: "+i);
nuevos[i].DELAY (this);
}
hayIngredientes = false;
tablaVacia.RESUME (this);
}
}
BUAP FCC.
Mary carmen Cerón Garnica
101
Programación Concurrente
Clase Fumador
public class Fumador implements Runnable {
int i;
Mesa mesa;
public Fumador (int _i, Mesa _mesa) {
i=_i;
mesa = _mesa;
}
public void run () {
while (true) {
mesa.coger (i);
System.out.println ("Fumando el fumador: "+i);
try {Thread.sleep (100);}
catch (Exception e) {}
}
}
}
Main fumadores
public class MonFumadores {
public static void main (String args[]) {
Mesa mesa = new Mesa();
for (int i=0;i<3;i++) {
new Thread (new Fumador (i,mesa)).start();
}
new Thread (new Agente(mesa)).start();
}
}
3. Ejercicio de Lectores y Escritores
public class Lector implements Runnable {
private int i;
BaseDatos bd;
BUAP FCC.
Mary carmen Cerón Garnica
102
Programación Concurrente
ControladorPrefLectores controlador;
public Lector (BaseDatos _bd, ControladorPrefLectores _controlador, int _i)
{
i=_i;
bd = _bd;
controlador = _controlador;
}
public void run () {
for (int j=0;j<3;j++) {
System.out.println ("Lector "+i+" quiere leer");
controlador.empezarLectura(i);
bd.leer (i);
controlador.terminarLectura(i);
System.out.println ("Lector "+i+" usa su dato");
}
}
}
import Utilities.*;
public class Escritor implements Runnable {
private int i;
BaseDatos bd;
ControladorPrefLectores controlador;
public Escritor (BaseDatos _bd, ControladorPrefLectores _controlador, int
_i) {
i =_i;
bd = _bd;
controlador = _controlador;
}
public void run () {
for (int j=0;j<3;j++) {
System.out.println ("Escritor "+i+" quiere escribir");
controlador.empezarEscritura(i);
bd.escribir (i,i);
controlador.terminarEscritura(i);
System.out.println ("Escritor "+i+" usa su dato");
}
}
}
BUAP FCC.
Mary carmen Cerón Garnica
103
Programación Concurrente
mport ConditionVariable.CV2;
public class ControladorPrefLectores {
int contadorLectores = 0;
boolean hayEscritor = false;
CV2 okToWrite = new CV2("okToWrite");
CV2 okToRead = new CV2("okToRead");
public ControladorPrefLectores () {
}
public synchronized void empezarLectura (int i) {
while (hayEscritor) {
System.out.println ("el lector "+i+" se queda bloqueado en el
monitor");
okToRead.DELAY(this);
}
contadorLectores++;
okToRead.RESUME(this);
}
public synchronized void terminarLectura (int i) {
contadorLectores --;
if (contadorLectores == 0)
okToWrite.RESUME(this);
}
public synchronized void empezarEscritura (int i) {
while (hayEscritor || (contadorLectores != 0)) {
System.out.println ("el escritor "+ i +" se queda bloqueado en
el monitor");
okToWrite.DELAY(this);
}
hayEscritor = true;
}
public synchronized void terminarEscritura (int i) {
hayEscritor = false;
if (!okToRead.EMPTY(this))
okToRead.RESUME(this); //damos preferencia a lectores
else
okToWrite.RESUME(this);
}
}
BUAP FCC.
Mary carmen Cerón Garnica
104
Programación Concurrente
import Utilities.*;
public class BaseDatos {
private int valor = 0;
public BaseDatos (int v) {
valor = v;
}
public int leer (int i) {
System.out.println ("Lector "+i+" leyendo");
try {Thread.sleep (500);}
catch (Exception e) {}
System.out.println ("Lector "+i+" leyendo");
System.out.println ("Lector "+i+" leyendo");
return valor;
}
public void escribir (int v, int i) {
System.out.println ("Escritor "+i+" escribiendo");
System.out.println ("Escritor "+i+" escribiendo");
System.out.println ("Escritor "+i+" escribiendo");
valor = v;
}
}
public class MonLectoresEscritores {
static BaseDatos bd = new BaseDatos(0);
static ControladorPrefLectores controlador = new ControladorPrefLectores
();
public static void main (String args[]) {
for (int i=0;i<5;i++)
new Thread (new Lector (bd, controlador, i)).start();
for (int i=0;i<5;i++)
new Thread (new Escritor (bd, controlador, i)).start();
}
}
BUAP FCC.
Mary carmen Cerón Garnica
105
Descargar