notas de prograconcurr prim 2006

40
Benemérita Universidad Autónoma de Puebla Facultad de Ciencias de la Computación Notas del Curso: Programación Concurrente. Tomado del curso de M.C. Mireya Toval Vidal. Dra. Darnes Vilariño Ayala El objetivo de esta asignatura es presentar al alumno el paradigma de la programación concurrente en contraposición a la programación secuencial estudiada en cursos anteriores. La asignatura se desarrollará mediante la presentación de problemas típicos de la programación concurrente (productor/consumidor, panadería, filósofos, lectores/escritores, etc.) y la búsqueda de soluciones en modelos de memoria compartida y memoria distribuida. El alumno deberá analizar las distintas soluciones con respecto a requisitos de seguridad y viveza fundamentales en este tipo de programación. Es asimismo importante que el alumno conozca lenguajes de programación concurrente en los que pueda implementar las soluciones a los problemas antes mencionados. En particular en el curso se trabajara en Lenguaje Java. Concepto de Proceso y Concurrencia. La concurrencia está presente en casi todas las actividades que realiza el ser humano por lo que, intuitivamente, todos tenemos una idea de lo que significa el concepto concurrencia, independientemente del conocimiento que podamos tener sobre programación. Trataremos, por tanto, de clarificar dicho concepto para entender lo que es un programa concurrente y que características tiene. Normalmente, los primeros principios de programación se introducen estableciendo una analogía entre un programa y la consecución de tareas que se realizan en una actividad diaria cualquiera, como por ejemplo preparar una comida: abrir refrigerador si refrigerador está vacío entonces

Upload: others

Post on 16-Oct-2021

1 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: Notas de PrograConcurr Prim 2006

Benemérita Universidad Autónoma de Puebla

Facultad de Ciencias de la Computación

Notas del Curso:

Programación Concurrente.

Tomado del curso de

M.C. Mireya Toval Vidal.

Dra. Darnes Vilariño Ayala

El objetivo de esta asignatura es presentar al alumno el paradigma de la programación

concurrente en contraposición a la programación secuencial estudiada en cursos

anteriores. La asignatura se desarrollará mediante la presentación de problemas típicos de

la programación concurrente (productor/consumidor, panadería, filósofos,

lectores/escritores, etc.) y la búsqueda de soluciones en modelos de memoria compartida

y memoria distribuida. El alumno deberá analizar las distintas soluciones con respecto a

requisitos de seguridad y viveza fundamentales en este tipo de programación. Es

asimismo importante que el alumno conozca lenguajes de programación concurrente en

los que pueda implementar las soluciones a los problemas antes mencionados. En

particular en el curso se trabajara en Lenguaje Java.

Concepto de Proceso y Concurrencia.

La concurrencia está presente en casi todas las actividades que realiza el ser humano por

lo que, intuitivamente, todos tenemos una idea de lo que significa el concepto

concurrencia, independientemente del conocimiento que podamos tener sobre

programación. Trataremos, por tanto, de clarificar dicho concepto para entender lo que

es un programa concurrente y que características tiene.

Normalmente, los primeros principios de programación se introducen estableciendo una

analogía entre un programa y la consecución de tareas que se realizan en una actividad

diaria cualquiera, como por ejemplo preparar una comida:

abrir refrigerador si refrigerador está vacío entonces

Page 2: Notas de PrograConcurr Prim 2006

comer en el restaurante sin preparar entremeses preparar entrada preparar postre comer en casa fsi

Esta analogía introduce el concepto de programa secuencial como la descripción de una

secuencia de acciones. La ejecución de dicho programa la realiza un procesador y el

patrón de funcionamiento resultante se conoce como procesador secuencial, o

simplemente proceso.

Un gran número de programas se puede expresar aceptablemente de una forma

secuencial. Sin embargo, hay ciertas clases de problemas donde es esencial, o

simplemente más apropiado, desarrollar un programa como un conjunto de procesos

cooperativos que pueden ejecutarse en paralelo, o concurrentemente, para resolver dicho

problema. En particular, resultará esencial el desarrollar un programa de esta forma

cuando la concurrencia de actividades es un aspecto esencial del problema a resolver.

Considérese, por ejemplo, la descripción de la forma en que dos amigos preparan una

comida:

Juan Pablo preparar entremeses (1) preparar entrada (2) preparar postres (3) preparar la mesa (4) comer comer (5)

Obviamente, el tiempo consumido por el problema así descrito es menor que en el caso

secuencial; concretamente y, según la representación anterior, sería la mitad del caso

secuencial. Sin embargo, para obtener esa reducción temporal, que es la máxima ya que

se está suponiendo que todas las tareas solapadas comienzan y terminan a la vez –igual

duración, se ha duplicado el consumo de recursos. Por tanto, se debe establecer un

compromiso entre el espacio y el tiempo para determinar si es o no adecuado el

planteamiento concurrente frente al secuencial.

Page 3: Notas de PrograConcurr Prim 2006

Aplicaciones Inherentemente Concurrentes

Si nos centramos ahora dentro del campo de la Informática (Computer Science)

observamos que el número de áreas en las cuales la concurrencia es un aspecto

fundamental del problema a resolver es elevado, máxime si incorporamos el reciente

auge de Internet. Por enumerar algunas, las clásicas o más relevantes, podemos citar:

Sistemas Operativos

Las razones por las que un sistema operativo debe diseñarse como un programa

concurrente son conocidas suficientemente, cabe destacar:

1. La necesidad de soportar operaciones en paralelo para proveer servicios a uno o más

usuarios.

2. La interacción entre procesos debida a la gestión de recursos compartidos (por

ejemplo ficheros), o al intercambio de información entre estos (por ejemplo en el

correo electrónico).

3. La secuencia en la que se producen los eventos no es determinista.

Sistemas de Tiempo Real

Los programas concurrentes también son habituales en los procesos utilizados en

diversos tipos de control. A los sistemas computerizados de control se les denomina

sistemas de tiempo real, ya que reciben directamente las entradas de su entorno y deben

responder rápidamente para influir, y posiblemente, controlar este entorno. En la mayor

parte de los casos el corazón de estos sistemas son programas concurrentes de diversos

grados de complejidad. La necesidad de que sean programas concurrentes se deriva del

hecho de que deben responder a eventos que pueden ocurrir en cualquier instante de

tiempo.

Sistemas de Simulación

Page 4: Notas de PrograConcurr Prim 2006

Otra forma de programa concurrente es el que permite modelar, o simular, un sistema

complejo con varias actividades que interaccionan entre si. El objetivo del modelo es el

de ayudar a comprender dicho sistema y experimentar con variaciones del mismo. En este

tipo de aplicaciones la concurrencia es necesaria y clave para modelar la posible

secuencia de eventos que se produce en una situación del mundo real sujeta a

investigación.

Arquitecturas que soportan la Concurrencia

La simulación, el tiempo real y los sistemas operativos son clases de programas

inherentemente concurrentes porque los sistemas físicos que controlan, o reflejan, son en

si mismos concurrentes. Sin embargo la concurrencia también puede utilizarse en un

programa para mejorar su eficiencia, para lo cual será necesario que la máquina sobre la

que se va a ejecutar provea y permita la ejecución simultanea (paralela) de varios

procesos. De hecho, es este segundo principio, mejorar la eficacia de los algoritmos

explotando desde el diseño sus posibilidades de paralelismo, en el que nos centraremos

mayoritariamente en este curso.

Según el tipo de problema que se esté resolviendo nos encontraremos con problemas

donde:

1. sus componentes (procesos) se ejecutan concurrentemente con tiempos de ejecución

relativamente grandes,

2. los que el paralelismo es próximo al nivel de expresión o sentencia.

De los primeros diremos que tienen concurrencia de grano grueso, mientras que los

segundos tienen concurrencia de grano fino. Obviamente, las arquitecturas demandadas

en cada caso pueden ser diferentes.

Ejemplo de concurrencia de grano grueso

Ejemplos típicos de esta categoría son la predicción del tiempo, el diseño y modelado de

estructuras relevantes en dispositivos aéreos, etc. Se suelen caracterizar por constar de

pocas unidades o bloques funcionales que interaccionan relativamente poco con el resto

Page 5: Notas de PrograConcurr Prim 2006

(escaso intercambio de información) con unas demandas computacionales elevadas. Por

tanto, la arquitectura necesaria deberá constar de pocos pero muy potentes núcleos de

proceso donde la red de interconexión o el modelo de gestión de la memoria tiene poco

peso o influencia.

Obviamente cada uno de los bloques pueden ser susceptibles de ser paralelizados, con lo

que podríamos alcanzar descomposiciones de grano fino dentro de modelos teóricamente

gruesos.

Ejemplo de concurrencia de grano fino

Son muchos los problemas cotidianos para el alumno que podrían incluirse en esta

categoría: búsqueda, algoritmos numéricos, ordenación, etc. Consideraremos el problema

de ordenar una lista de N elementos. Un modo simple de introducir la concurrencia en la

resolución del problema es dividir la lista en varias sublistas de igual tamaño, ordenar

cada una de estas en paralelo (mediante algún algoritmo secuencial) y posteriormente

mezclarlas. El programa se ejecuta en dos fases:

a) las P sublistas se ordenan en paralelo

b) las sublistas ordenadas se mezclan en una lista ordenada.

Teniendo en cuenta que una lista de N elementos se puede ordenar en un tiempo O(N log

N) (en el mejor de los casos) y que la mezcla en una lista de longitud N es de O(N), el

tiempo de ejecución del algoritmo concurrente es de máximo(O(N/P log N/P),O(N)).

Consecuentemente, el número de sublistas P en las que se divida la lista inicial decidirá

que término domina en la expresión anterior. Así, si el número de sublistas P es igual a N

la fase de ordenación es despreciable y habríamos alcanzado la descomposición más

fina.

Aunque el tiempo de ejecución obtenido a priori es mejor que el del correspondiente

algoritmo secuencial, será necesario que el hardware permita implementar la

concurrencia especificada para lo cual deberá disponer al menos de P procesadores. En

otro caso podría ocurrir que el algoritmo concurrente fuese menos eficiente que su

correspondiente algoritmo secuencial. Estamos pues, necesitando computadores con gran

cantidad de nodos de proceso que no tienen que ser individualmente muy potentes.

Page 6: Notas de PrograConcurr Prim 2006

Además y, dado que habrá gran cantidad de intercambio (dependencia) de información,

la forma de conectarlos (comunicarlos) será muy importante, llegando a ser un factor

decisivo en la eficiencia del algoritmo.

Pero no solo el grosor del grano de los procesos es la única forma de clasificar los

algoritmos concurrentes. Podemos, por ejemplo, fijarnos en el número de instrucciones y

el número de datos que se ejecutan/acceden simultáneamente, lo que nos lleva a la

clasificación clásica de las arquitecturas paralelas:

SIMD (single instruction, multiple data),

MISD (multiple instruction, single data) y

MIMD (multiple instruction, multiple data).

Conceptos básicos

Un programa concurrente contiene componentes (procesos) que pueden ejecutarse

simultáneamente. La materia de este curso pretende enseñar a construir tales programas,

sin prestar especial atención a ningún lenguaje de programación en particular, ni a la

máquina concreta en la que se ejecutan dichos programas.

La materia se centrará en la descripción de los principios y metodologías de la

programación concurrente, en los problemas que genera la ejecución en paralelo de los

procesos y en las técnicas y herramientas existentes para afrontar tales problemas.

Programas Concurrentes

En términos operativos un programa concurrente se puede definir como un programa que

contiene partes que están diseñadas para ejecutarse en paralelo. La palabra clave en esta

definición es diseñadas porque la ejecución de cualquier programa tiene un

comportamiento concurrente. Así, en un programa secuencial la concurrencia está

presente en el sistema operativo y en la implementación hardware de las instrucciones

individuales de la máquina.

En general, hay dos razones para que un programador desarrolle un programa concurrente

para resolver un problema dado:

Page 7: Notas de PrograConcurr Prim 2006

1. Porque el problema a resolver sugiera de forma natural una solución concurrente, tal

y como ocurre si el programa debe realizar operaciones que deben desarrollarse en

paralelo, o si se deben gestionar eventos que se pueden producir en cualquier

instante.

2. Porque el hardware del computador sobre el que se va a ejecutar el programa soporte

ejecución de operaciones en paralelo y el tiempo de ejecución del programa pueda

reducirse si este se expresa de forma concurrente.

Como se ha comentado, un programa concurrente contiene un conjunto de procesos que

pueden ejecutarse en paralelo. Si en la máquina sobre la que se va a ejecutar el programa

están disponibles tantos procesadores como procesos entonces la concurrencia puede

realizarse completamente, en caso contrario, los procesos deben ejecutarse en tiempo

compartido sobre el procesador o procesadores disponibles.

De lo anterior se deriva la necesidad de asignar procesos lógicos a procesadores en el

momento de la ejecución de un programa concurrente. Evidentemente, tanto el número de

procesos como el de procesadores puede variar durante la ejecución del programa.

La asignación de procesos a procesadores dependerá en gran medida del entorno de

ejecución disponible. Dicho entorno puede clasificarse en tres tipos:

1. Entornos secuenciales. En este entorno la ejecución de programas concurrentes está

controlada por un sistema operativo multiusuario de propósito general. Tales

entornos están diseñados, en principio, para ejecutar programas secuenciales pero

puede simularse la ejecución de programas concurrentes.

2. Entornos con un único procesador central y múltiples procesadores de entrada/salida.

Este entorno es el que se da en la mayor parte de los ordenadores monousuario. En

este caso se pueden ejecutar en paralelo, o asíncronamente, las instrucciones

máquina y las operaciones de entrada/salida.

3. Entorno con múltiples procesadores centrales y procesadores de entrada/salida.

Dentro de este entorno podemos distinguir entre procesadores con acceso a memoria

compartida y procesadores distribuidos cada uno de ello con su propia memoria.

Entre estos últimos se puede citar el transputer de Inmos el cuál tiene un procesador

central y memoria principal en un chip. Un ordenador puede construirse a partir de un

Page 8: Notas de PrograConcurr Prim 2006

sólo transputer o desde varios conectados mediante canales físicos en un único

sistema multiprocesador.

Propiedades de la Programación Concurrente

Una de las principales diferencias entre un algoritmo secuencial y uno concurrente es que

el primero impone un orden total en el conjunto de tareas (instrucciones) que establece

mientras que el segundo únicamente especifica un orden parcial. En la preparación de la

comida, por ejemplo, si el refrigerador no está vacío los amigos deben tenerlo todo

preparado antes de comenzar a comer; sin embargo, la entrada puede prepararse antes

que los entremeses o después, y lo mismo puede decirse con respecto al postre.

Como un algoritmo concurrente especifica únicamente un orden parcial de las tareas a

realizar, debe tenerse en cuenta el hecho de que los tiempos de ejecución de las mismas

no está predeterminado. Así, si una comida se prepara varias veces siguiendo los

algoritmos descritos anteriormente, la ejecución puede completarse de varias formas

sin violar los requerimientos. En los programas concurrentes se necesita un cierto grado

de flexibilidad porque el tiempo de algunas operaciones puede no ser conocido o

constante, como por ejemplo cuando se trabaja con dispositivos periféricos, y en

cualquier caso siempre existen pequeñas variaciones de tiempo en la ejecución de las

instrucciones individuales de la máquina.

La falta de certeza sobre el orden preciso en que se producen algunos eventos en un

sistema concurrente es una propiedad denominada indeterminismo. La presencia de

dicha propiedad en un programa puede crear problemas al programador ya que se pueden

producir fallos provenientes de errores transitorios. Un error transitorio es aquél que

puede ocurrir dependiendo del orden en que se ejecuten las tareas en una activación

concreta del programa. Así, uno de los aspectos más importantes del diseño de

programas concurrentes es expresar estos de forma que se garantice su funcionamiento

correcto independientemente del orden en el que se puedan realizar algunas de sus

acciones individuales.

Por otra parte, el segundo algoritmo de la sección visto con anterioridad es incompleto

en lo siguiente: no

Page 9: Notas de PrograConcurr Prim 2006

identifica la interacción entre los dos procesos que lo componen. Es precisamente esta

interacción (cooperación) la que de hecho hace el algoritmo concurrente, pues en otro

caso simplemente tendríamos un conjunto de algoritmos secuenciales.

En general, la interacción entre procesos se produce en tres circunstancias diferentes:

1. Cuando los procesos compiten por acceder a un recurso compartido, tal y como

sucede en el ejemplo al utilizar cuchillos y otros utensilios compartidos durante la

preparación de la comida.

2. Cuando los procesos necesitan suspender temporalmente su ejecución, tal y como

sucede en el ejemplo cuando uno cualquiera de los amigos tiene que esperar para

comenzar a comer a que todos los platos estén preparados.

3. Cuando los procesos intercambian datos, tal y como ocurre cuando los amigos

intercambian ideas y bromas durante la comida.

En los tres casos es necesario que los procesos involucrados sincronicen sus

actividades, para evitar conflictos como en el caso 1, o establecer comunicaciones como

en los casos 2 y 3. En el caso de la gestión de recursos es esencial tener restricciones

sobre la forma en que estos se administran para asegurar la equidad y evitar o recuperar

el bloqueo (lockout). El bloqueo es una situación en la cual un proceso tiene asignado un

recurso solicitado por otro, rechazando o negándose a liberarlo, lo que produce que el

otro proceso pueda estar esperando indefinidamente. La equidad es necesaria para

asignar recursos a procesos en el mismo orden en el que se solicitan, de no ser así, puede

producirse la suspensión indefinida de algún proceso (starvation). Esta es una situación

que se produce cuando la petición de recurso de un proceso queda a la espera de

asignación del mismo y este se asigna continuamente a otros procesos, no garantizándose

la asignación del recurso en un tiempo finito.

En general los procesos de un programa concurrente tienen un tiempo de vida muy largo

(normalmente infinito, salvo que se produzcan interbloqueos).

Exclusión Mutua. Algoritmos de Dekker y Peterson

Como ya se ha comentado, los programas concurrentes deben diseñarse de forma que

sean deterministas y los procesos que lo componen deben sincronizar sus actividades,

Page 10: Notas de PrograConcurr Prim 2006

entre otros motivos, para acceder a un recurso compartido.

En general, un recurso compartido será un objeto (fichero, cola, ...) con una cierta

representación interna, y por tanto, dicho recurso estará identificado por una variable.

Así, dado que habitualmente los procesos de un programa concurrente accederán a

recursos (objetos) compartidos deberá evitarse la posibilidad de que un proceso pueda

acceder a la información de un objeto mientras otro proceso la está modificando. Es

decir, deberá conseguirse la exclusión mutua de los procesos respecto al recurso

compartido.

El caso más simple que permite ilustrar la necesidad de la exclusión mutua es el de dos

procesos que acceden al valor de una variable compartida (ver figura 2.2). En los

procesos de la figura 2.2 el valor de x que se imprime depende de las velocidades

relativas de ambos procesos, por tanto, el programa concurrente compuesto por dichos

procesos no será determinista.

Proceso 1 Proceso 2 si x>250 x:=(x+10) mod 500 entonces escribir(x) sino escribir(x-100) fsi

Figura 2.2 Acceso a una variable compartida

Una primera aproximación para resolver la exclusión mutua entre dos procesos podría

ser utilizar una variable global para gestionar el acceso a las secciones críticas, como se

muestra en la siguiente figura:

acceso: entero en el rango 1..2 con valor inicial 1 Proceso 1 Proceso 2 sección no crítica sección no crítica repetir hasta que acceso = 1 repetir hasta que acceso = 2 sección critica sección critica acceso = 2 acceso = 1 Figura 2.3 Primera aproximación al algoritmo de Dekker

La solución anterior es incorrecta porque ambos procesos examinan y actualizan una

única variable global. Así, si uno de los procesos “muere” se producirá una situación de

Page 11: Notas de PrograConcurr Prim 2006

bloqueo en el otro.

Para solventar esta situación, podemos pensar en que cada proceso actúe sobre su propia

variable, como se muestra en la figura 2.4.

C1, C2: enteros en el rango 0..1 con valor inicial 1 Proceso 1 Proceso 2 sección no crítica sección no crítica repetir hasta que C2 = 1 repetir hasta que C1 = 1 C1 = 0 C2 = 0 sección critica sección critica C1 = 1 C2 =1

Figura 2.4 Segunda aproximación al algoritmo de Dekker

Cada proceso Pi asigna a la variable Ci el valor 0 cuando desea entrar en su sección

crítica y el valor 1 Ci cuando la ha concluido. De esta manera, mientras un proceso no

está en su sección crítica el valor de las variables de control es 1, con lo que si el

proceso entra en un estado de halt el resto puede seguir trabajando.

Ahora bien, aunque este segundo algoritmo garantiza la ausencia de bloqueos, incumple

la propiedad de la seguridad, ya que los dos procesos pueden alcanzar sus secciones

críticas simultáneamente.

En la figura 2.4 cuando un proceso concluye el bucle de espera, inicia una secuencia de

instrucción que permiten alcanzar sin ninguna prevención su sección crítica. Este

conjunto de acciones pueden no ser, no lo son de hecho, atómicas. Por tanto el error está

en no considerar las actuaciones sobre las variables de control como sección crítica.

Para solucionarlo se puede utilizar el siguiente algoritmo,

C1, C2: enteros en el rango 0..1 con valor inicial 1 Proceso 1 Proceso 2 sección no crítica sección no crítica C1 = 0 C2 = 0 repetir hasta que C2 = 1 repetir hasta que C1 = 1 sección critica sección critica C1 = 1 C2 =1 Figura 2.5 Tercera aproximación al algoritmo de Dekker

que si bien garantiza que los dos proceso no entran en sus secciones críticas

Page 12: Notas de PrograConcurr Prim 2006

simultáneamente, no garantiza la ausencia de bloqueos si los dos procesos insisten en

entrar sus secciones críticas simultáneamente; lo que podemos solucionar con:

C1, C2: enteros en el rango 0..1 con valor inicial 1 Proceso 1 Proceso 2 sección no crítica sección no crítica C1 = 0 C2 = 0 Repetir Repetir C1 = 1 C2 = 1 C1 = 0 C2 = 0 hasta que C2 = 1 hasta que C1 = 1 sección critica sección critica C1 = 1 C2 =1 Figura 2.6 Cuarta aproximación al algoritmo de Dekker Sin embargo, el algoritmo de la figura 2.6 tiene dos defectos:

Starvation y

livelock, una variante o clase de bloqueo donde según la casuística temporal es

posible alternar ejecuciones satisfactorias con escasos intentos fallidos.

Ahora bien, si el número de colisiones por starvation o livelock es razonable (baja

probabilidad) el programador los puede asumir y así evitar soluciones más complejas.

Combinando las filosofías de la primera y cuarta aproximación (figuras 2.3 y 2.6

respectivamente) Dekker propuso un algoritmo para resolver la exclusión mutua entre dos

procesos que solventa los inconvenientes del anteriormente descrito. El algoritmo es:

C1, C2: enteros en el rango 0..1 con valor inicial 1 Acceso: entero en el rango 1..2 con valor inicial 1 Proceso 1 Proceso 2 sección no crítica sección no crítica C1 = 0 C2 = 0 repetir repetir if Acceso = 2 entonces if Acceso = 1 entonces C1 = 1 C2 = 1 repetir hasta Acceso = 1 repetir hasta Acceso = 2 C1 = 0 C2 = 0 fin si fin si hasta que C2 = 1 hasta que C1 = 1 sección critica sección critica C1 = 1 C2 =1

Page 13: Notas de PrograConcurr Prim 2006

Acceso = 2 Acceso = 1 Figura 2.7 Algoritmo de Dekker

Como se ha visto, el algoritmo de Dekker resuelve la exclusión mutua para dos procesos.

Es posible construir algoritmos que resuelvan este problema para N procesos, donde N

toma un valor arbitrario mayor que 2.

Mecanismos de Comunicación y Sincronización en Memoria Compartida En este tema se verán los mecanismos o herramientas disponibles para controlar la

ejecución concurrente de procesos. Dado que el uso de estos mecanismos persigue un

mismo objetivo, existe algún tipo de equivalencia entre ellos. De hecho, es posible

implementar unas herramientas con otras, si bien, cada una de ellas tendrá una semántica

específica que la hace más apropiada para resolver cierto tipo de problemas.

Regiones Críticas

En el tema anterior se presentó el problema de la exclusión mutua. También se vieron

varias formas de conseguir la exclusión mutua entre procesos (algoritmos de Dekker),

Sin embargo estos algoritmos conllevan una espera activa de los

procesos; es decir, cuando un proceso está intentando acceder a un recurso que ya ha sido

asignado a otro proceso, continua consumiendo tiempo del procesador en su intento de

conseguir el recurso. Además, la extensión de dicho algoritmo al caso de más de dos

procesos puede resultar excesivamente compleja.

Brinch Hansen propuso un constructor, la región crítica, para conseguir la exclusión

mutua.

El acceso a una variable v declarada como compartida debe efectuarse siempre dentro de

la región crítica (RC en adelante) asociada a v; de lo contrario el compilador dará un

mensaje de error.

La semántica de la RC establece que:

1. Los procesos concurrentes sólo pueden acceder a las variables compartidas dentro de

sus correspondientes RC.

Page 14: Notas de PrograConcurr Prim 2006

2. Un proceso que quiera entrar a una RC lo hará en un tiempo finito.

3. En un instante t de tiempo sólo un proceso puede estar dentro de una RC determinada.

Sin embargo, las RCs que hacen referencia a variables distintas pueden ejecutarse

concurrentemente.

4. Un proceso está dentro de una RC un tiempo finito, al cabo del cual la abandona.

Dicha semántica implica que:

1. Si el número de procesos dentro de una RC es igual a 0, un proceso que lo desee

puede entrar a dicha RC.

2. Si el número de procesos en una RC es igual a 1 y k procesos quieren entrar, esos k

procesos deben esperar.

3. Cuando un proceso sale de una RC se permite que entre uno de los procesos que

esperan.

4. Las decisiones de quien entra y cuando se abandona una RC se toman en un tiempo

finito.

5. Se supone que la puesta en cola de espera es justa.

Debe resaltarse que la espera que realizan los procesos es una espera pasiva; es decir,

que cuando un proceso intenta acceder a una RC y está ocupada, abandona el procesador

en favor de otro proceso. Con esto se evita que un proceso ocupe el procesador en un

trabajo inútil. Por otra parte se supone que la puesta en cola es justa; es decir, un proceso

no espera indefinidamente para entrar en una RC.

La utilización de una RC en el acceso a la variable compartida de los procesos de la

figura 2.2 del tema anterior permite resolver el problema de indeterminación que allí se

producía (ver figura 3.3)

Proceso 1 Proceso 2 región x hacer región x hacer x:=(x+10) mod 500 si x>250 fin región entonces escribir(x) sino escribir(x-100) fin si fin región

Figura 3.3 Acceso a una variable compartida utilizando RCs

Page 15: Notas de PrograConcurr Prim 2006

Las RCs se pueden anidar, pero debe tenerse en cuenta que las RCs no se deben anidar en

orden inverso en los procesos, de lo contrario podría producirse un interbloqueo.

Una implementación que refleja fielmente la semántica de las RCs es el tipo SpinLock

del Multi-Pascal.

Semáforos

Con una RC no resulta sencillo expresar que un proceso deba esperar a que otro termine

para comenzar o continuar su ejecución. Para resolver este problema Dijkstra introdujo

un mecanismo de intercambio de señales de sincronización: el semáforo.

Un semáforo es un TAD caracterizado por:

1. Estructura de datos

1. un contador entero no negativo

2. una cola de procesos esperando por ese semáforo

2. Operaciones

1. P(s) ò Wait(s)

2. V(s) ò Signal(s)

3. Init(s,valor)

siendo s una variable de tipo semáforo.

Las operaciones wait y signal se excluyen mutuamente en el tiempo. La operación Init

permite dar un valor inicial al contador de un semáforo y sólo está permitida en el cuerpo

principal del programa en la parte que no es concurrente. Por el contrario, las otras dos

operaciones sólo se pueden utilizar en procesos concurrentes.

Las operaciones sobre semáforos tienen el siguiente significado:

Wait(s) si el contador del semáforo S es igual a 0 entonces se lleva el proceso que realiza la operación a la cola asociada con el semáforo S suspendiendo su ejecución y abandonando el procesador a favor de otro proceso sino se decrementa el valor del contador asociado a S en una unidad y el proceso que realiza la operación sigue ejecutándose

Page 16: Notas de PrograConcurr Prim 2006

fin si Signal(s) si la cola asociada al semáforo S está vacía entonces se incrementa el valor del contador asociado a S en una unidad y el proceso que realiza la operación sigue ejecutándose sino se toma uno de los procesos que esperan en la cola del semáforo S y se le pone en un estado de preparado para ejecutarse. El proceso que realiza la operación sigue ejecutándose fin si

Invariante de los semáforos

Los semáforos se pueden considerar como un mecanismo de comunicación de procesos

en el cual los mensajes son vacíos. Sea e(s) el número de señales enviadas a un semáforo

s y r(s) el número de señales recibidas, los semáforos descritos deben cumplir el

siguiente invariante:

0 £ r(s) £ e(s) £ r(s) + máximo_entero

si a esto le añadimos que un semáforo puede tener un valor inicial que denotamos con

i(s), obtenemos:

0 £ r(s) £ e(s) + i(s) £ r(s) + máximo_entero

En definitiva el invariante de los semáforos expresa que:

1. No se pueden recibir señales más rápidamente de lo que se envían.

2. El Número de señales enviadas a un semáforo y no recibidas no puede exceder de

la capacidad del semáforo.

Con este invariante las operaciones wait y signal se pueden expresar de la siguiente

forma:

Wait(s) si r(s) £ e(s) + i(s) entonces r(s) = r(s) + 1 y el proceso continua fin si si r(s) = e(s) + i(s) entonces el proceso espera en la cola fin si

Page 17: Notas de PrograConcurr Prim 2006

Signal(s) e(s) = e(s) + 1 si la cola de procesos no está vacía entonces selecciona un proceso de la cola r(s) = r(s) + 1 fin si

Implementación de RCs con semáforos

Los semáforos binarios (aquellos en los que el contador sólo toma los valores 0 y 1)

permiten implementar fácilmente las RCs. Para ello es necesario poner el contador

inicialmente a 1 y rodear la sección crítica (cuerpo de la RC) con las operaciones wait y

signal de la siguiente forma:

wait(s) {entrada a la RC} sección crítica o cuerpo de la RC signal(s) {salida de la RC} siendo S un semáforo cuyo contador está puesto inicialmente a 1.

Se puede comprobar fácilmente que implementando una RC de la forma indicada se

garantiza su semántica. En cualquier caso existen razones para no implementar RCs con

semáforos (salvo que el lenguaje utilizado no permita definir RCs), entre ellas:

1. El compilador no reconoce qué variable protege un semáforo, con lo cual no nos

ayudaría si estamos utilizando una variable compartida fuera de su RC (tendría que

controlarlo el usuario).

2. El compilador no podrá distinguir procesos disjuntos y por tanto debe permitir el

acceso a cualquier variable.

También hay que tener sumo cuidado cuando se combinan los semáforos y las RCs, pues

debe tenerse en cuenta que si un proceso que está dentro de una RC realiza una operación

wait sobre un semáforo cuyo contador vale 0 este proceso se parará dentro de la RC

bloqueándola.

Ejemplo

Supóngase que existen 4 montones de papeles y que hay que coger uno de cada montón y

grapar los cuatro juntos. El proceso debe repetirse hasta que se acaben los montones (que

contienen el mismo número de papeles). Este problema lo podemos programar con dos

Page 18: Notas de PrograConcurr Prim 2006

procesos: uno que se encargue de formar los grupos de 4 papeles y otro que tome estos

grupos y los vaya grapando.

Evidentemente, el proceso que grapa no pude hacerlo si no tiene nada que grapar, tendrá

que esperar a que exista algún montón de 4 papeles. Precisamente, este es el punto de

sincronización de los dos procesos y lo implementaremos con un semáforo. El proceso

que hace los montones ejecutará una operación signal cada vez que haya hecho uno y el

que grapa ejecutará una operación wait cada vez que quiera grapar uno; esto quiere decir,

que el contador del semáforo está actuando como contador del número de montones de 4

papeles que quedan por grapar (número de señales enviadas al semáforo y no recibidas).

Por otro lado, deberá tenerse en cuenta que la mesa es un recurso compartido por ambos

procesos (el proceso que amontona no puede dejar un grupo sobre la mesa a la vez que el

que grapa coge un grupo), y por tanto, el acceso a dicho recurso deberá programarse

mediante RCs.

programa GraparHojas var s: semáforo mesa: Tipo T proceso amontonar repetir Toma una hoja de cada montón región mesa hacer deja el grupo de 4 hojas en la mesa fin región signal(s) hasta que se acaben las hojas proceso grapar repetir wait(s) región mesa hacer toma un grupo de la mesa y grápalo fin región hasta que no queden montones que grapar

Page 19: Notas de PrograConcurr Prim 2006

inicio init(s, 0) {inicialmente no hay grupo que grapar} en paralelo amontonar grapar fin en paralelo

Una posible alternativa al programa anterior que puede estudiarse es la de considerar que

existen 4 procesos cada uno de los cuales contribuye con una hoja (de uno de los cuatro

montones y siempre del mismo) para formar un grupo de cuatro hojas. Un quinto proceso

sería el encargado de grapar dichos grupos.

Regiones Críticas Condicionales.

Con los mecanismos vistos hasta ahora (RCs y semáforos) el hecho de que un proceso

que quiere acceder a un objeto compartido deba esperar a que se cumpla una cierta

condición (espera condicional) no resulta fácil de implementar, al menos si se pretende

que dicha espera sea pasiva. La condición, habitualmente, hará referencia al objeto

compartido.

Supóngase que un proceso quiere acceder a una variable compartida x pero sólo si se

cumple la condición B(x). Una forma simple de implementar la espera condicional pero

que implica una espera activa es la siguiente:

dentro = falso repetir región x hacer si B(x) entonces dentro = cierto {acceso a x} fin si fin región hasta que dentro sea cierto Obsérvese que la RC debe estar dentro del bucle y no al revés, de lo contrario un

proceso que acceda a la RC monopolizará dicha región, así si resulta que no se cumple

B(x) se producirá un interbloqueo.

A principios de los 70 Hoare y Brinch Hansen propusieron un mecanismo de alto nivel

Page 20: Notas de PrograConcurr Prim 2006

que permite realizar la espera condicional de forma pasiva: la región crítica

condicional (RCC en adelante).

Una RCC sólo se diferencia de una RC en que dentro de la RCC existe una sentencia

espera_a_que B. Dicha primitiva sólo puede estar dentro de una RC. Si existen varias

RCs anidadas, espera_a_que se asocia con la más próxima. Esta sentencia produce una

espera pasiva. Su semántica es la siguiente:

1. Si la condición B es cierta el proceso continúa por la siguiente sentencia a la

espera_a_que.

2. Si la condición B es falsa el proceso detiene su ejecución, abandona la RC para

permitir a otros procesos entrar en ella y pasa a una cola Q s de espera asociada con

la RC.

Cuando un proceso, que había evaluado la condición B a falso, vuelve a entrar a su RC lo

hace ejecutando de nuevo la sentencia espera_a_que, repitiéndose el comportamiento

antes descrito.

Un proceso que haya evaluado la condición del espera_a_que a falso, no vuelve a entrar

en su RC hasta que otro proceso abandone esta. Esto significa que un proceso espera a

que se cumpla una condición de forma pasiva (sin ejecutarse). Vuelve a ejecutarse

cuando es probable que se haya modificado dicha condición; esto es, cuando algún otro

proceso entra en una RC asociada a la misma variable compartida y sale de ella. Es en

este momento cuando a los procesos que estaban esperando en la cola Q s de la RC se les

da la oportunidad de ejecutarse. Evidentemente puede que la condición no haya cambiado

por lo que dichos procesos se suspenderán de nuevo cuando ejecuten la sentencia

espera_a_que. Es más, si estaban esperando N procesos, sólo uno de ellos puede

conseguir acceder a la RC y puede que después de evaluar a cierto la condición del

espera_a_que cambie dicha condición haciéndola falsa de nuevo. En estas circunstancias

los N-1 procesos restantes se dormirán de nuevo después de ejecutar la sentencia

espera_a_que.

Una RCC (entiéndase la variable compartida a la que está ligada) tiene asociada dos

colas

Q v que es donde espera un proceso cuando quiere entrar a una RC que está ocupada.

Constituye la cola de entrada a la RC.

Page 21: Notas de PrograConcurr Prim 2006

Q s que es donde esperan los procesos que evaluaron la condición de la sentencia

espera_a_que a falso.

La transición de Q s a Q v se produce cuando un proceso abandona la RC. La razón para

tener otra cola Q s , además de la de entrada, estriba en que los procesos que evaluaron a

falso la condición del espera_a_que no pueden volver a entrar a la RC hasta que la

condición haya podido ser modificada. Si estos procesos se suspendieran directamente

en la cola Q v de entrada podrían intentar entrar de nuevo a la RC sin que la condición

hubiera podido modificarse, con la consiguiente pérdida de tiempo del procesador. Sin

embargo, teniendo dos colas separamos a los procesos que están esperando entrar por

una condición de los que quieren entrar a la RC por primera vez.

Implementación de semáforos con CRS

Para implementar semáforos lo primero que debemos elegir es una representación para

estos objetos, los cuales, preferiblemente deben crearse de forma dinámica (en tiempo de

ejecución).

Tad Semáforo_RCC Exportar semáforo, init, wait, signal const maxsem = 100 tipo semáforo = 1..maxsem datos_semaforo = compartida registro contador: entero fin registro var disponible: entero esp: vector[1..maxsem] of datos_semáforo acción init(var s: semáforo; valor: entero) si (disponible > maxsem) entonces error sino s = disponible esp[s].contador = valor fin si fin acción

Page 22: Notas de PrograConcurr Prim 2006

acción signal(s: semáforo) región esp[s] hacer contador = contador + 1 fin región fin acción acción wait(s: semáforo) región esp[s] hacer espera_a_que (contador > 0) contador = contador - 1 fin región fin acción inicio disponible = 1 fin Tad

Implementación de RCCs con semáforos.

Se necesita disponer de dos colas de procesos suspendidos. Ya que un semáforo tiene

asociada una única cola de procesos esperando por el semáforo es obvio que, para

implementar una RCC, necesitaremos utilizar dos semáforos por cada variable

compartida a la que se acceda mediante una RCC:

Un semáforo v para disponer de la cola Q v y un semáforo s para disponer de la cola Q s .

El semáforo v debe ser un semáforo binario que garantice la exclusión mutua del cuerpo

de la RCC (sección crítica), por lo que el valor inicial de su contador asociado debe ser

1. Por otra parte, un proceso que evalúe la condición del espera_a_que a falso debe

suspenderse sobre la cola Q s del semáforo s, para ello es necesario hacer una operación

wait(s) y que el contador del semáforo s sea 0. El valor 0 del contador deberá

mantenerse constante, de lo contrario un proceso podría continuar su ejecución incluso

aunque evaluará la condición del espera_a_que a falso.

Finalmente, para obtener la implementación de una RCC mediante semáforos, debe

tenerse en cuenta que antes de suspender un proceso sobre la cola Q s debe indicarse que

dicho proceso sale momentáneamente de la RCC y que cuando vuelve a entrar lo hace

ejecutando de nuevo el equivalente a la sentencia espera_a_que.

Con las consideraciones hechas anteriormente la implementación de la RCC asociada a

la variable compartida x sería la siguiente:

Page 23: Notas de PrograConcurr Prim 2006

{inicio de la RCC} wait(v) {ejecuta el equivalente del espera_a_que hasta que se cumpla B(x)} mientras no B(x) hacer suspendidos_s = suspendidos_s + 1 {inicialmente 0} signal(v) {salida momentánea de la RCC} wait(s) wait(v) {intento de reentrada a la RCC (espera sobre Q v )} fin mientras {en este punto un proceso ha completado la RCC por lo que deben pasarse los procesos suspendidos sobre la cola Q s a la cola Q v } mientras (suspendidos_s > 0) hacer suspendidos_s = suspendidos_s - 1 signal(s) fin mientras signal(v) {final de la RCC}

La implementación previa puede dar lugar a que un proceso este continuamente

esperando para ejecutar la parte de la RCC que sigue a la sentencia espera_a_que

(starvation), cuando hay más de dos procesos que necesitan acceder a una misma

variable compartida. Esta situación se produce cuando un proceso evalúa siempre a falso

la condición del espera_a_que, pero no porque esta no sea cierta en algún momento

(situación de interbloqueo) sino porque se de la circunstancia de que siempre entre antes

a la RCC otro proceso que modifica la condición y la hace falsa. Evidentemente, la

posibilidad de que se produzca la situación descrita depende en gran medida de la forma

en que se gestione la cola de procesos suspendidos sobre un semáforo. Así, por ejemplo,

en el caso de que la gestión de la cola sea de tipo FIFO (lo habitual) dicha situación

podría producirse.

Una forma de asegurar que no se va a producir la situación anteriormente descrita cuando

la gestión de las colas de procesos suspendidos es de tipo FIFO es llevar los procesos

que quieren entrar de nuevo a la RCC, porque estaban esperando por una condición, a

una cola distinta de la cola Q v . La nueva cola la denotaremos por Q urgente y tendrá

más

prioridad que la cola Q v , de modo que cuando un proceso salga de la RCC tendrán

prioridad para acceder a la misma los procesos que estaban esperando por una condición

Page 24: Notas de PrograConcurr Prim 2006

sobre los procesos que estaban esperando a entrar por vez primera.

En este caso para implementar una RCC con semáforos necesitaremos un semáforo más,

urgente, para disponer de la cola Q urgente . El contador de este semáforo deberá de ser

siempre 0 para que los procesos esperen a volver intentar a entrar a la RCC cuando esta

quede libre. La implementación de la RCC es la siguiente:

{inicio de la RCC} wait(v) {ejecuta el equivalente del espera_a_que hasta que se cumpla B(x)} mientras no B(x) hacer suspendidos_s = suspendidos_s + 1 {inicialmente 0} signal(v) {salida momentánea de la RCC} wait(s) wait(urgente) {intento de reentrada a la RCC (espera sobre Q urgente )} fin mientras {en este punto un proceso ha completado la RCC por lo que deben pasarse los procesos suspendidos sobre la cola Q s a la cola Q urgente }

mientras (suspendidos_s > 0) hacer suspendidos_s = suspendidos_s – 1 signal(s) suspendidos_urgente = suspendidos_urgente + 1 fin mientras {final de la RCC} si (suspendidos_urgente > 0) entonces {pemite la reentrada a un proceso} suspendidos_urgente = suspendidos_urgente - 1 signal(urgente) sino {permite que un proceso entre por primera vez} signal(v) fin si Ejemplos

Ejemplo 1

En un hotel hay 10 vehículos automáticos pequeños y otros 10 grandes. Todos ellos están

controlados por un programa con procesos concurrentes (uno por vehículo). Estamos

interesados en la parte del programa que controla la entrada en un montacargas en el que

cogen hasta 4 vehículos pequeños o 2 vehículos pequeños y 1 grande

La definición del montacargas simplemente será la cantidad de vehículos de cada clase

Page 25: Notas de PrograConcurr Prim 2006

que están dentro de él.

programa montacargas var montacargas: compartida registro numVg: 0..1 numVp: 0..4 fin registro proceso Vg(n: entero) {otras cosas} región montacargas hacer {entrada al montacargas} espera_a_que ((numVg = 0) Ù (numVp £ 2)) numVg = numVg + 1 fin región {otras cosas} fin proceso proceso Vp(n: entero) {otras cosas} región montacargas hacer {entrada al montacargas} espera_a_que (((numVp < 4) Ù (numVg = 0)) Ú ((numVp < 2) Ù (numVg = 1))) numVp = numVp + 1 fin región {otras cosas} fin proceso inicio con montacargas hacer numVp = 0 numVg = 0 fin con en paralelo hacer para i = 1 hasta 10 hacer Vp(i) Vg(i) fin para fin en paralelo fin Ejemplo 2 Una posible solución basada en RCCs al problema de grapar hojas sería la siguiente: programa GraparHojas

Page 26: Notas de PrograConcurr Prim 2006

var numhojas: compartida of entero {número de hojas sobre la mesa} turno: vector[0..3] compartida of booleano proceso p(n: entero) repetir toma 1 hoja del montón n región turno[n] hacer {espera que toque el turno a la hoja del montón n} espera_a_que turno[n] turno[n] = falso fin región {deja la hoja sobre la mesa} región numhojas hacer numhojas = numhojas + 1 fin región {indica al proceso siguiente, (n+1) mod 4, que es su turno} región turno[(n+1) mod 4] hacer turno[(n+1) mod 4] = cierto fin región hasta que se acaben las hojas del montón n fin proceso proceso grapar repetir región numhojas hacer {espera a que haya un grupo completo de 4 hojas sobre la mesa} espera_a_que ((numhojas > 0) Ù (numhojas mod 4 = 0)) numhojas = numhojas - 4 fin región grapa el grupo de 4 hojas hasta que no queden hojas que grapar fin proceso inicio numhojas = 0 turno[0] = cierto para i = 1 to 3 hacer turno[i] = falso fin para en paralelo hacer para i = 0 hasta 3 hacer p(i)

Page 27: Notas de PrograConcurr Prim 2006

grapar fin en paralelo fin

En el algoritmo se ha utilizado una variable compartida por todos los procesos,

numhojas (número de hojas sobre la mesa, y cuatro variables booleanas compartidas

únicamente por los procesos que forman los grupos de hojas, turno[i] (i=0,1,2,3), que

indica a cual de ellos le toca poner una hoja sobre la mesa. La solución indicada es

válida porque el proceso grapar sabe cuando puede coger un grupo de la mesa (condición

del espera_a_que de su RCC). Sin embargo, en muchos problemas similares a este el

proceso pendiente de la actuación de otros, y no necesariamente de todos ellos, necesita

ser informado de que puede comenzar o continuar su ejecución (en nuestro caso el

proceso grapador). En estos casos suele ser habitual la presencia de otro proceso que es

el que se encarga de gestionar o controlar la ejecución sincronizada de todos los demás.

Ejemplo 3

Sea una carretera por la que circulan coches en los dos sentidos. La carretera cruza un río

donde sólo es posible la circulación de coches en un sentido (ver figura 3.8). Sobre el

puente pueden estar varios coches del mismo sentido.

Se pide diseñar un protocolo que permita circular a los coches sobre el puente y realizar

un programa siguiendo tal protocolo. En este programa cada coche estará representado

por un proceso. Se debe conseguir que:

a) No haya interbloqueos. Dos coches de diferentes sentidos no pueden bloquearse en

medio del puente.

b) El protocolo que permite el paso de los coches sobre el puente sea justo. No se puede

favorecer a los coches de algún sentido a costa de perjudicar a los otros.

El protocolo será el siguiente:

1. El puente será una variable compartida por los procesos. Cada vehículo (proceso)

deberá indicar el sentido de circulación (Norte->Sur ò Sur->Norte). Así,

programando el acceso al puente como RC se evitaran los interbloqueos si se

controla el número de vehículos de cada sentido dentro del puente.

2. Cada 10 vehículos que pasen en un sentido (cada cierto tiempo) se invertirá el

Page 28: Notas de PrograConcurr Prim 2006

sentido que tiene preferencia de paso por el puente. Habrá que controlar, por tanto, el

número de vehículos de cada sentido que pasan el puente. Este número debe

incrementarse cada vez que un vehículo entra en el puente, pues en caso contrario el

número de vehículos que cruzan el puente entre dos cambios consecutivos del sentido

de preferencia de paso dependería de la longitud del puente.

3. Un vehículo que llega al puente en el sentido que no tiene preferencia, podrá cruzarlo

si dentro del puente no hay ningún vehículo en el sentido preferente (contrario) y si no

hay ningún vehículo de dicho sentido esperando entrar al puente.

programa Obras_Públicas const max_seguidos = 10 tipo sentido = (norte, sur) var puente: compartida registro dentro vector[sentido] de enteros {vehículos sobre el puente} pasan vector[sentido] de enteros {vehículos que cruzan el puente} esperando: vector[sentido] de enteros turno: vector[sentido] de booleano fin registro proceso vehículo(n: entero; ent, sal: sentido) {entrada al puente por el sentido ent} región puente hacer esperando[ent] = esperando[ent] + 1 espera_a_que (dentro[sal] = 0) Ù (turno[ent] Ú (esperando[sal] = 0)) esperando[ent] = esperando[ent] - 1 dentro[ent] = dentro[ent] + 1 pasan[ent] = (pasan[ent] + 1) mod max_seguidos si (pasan[ent] = 0) entonces turno[ent] = falso turno[sal] = cierto fin si fin región {cruza el puente} {salida del puente por el sentido sal} región puente hacer dentro[ent] = dentro[ent] - 1 fin región fin proceso

Page 29: Notas de PrograConcurr Prim 2006

inicio con puente hacer dentro[norte] = 0; dentro[sur] = 0 pasan[norte] = 0; pasan[sur] = 0 esperando[norte] = 0; esperando[sur] = 0 turno[norte] = cierto; turno[sur] = cierto fin con en paralelo hacer para i = 1 hasta 10 hacer vehículo(i, norte, sur) vehículo(i, sur, norte) fin para fin en paralelo fin

Sucesos

Vimos en el apartado anterior que todos los procesos que entran en una RCC y no

cumplen la condición del espera_a_que se suspenden sobre la misma cola (Q s ) y todos

vuelven a intentar continuar la ejecución de la RCC cuando otro proceso termina la

misma. Esto es un inconveniente cuando no todos los procesos que entran en la RCC son

equivalentes (la condición del espera_a_que no es la misma) y es posible distinguir que

procesos podrían verificar la condición de otros que no la verificarían en función del

proceso que ha completado la RCC. Así, en el ejemplo del montacargas visto

anteriormente había 4 vehículos pequeños y salía uno de ellos, entonces

podía entrar uno de los vehículos pequeños que estuvieran esperando, pero no podía

entrar uno grande, por lo que no tiene sentido despertar a este tipo de proceso.

Para conseguir una gestión explícita de la cola de entrada a una RC y decidir que

procesos pueden entrar y cuales no es necesario introducir una nueva herramienta: los

sucesos.

Un suceso siempre debe ir asociado a una RC.

Por ejemplo, se puede declarar un suceso ev asociado a la variable compartida v como:

var v:compartida registro .............. ev: evento v fin registro El suceso es un TAD compuesto por una estructura de datos (una cola) que soporta las

Page 30: Notas de PrograConcurr Prim 2006

siguientes operaciones:

.Espera_a_que(ev). El proceso que ejecuta esta operación abandona la RC donde se

ejecutó, pasa a la cola del suceso ev asociada con dicha RC y se suspende cediendo

la CPU a otro proceso.

Causa(ev). Los procesos encolados en este suceso pasan a la cola principal de la RC

y entrarán a la misma cuando esta quede libre. Si no hay nadie en la cola del suceso

esta operación no tiene efecto. En cualquier caso el proceso que ejecutó la operación

continua.

Estas dos operaciones se excluyen mutuamente en el tiempo y tan sólo se pueden ejecutar

dentro de una RC. Puede haber varios sucesos asociados a una misma variable

compartida. Si hay N sucesos, existen N + 1 colas (las de los N sucesos y la cola de

entrada a la RC).

Normalmente una variable compartida tendrá tantos sucesos como condiciones de espera

distintas, referidas a esta variable, se puedan producir.

Implementación de sucesos con semáforos

La implementación de sucesos con semáforos es muy similar a la implementación de

RCCs, si bien, aquí cada variable compartida tendrá asociada en lugar de la cola Q s

tantas colas como sucesos tenga. Así, si la variable compartida tiene N sucesos serán

necesarios N semáforos, además del semáforo v de exclusión mutua de la RC (para la

cola Q v ).

Para evitar los problemas que se indicaron en la sección 3.3.2, utilizaremos una cola

adicional de entrada a la RC, Q urgente , de mayor prioridad que la Q v .

Sea la región crítica

región x hacer s1 si no B1(x) entonces espera_a_que(ev1) fin si s2 causa(ev2) s3

Page 31: Notas de PrograConcurr Prim 2006

fin región donde ev1 y ev2 son eventos correspondientes a dos condiciones de espera distintas,

B1(x) y B2(x) respectivamente. Su equivalente con semáforos es:

wait(v) {inicio de la RC} s1 si no B1(x) entonces {ejecuta el equivalente del espera_a_que(ev1)} suspendidos[1] = suspendidos[1] + 1 {inicialmente 0} signal(v) {salida momentánea de la RCC} wait(s[1]) wait(urgente) fin si s2 {ejecuta el equivalente del causa(ev2)} si (suspendidos[2] > 0) entonces suspendidos[2] = suspendidos[2] - 1 signal(s[2]) suspendidos_urgente = suspendidos_urgente + 1 fin si

s3

{final de la RC}

si (suspendidos_urgente > 0) entonces suspendidos_urgente = suspendidos_urgente - 1 signal(urgente) sino signal(v) fin si Obsérvese que por cada suceso i de la variable compartida x es necesario un semáforo

s[i] (cuyo contador deberá ser siempre 0) y un contador, suspendidos[i], del número de

procesos suspendidos sobre el mismo.

Ejemplo

Cierto número de procesos comparten un fondo de recursos equivalentes. Cuando hay

recursos disponibles, un proceso puede adquirir uno inmediatamente; en caso contrario

Page 32: Notas de PrograConcurr Prim 2006

debe enviar una petición y esperar a que se le conceda un recurso.

tad gestión_recursos exportar adquirir, liberar tipo recurso = 1..max_recursos proceso = 1..max_procesos var l: compartida registro disponibles: Pila_Recursos peticiones: Cola_Procesos turno: vector[proceso] de evento l fin registro acción adquirir(p: proceso; var r: recurso) región l hacer mientras Lista_Vacía(disponibles) hacer Guarda(peticiones, p) espera_a_que(turno[p]) fin mientras r = Tope(disponibles) Desapila(disponibles) fin región fin acción adquirir acción liberar(r: recurso) var p: proceso región l hacer Apila(disponibles, r) si no Cola_Vacía(peticiones) entonces Saca(peticiones, p) causa(turno[p]) fin si fin región fin acción inicio con l hacer Haz_Vacia(peticiones) Haz_Llena(disponibles) fin con fin tad

Page 33: Notas de PrograConcurr Prim 2006

Monitores

Las herramientas previamente descritas para manipular la concurrencia proporcionan un

enfoque conceptual de los problemas inherentes a la programación concurrente a la vez

que nos dotan de mecanismos para evitar problemas tales como el interbloqueo,

inanición, exclusión mutua de recursos compartidos, etc.; haciendo la programación

más fiable. De las herramientas definidas, pocas de ellas están incorporadas como

constructores en lenguajes de programación. Quizás la más extendida sea el semáforo.

Un constructor que permite controlar la cooperación entre procesos concurrentes y que

está incorporada en varios lenguajes de programación es el monitor. Fue propuesto por

Brinch Hansen y por Hoare, aunque de manera independiente.

Un monitor es un mecanismo que permite compartir de una manera fiable y efectiva tipos

abstractos de datos entre procesos concurrentes. Así pues, un monitor proporciona:

1. Abstracción de datos y

2. Exclusión mutua y mecanismos de sincronización entre procesos.

Un monitor es similar a un TAD (incluso para declararlo se utiliza la misma sintaxis que

para declarar un TAD sustituyendo la palabra reservada tad por monitor) en el sentido

de que esconde la representación interna de sus variables y proporciona al exterior sólo

el comportamiento funcional definido por las operaciones exportadas. Pero un monitor

proporciona más.

Por un lado, garantiza que el número de procesos que en un instante de tiempo están

ejecutando operaciones del monitor es como máximo 1. Esta propiedad, exclusión

mutua, asegura la consistencia de los datos del monitor.

Por otra parte, el monitor proporciona un mecanismo de sincronización entre procesos: el

constructor condición. Una operación del monitor puede suspender la ejecución de un

proceso durante una cantidad arbitraria de tiempo ejecutando una operación espera sobre

una variable de tipo condición. Cuando un proceso realiza la operación espera, pierde el

acceso al monitor (lo abandona temporalmente) y se suspende sobre la cola de espera de

la condición. Cuando un proceso realiza la operación señala sobre una variable de tipo

condición, despertará a uno de los procesos encolados en la condición en función de la

política de gestión de la cola (normalmente FIFO).

Cuando se ejecuta la operación señala se corre el peligro de que en el monitor se estén

Page 34: Notas de PrograConcurr Prim 2006

ejecutando concurrentemente dos procesos: el que ejecutó la operación señala y el que

estaba esperando y ahora es reanudado por la siguiente sentencia al espera. Para evitar

está situación, Hoare propuso que el proceso que ejecutó señala abandone el monitor y

se suspenda en una cola de alta prioridad del mismo, de modo que el proceso que entra

en el monitor y continua ejecutándose para completar la operación del monitor que estaba

realizando es el recién despertado. Cuando el monitor queda libre se permite que el

proceso que estaba en la cola de alta prioridad reanude su ejecución dentro del monitor.

Brich Hansen también propuso que el proceso que ejecutó la operación señala

abandonará inmediatamente el monitor, pero en lugar de encolarse, Hansen obligó a que

la instrucción señala fuese siempre la última de una operación del monitor. Esto tiene el

problema de que no puede ejecutarse más de una instrucción señala en una operación del

monitor, pero tiene la ventaja de que la implementación de esta política es muy sencilla.

En cualquiera de los dos métodos, la filosofía es la misma: se reanuda inmediatamente el

proceso suspendido con lo cual se asegura que el estado de las variables del monitor no

se modifica en el intervalo transcurrido desde que se ejecuta la operación señala hasta

que el proceso despertado reanuda su ejecución.

Implementación de semáforos con monitores

Dado que un monitor dispone de un constructor, condición, que tiene ya asociado una

cola de espera, podremos implementar fácilmente los semáforos haciendo corresponder

un semáforo con una condición y un contador. Además, para poder realizar fácilmente las

operaciones necesitaremos conocer el número de procesos suspendidos sobre un

semáforo.

monitor semáforos

exportar semáforo, init, wait, signal

const

max_semáforos = 100

tipo

semáforo = 1..max_semáforos

Page 35: Notas de PrograConcurr Prim 2006

datos_semáforo = registro

c: condición

valor, susp: entero

fin registro

var

espacio: vector [1..max_semaforos] de datos_semáforo

disponible: entero

la condición. Cuando un proceso realiza la operación señala sobre una variable de tipo

condición, despertará a uno de los procesos encolados en la condición en función de la

política de gestión de la cola (normalmente FIFO).

Cuando se ejecuta la operación señala se corre el peligro de que en el monitor se estén

ejecutando concurrentemente dos procesos: el que ejecutó la operación señala y el que

estaba esperando y ahora es reanudado por la siguiente sentencia al espera. Para evitar

está situación, Hoare propuso que el proceso que ejecutó señala abandone el monitor y

se suspenda en una cola de alta prioridad del mismo, de modo que el proceso que entra

en el monitor y continua ejecutándose para completar la operación del monitor que estaba

realizando es el recién despertado. Cuando el monitor queda libre se permite que el

proceso que estaba en la cola de alta prioridad reanude su ejecución dentro del monitor.

Brich Hansen también propuso que el proceso que ejecutó la operación señala

abandonará inmediatamente el monitor, pero en lugar de encolarse, Hansen obligó a que

la instrucción señala fuese siempre la última de una operación del monitor. Esto tiene el

problema de que no puede ejecutarse más de una instrucción señala en una operación del

monitor, pero tiene la ventaja de que la implementación de esta política es muy sencilla.

En cualquiera de los dos métodos, la filosofía es la misma: se reanuda inmediatamente el

proceso suspendido con lo cual se asegura que el estado de las variables del monitor no

se modifica en el intervalo transcurrido desde que se ejecuta la operación señala hasta

que el proceso despertado reanuda su ejecución.

Implementación de semáforos con monitores

Dado que un monitor dispone de un constructor, condición, que tiene ya asociado una

Page 36: Notas de PrograConcurr Prim 2006

cola de espera, podremos implementar fácilmente los semáforos haciendo corresponder

un semáforo con una condición y un contador. Además, para poder realizar fácilmente las

operaciones necesitaremos conocer el número de procesos suspendidos sobre un

semáforo.

monitor semáforos exportar semáforo, init, wait, signal const max_semáforos = 100 tipo semáforo = 1..max_semáforos datos_semáforo = registro c: condición valor, susp: entero fin registro var espacio: vector [1..max_semaforos] de datos_semáforo disponible: entero acción init(var s: semáforo; n: entero) si (disponible > max_semáforos) entonces error sino s = disponible con espacio[s] hacer valor = n susp = 0 fin con disponible = disponible + 1 fin si fin acción init acción wait(s: semáforo) con espacio[s] hacer si (valor = 0) entonces susp = susp + 1 espera(c) susp = susp - 1 sino valor = valor - 1 fin si fin con fin acción wait

Page 37: Notas de PrograConcurr Prim 2006

acción signal(s: semáforo) con espacio[s] hacer si (susp = 0) entonces valor = valor + 1 fin si señala(c) fin con fin acción signal inicio disponible = 1 fin monitor Implementación de monitores con semáforos

Se dará a continuación un algoritmo para transformar un programa que utiliza monitores

en un programa que utilice semáforos. Esto nos permitirá mostrar que los monitores no

son más potentes que los semáforos y que, por tanto, la decisión de utilizar

preferiblemente monitores a semáforos se debe, exclusivamente, a su mayor contribución

a la claridad y legibilidad del sistema concurrente.

La exclusión mutua de las operaciones del monitor puede simularse fácilmente mediante

un semáforo binario, mutex (tal y como se vio en el apartado 3.2.2: Implementación de

RCs con semáforos). Por cada condición c del monitor necesitaremos un semáforo sc en

el que poder suspender los procesos (por tanto el contador del semáforo siempre debe

ser 0) y un contador, susp_sc, del número de procesos suspendidos sobre el mismo.

Cada instrucción espera(c) del monitor deberá sustituirse por el código:

susp_sc = susp_sc + 1 signal(mutex) {salida momentánea del monitor simulado} wait(sc) susp_sc = susp_sc - 1 {reentrada al monitor simulado} Cada instrucción señala(c) de salida de una operación del monitor deberá sustituirse por

el código:

si (susp_sc > 0) entonces signal(sc) sino signal(mutex) fin si

Page 38: Notas de PrograConcurr Prim 2006

Esta implementación de monitores con semáforos es válida para los monitores definidos

por Brich Hansen; es decir, para monitores en los que la instrucción señala(c) es la

última instrucción del monitor.

La implementación de monitores con semáforos para los monitores definidos por Hoare

es algo más compleja y requiere utilizar una cola adicional de entrada al monitor de

mayor prioridad que la proporcionada por el semáforo mutex. Para disponer de la cola

de alta prioridad utilizaremos otro semáforo (urgente), ya que sobre él se pretenden

suspender los procesos que realizan la operación señala(c) el contador del mismo

deberá ser siempre 0.

En este caso cada instrucción espera(c) del monitor deberá sustituirse por el código:

susp_sc = susp_sc + 1 {salida momentánea del monitor simulado} si (susp_urgente > 0) entonces signal(urgente) sino signal(mutex) fin si wait(sc) susp_sc = susp_sc - 1 {reentrada al monitor simulado} y cada instrucción señala(c) del monitor deberá sustituirse por el código:

susp_urgente = susp_urgente + 1 si (susp_sc > 0) entonces signal(sc) wait(urgente) {se suspende a si mismo} fin si susp_urgente = susp_urgente – 1 Ahora la entrada a una operación del monitor vendrá dada por la instrucción

wait(mutex)

y la salida de una operación por la instrucción

si (susp_urgente > 0) entonces signal(urgente) sino signal(mutex) fin si

Page 39: Notas de PrograConcurr Prim 2006

Ejemplo

Obtener un monitor para definir semáforos en los que la política de gestión de la cola sea

FIFO. Se supondrá que la gestión de la cola de las condiciones del monitor es

desconocida. Ya que se debe gestionar la cola de procesos suspendidos, se supone

definida la función identificación_proceso, que retorna el número de proceso, entre 1 y

max_procesos, del proceso que la ejecuta.

monitor Semáforos_FIFO exportar semfifo, initsem, waitfifo, signalfifo const max_semáforos = 100 max_procesos = 10 tipo semfifo = 1..max_semáforos proceso = 1..max_procesos datos_semáforo = registro c: vector [proceso] de condición cola: Cola_Procesos valor: entero fin registro var espacio: vector[semfifo] de datos_semáforo disponible: entero acción initsem(var s: semfifo; n: entero) si (disponible > max_semáforos) entonces error sino s = disponible con espacio[s] hacer Hacer_Vacia(cola) susp = 0 valor = n fin con disponible = disponible + 1 fin si facción initsem acción waitfifo(s: semfifo) var p: proceso con espacio[s] hacer si (valor > 0) entonces valor = valor - 1

Page 40: Notas de PrograConcurr Prim 2006

sino p = identificación_proceso Guardar(cola, p) espera(c[p]) fin si fin con fin acción waitfifo acción signalfifo(s: semfifo) var p: proceso con espacio[s] hacer si Cola_Vacía(cola) entonces valor = valor + 1 sino Sacar(cola, p) señala(c[p]) fin si fin con fin acción signalfifo inicio disponible = 1 fin monitor.