Você está na página 1de 12

Receta

general para resolver problemas de


sincronizacin con semforos
La primera vez que te enfrentas a la tarea de implementar una solucin a un problema de
sincronizacin entre procesos, es normal que te parezca algo inabordable y que no sepas
por dnde empezar. Tal vez ya conoces y entiendes las soluciones a algunos problemas tipo,
como el bfer cinito o los cilsofos, pero no sabes cmo aplicar estos algoritmos a nuevos
problemas que aparentemente no tienen nada que ver con lo que has aprendido hasta
ahora. Te invaden el desconcierto y la parlisis.
Afortunadamente, existen tcnicas sistemticas (y simples) para construir algoritmos de
sincronizacin entre procesos y que son tiles en una gran variedad de casos. En este
artculo explicaremos una tcnica que para resolver problemas de sincronizacin entre
procesos utilizando semforos. La tcnica puede servir como receta general para abordar
situaciones en las que hay necesidad de sincronizar procesos y no se tiene muy claro por
dnde empezar.
La tcnica descrita tambin tiene sus limitaciones. Hay problemas socisticados que son
dicciles de modelar con ella. Por otro lado, no siempre da lugar a una solucin eciciente.
Pero al menos decine un camino seguro para elaborar soluciones sin necesidad de recurrir
a la inspiracin.

Contenido
1 La receta: descripcin terica.......................................................................................................................... 2
Patrones de cdigo: bloqueo condicional y desbloqueo.................................................................... 2
Los pasos de la receta........................................................................................................................................ 4
2 La tcnica en accin.............................................................................................................................................. 5
Paso 1. Esquema bsico del algoritmo....................................................................................................... 5
Paso 2. Localizar los puntos de bloqueo y las condiciones............................................................... 5
Paso 3. Escribir las condiciones de bloqueo de manera formal...................................................... 6
Paso 4. Localizar los puntos de desbloqueo............................................................................................. 6
Paso 5. Aplicar los patrones de cdigo ...................................................................................................... 7
Paso 6. Optimizar el cdigo.......................................................................................................................... 10
3 Cerrojos y variables condicin...................................................................................................................... 10
4 Versin simplicicada: cola de espera nica.............................................................................................. 10
5 Conclusin.............................................................................................................................................................. 12

2007-2014 J.M. Santos Espino, A. Quesada, E. Rodrguez, F. Santana

1 La receta: descripcin terica


La receta se basa en reducir todos los problemas de sincronizacin a dos clases: accesos a
datos compartidos y esperas condicionales. Los primeros los resolveremos con un
semforo de tipo mutex, que es un semforo inicializado a uno. Las esperas condicionales
las trataremos mediante semforos de tipo cola de espera, que son semforos inicializados
a cero y se usarn para bloquear a los procesos que deben esperar a que se cumpla una
condicin lgica.
Para solucionar un problema genrico de sincronizacin, nos bastar con decinir un solo
semforo de tipo mutex. Por cada situacin de bloqueo que identiciquemos, normalmente
declararemos un semforo de tipo cola de espera, aunque si no nos queremos complicar,
podemos utilizar una sola cola de espera para todo el sistema: es menos eciciente, pero
funciona.
Nuestra receta consiste en analizar el algoritmo que queremos sincronizar y localizar en l
todos los puntos donde se pueden producir bloqueos. Cada uno de los bloqueos estar
condicionado a que se cumpla una expresin lgica (booleana), que tendremos que
averiguar. Una vez decinidas esas condiciones, se aplicarn unas plantillas de cdigo
universales que resuelven los bloqueos y desbloqueos.

Patrones de cdigo: bloqueo condicional y desbloqueo


Para implementar un bloqueo, se utilizarn siempre los mismos patrones de cdigo, que
estn basados en operar con una cola de espera de procesos.
El punto de partida es que hemos identicicado puntos de bloqueo dentro del cdigo,
vinculados al cumplimiento de condiciones lgicas. El cdigo alrededor de esos puntos de
bloqueo tiene que tener esta estructura:
... bloquearse mientras se cumpla COND
... cdigo que slo se puede ejecutar si no se cumple COND
Un ejemplo puede ser lo que le ocurre a un cilsofo en el problema la cena de los cilsofos:
... bloquearse mientras el palillo(i) y el palillo(i+1) estn ocupados
... tomar el palillo(i) y el palillo(i+1)
... comer
O al productor en el problema del bfer cinito:
... bloquearse mientras el bfer est lleno
... insertar un elemento en el bfer
Cada situacin de bloqueo (en los ejemplos, el cdigo que empieza por bloquearse
mientras...) estar asociada con una cola de espera, que a su vez se implementar
mediante dos objetos: un semforo inicializado a cero y una variable entera. El semforo se
usar para bloquear a los procesos que deban mantenerse en espera, y la variable entera
llevar la cuenta de cuntos procesos esperan en cada momento.
En todos los problemas de sincronizacin que resolveremos con esta receta, existirn datos
compartidos: como mnimo, los contadores enteros relacionados con las colas de espera.
Para regular el acceso a los datos compartidos siempre habr que declarar un semforo de
tipo mutex. Hay que utilizar el mutex para envolver TODOS los usos de variables
compartidas en el cdigo.

2007-2014 J.M. Santos Espino, A. Quesada, E. Rodrguez, F. Santana

Los patrones de cdigo que resuelven los bloqueos y desbloqueos son exactamente tres:
bloqueo de un proceso
desbloqueo de un proceso
desbloqueo de todos los procesos que estn en cola
En los siguientes apartados describimos esos tres patrones.

Variables compar=das
Esta receta se basa en dos semforos y una variable entera.
Semforo mutex = 1;
Semforo colaCOND = 0;
int contadorCOND = 0;

La variable contadorCOND contendr en todo momento el nmero de procesos que estn


bloqueados en el semforo colaCOND.

Cdigo de bloqueo
Este cdigo se debe escribir en cada punto donde detectemos que debe existir un bloqueo
condicional. El cdigo debe insertarse justo antes del cdigo original que slo puede
ejecutarse cuando se cumpla la condicin lgica del bloqueo.
P ( mutex );
// esperar hasta que se cumpla una condicin lgica COND
while ( COND ) {
contadorCOND++; // registra que hay alguien en espera
V (mutex); // libera el mutex para no provocar interbloqueos
P (colaCOND); // bloquea al proceso
P (mutex); // vuelve a adquirir el mutex para reevaluar COND
}
// Colocar aqu el cdigo que toque variables de estado.
// Este cdigo se ejecuta en exclusin mutua (mutex est adquirido).
V ( mutex );

Cdigo para desbloquear a un solo proceso


Este cdigo se escribir cuando en nuestro programa debe desbloquearse a uno solo de los
procesos que estn en espera.
P ( mutex );
// desbloquear UN SOLO proceso
if ( contadorCOND > 0 ) { // hay alguien esperando?
contadorCOND--; // entonces, anota que hay uno menos
V ( colaCOND ); // y desbloqualo
}
V ( mutex );

Justo despus de la operacin P(mutex) podra haber otras instrucciones, por ejemplo para

2007-2014 J.M. Santos Espino, A. Quesada, E. Rodrguez, F. Santana

actualizar variables de estado. Esto depender de cada sistema que estemos desarrollando.

Cdigo para desbloquear a todos los procesos


A veces nos interesar desbloquear a todos los procesos que se encuentran en la cola. El
nico cambio sobre el algoritmo anterior es sustituir el if del desbloqueo por un bucle
while. El nmero de iteraciones viene determinado por la variable contadorCOND, que
contiene el nmero de procesos que actualmente estn en cola.
P ( mutex );
// desbloquear a todos los procesos
while ( contadorCOND > 0 ) {
contadorCOND--;
V ( colaCOND );
}
V ( mutex );

Los pasos de la receta


Estos son los pasos que hay que seguir para resolver el problema de sincronizacin.
1. Escribir el esquema bsico del algoritmo, sin instrucciones de sincronizacin.
2. Localizar los puntos de bloqueo:
marcar las zonas del algoritmo donde los procesos se pueden bloquear y escribir las
condiciones de bloqueo en lenguaje natural.
3. Formalizar las condiciones de bloqueo:
reescribirlas usando variables, expresiones booleanas, etc. variables de estado
4. Localizar los puntos de desbloqueo:
marcarlos en el cdigo y distinguir si se desbloquea a un solo proceso o a todos los
procesos que esperan.
5. Sustituir los bloqueos y desbloqueos por plantillas de cdigo universales, siguiendo
estas reglas:
a) Cada situacin de bloqueo y desbloqueo se resuelve con operaciones P y V de un
semforo cola de espera, inicializado a cero.
b) Usamos las variables contadoras para saber cuntos procesos hay esperando en
cada cola.
c) Usamos un mutex (un semforo inicializado a uno) para proteger las zonas de
cdigo donde estn las sincronizaciones y los accesos a variables compartidas,
incluyendo las variables de estado.
6. Optimizar el cdigo resultante, si se desea.

2007-2014 J.M. Santos Espino, A. Quesada, E. Rodrguez, F. Santana

2 La tcnica en accin
Ahora que se ha presentado la receta, la aplicaremos a un ejemplo simple y conocido: el
primer problema de los lectores y escritores. Recordemos que en este problema hay dos
tipos de procesos que acceden a una base de datos: lectores y escritores. Se debe permitir
el acceso concurrente de los lectores; y mientras un proceso escritor est accediendo a la
base de datos, no puede haber ningn otro proceso (ni escritor ni lector). En el llamado
primer problema, los lectores tienen preferencia sobre los escritores.
Vamos a aplicar los pasos de la receta a este problema y ver cmo evoluciona el programa
que vamos escribiendo.

Paso 1. Esquema bsico del algoritmo


Este paso es el ms importante: si no modelamos correctamente el sistema, el resto del
trabajo ser intil. En nuestro ejemplo, tenemos dos algoritmos: el del proceso lector, y el
del escritor.
// Proceso lector
Lector() {
... leer de la base de datos ...
}
// Proceso escritor
Escritor() {
... escribir en la base de datos ...
}

No ha sido muy complicado...

Paso 2. Localizar los puntos de bloqueo y las condiciones


Ahora tenemos que ubicar las zonas del cdigo donde los lectores y escritores deberan
bloquearse y escribir las condiciones de bloqueo, en lenguaje natural:
// Proceso lector
Lector() {
bloquearse si hay algn escritor trabajando
... leer de la base de datos ...
}
// Proceso escritor
Escritor() {
bloquearse si hay otro proceso trabajando (lector o escritor)
... escribir en la base de datos ...
}

2007-2014 J.M. Santos Espino, A. Quesada, E. Rodrguez, F. Santana

Paso 3. Escribir las condiciones de bloqueo de manera formal


Escribir las condiciones de bloqueo ha sido fcil, porque lo hemos hecho en lenguaje
natural. El siguiente paso es delicado, porque hay que reescribir estas condiciones de
manera que lo pueda entender un compilador. Por tanto, tendremos que crear variables de
estado que nos permitan conocer si hay lectores o escritores trabajando. No slo se trata de
reescribir las condiciones, sino tambin de escribir el cdigo necesario para que las
variables de estado se mantengan siempre con un valor correcto.
En nuestro ejemplo, crearemos dos variables nlectores y nescritores que mantendrn la
cantidad de procesos lectores y escritores que estn operando con la base de datos.
// Variables de estado
int nlectores = 0;
int nescritores = 0;
// Proceso lector
Lector() {
// bloquearse si hay algn escritor trabajando
bloquearse si: nescritores > 0
nlectores++;
... leer de la base de datos
nlectores--;
}
// Proceso escritor
Escritor() {
// bloquearse si hay otro proceso trabajando (lector o escritor)
bloquearse si: nlectores>0 or nescritores>0
nescritores++;
... escribir en la base de datos
nescritores--;
}

Paso 4. Localizar los puntos de desbloqueo


Ahora ya podemos identicicar los puntos en el cdigo en los que deberan ir los
desbloqueos. Cmo localizamos estos puntos? Tenemos que ver dnde cambian las
variables de estado y en qu puntos podran cambiar de manera que varen las condiciones
de bloqueo que mantienen detenidos a otros procesos.
Es importante marcar si el desbloqueo se hace sobre un solo proceso en espera, o bien hay
que desbloquear a todos los procesos. Si no tenemos claro cul es el caso, optaremos por
desbloquearlos a todos: la solucin ser menos eciciente, pero por lo menos ser correcta.
En nuestro caso, podemos observar que hay dos puntos de desbloqueo:
cuando un lector cinaliza (podra desbloquearse un escritor) y
cuando un escritor cinaliza (podran desbloquearse todos los lectores en espera, o
bien un escritor).

2007-2014 J.M. Santos Espino, A. Quesada, E. Rodrguez, F. Santana

El segundo punto de desbloqueo es interesante, porque puede dar lugar a dos clases de
desbloqueos (a los lectores o al escritor). Hay que anotar esos dos posibles desbloqueos,
porque cada uno generar cdigo diferente.
Marcaremos los puntos de desbloqueo en el cdigo de esta forma:
// Variables de estado
int nlectores = 0;
int nescritores = 0;
// Proceso lector
Lector() {
// bloquearse si hay algn escritor trabajando
bloquearse si: nescritores > 0
nlectores++;
... leer de la base de datos
nlectores--;
si nlectores == 0 desbloquear a un escritor
}
// Proceso escritor
Escritor() {
// bloquearse si hay otro proceso trabajando (lector o escritor)
bloquearse si: nlectores>0 or nescritores>0
nescritores++;
... escribir en la base de datos
nescritores--;
si hay lectores esperando desbloquear a todos los lectores
si no hay lectores y hay escritores esperando desbloquear a un escritor
}

No hay que preocuparse mucho por decinir las condiciones que preceden a los
desbloqueos. Si no se tiene muy claro cules pueden ser esas condiciones, se puede indicar
que el desbloqueo ocurra siempre. En ese caso el algoritmo seguir siendo correcto,
aunque algo menos eciciente, ya que se harn desbloqueos de ms. En caso de duda, mejor
forzar al desbloqueo siempre.

Paso 5. Aplicar los patrones de cdigo


En este punto se acab la parte creativa de la receta. Todo lo que viene a continuacin es
pura rutina. De hecho, el cdigo que vamos a escribir se podra generar automticamente.
Hay que transformar las acciones de bloqueo en operaciones P de semforos utilizados
como colas de espera. Por cada condicin de bloqueo, crearemos un semforo inicializado a
cero y un contador. En cada situacin de bloqueo insertaremos la plantilla genrica de
cdigo de bloqueo. Las acciones de desbloqueo se transformarn en operaciones V en esas
colas de espera. Todo ello, siguiendo los patrones de cdigo presentados anteriormente.

2007-2014 J.M. Santos Espino, A. Quesada, E. Rodrguez, F. Santana

Siguiendo las reglas generales, para nuestro ejemplo tenemos que crear estos objetos:
Un mutex, que siempre es necesario.
Dos colas de espera, una para gestionar el bloqueo de los lectores ( colaLectores) y
otra para gestionar el bloqueo de los escritores ( colaEscritores).
Dos contadores enteros, uno para cada cola.
Tambin es importante colocar el cdigo que actualiza las variables de estado dentro de la
proteccin del mutex. En nuestro caso, se trata de la manipulacin de las variables
nlectores y nescritores.

Cdigo de inicializacin
// Variables de estado
int nlectores = 0;
int nescritores = 0;
// Control de la exclusin mutua
Semforo mutex = 1;
// Cola de espera de los lectores
Semforo colaLectores = 0;
int contadorLectores = 0;
// Cola de espera de los escritores
Semforo colaEscritores = 0;
int contadorEscritores = 0;

Proceso lector
Lector() {
// bloquearse si hay algn escritor trabajando
// bloquearse si: nescritores > 0
P ( mutex );
while ( nescritores > 0 ) {
contadorLectores++;
V ( mutex );
P ( colaLectores );
P ( mutex );
}
nlectores++;
V ( mutex );
... leer de la base de datos ...
// actualizacin de variables de estado y desbloqueo
P ( mutex );
2007-2014 J.M. Santos Espino, A. Quesada, E. Rodrguez, F. Santana

nlectores--;
if ( nlectores == 0 ) {
if ( contadorEscritores > 0 ) {
contadorEscritores--;
V ( colaEscritores );
}
}
V ( mutex );
}

Proceso escritor
Escritor() {
// bloquearse si hay otro proceso trabajando (lector o escritor)
// bloquearse si: nlectores>0 or nescritores>0
P ( mutex );
while ( nlectores>0 or nescritores > 0 ) {
contadorEscritores++;
V ( mutex );
P ( colaEscritores );
P ( mutex );
}
nescritores++;
V ( mutex );
... escribir en la base de datos ...
// actualizacin de variables de estado y desbloqueo
P ( mutex );
nescritores--;
if ( contadorLectores > 0 ) {
// desbloqueamos a todos los lectores
while ( contadorLectores > 0 ) {
contadorLectores--;
V ( colaLectores );
}
} else {
// desbloqueamos a un escritor
if ( contadorEscritores > 0 ) {
contadorEscritores--;
V ( colaEscritores );
}
}
V ( mutex );
}

2007-2014 J.M. Santos Espino, A. Quesada, E. Rodrguez, F. Santana

Paso 6. Op:mizar el cdigo


El algoritmo anterior ya es una solucin correcta al problema de los lectores y escritores.
No obstante, se puede mejorar de muchas formas. Por ejemplo, la variable nescritores
podra ser un booleano.

3 Cerrojos y variables condicin


Los patrones de cdigo aqu presentados son tan rutinarios que se pueden encapsular en
un tipo abstracto de datos que ofrezca unas operaciones de alto nivel de bloquear y
desbloquear. De hecho, muchos lenguajes y frameworks de programacin concurrente
ofrecen estos servicios. Por ejemplo, la biblioteca pthreads ofrece los llamados cerrojos
(mutex) y variables condicin. Los cerrojos tienen sendas operaciones de bloqueo y
desbloqueo (lock/unlock) y equivalen al semforo mutex de nuestra receta. Una variable
condicin equivale al conjunto de semforo+contador utilizado en la receta para mantener
una cola de espera. Normalmente ofrece tres operaciones: bloqueo, desbloqueo a un
proceso y desbloqueo a toda la cola ( wait/signal/broadcast).
En el caso del lenguaje Java, los objetos pueden invocar a varias operaciones similares: una
operacin de bloqueo en cola ( wait), una operacin de desbloqueo individual ( notify) y una
operacin de desbloqueo general a todos los hilos en espera ( notifyAll). Cada objeto en Java
tiene su propio cerrojo o mutex, que se adquiere y libera cuando se invoca a un mtodo de
forma exclusiva (los mtodos se pueden declarar como de acceso exclusivo utilizando la
palabra reservada synchronized).

4 Versin simplicada: cola de espera nica


La receta aqu descrita utiliza una cola por cada bloqueo encontrado en el cdigo. En
realidad, el problema se puede resolver con una sola cola para todos los procesos. En
algunos algoritmos esto podra llegar a ser bastante ineciciente, pero por lo menos
funciona.
La receta para la cola nica es ms sencilla: todos los bloqueos van al mismo semforo cola
y en todos los puntos de desbloqueo se deben reactivar todos los procesos de la cola.
A continuacin mostramos el cdigo de los lectores y escritores que hace uso de una cola
nica. Como el cdigo de bloqueo y desbloqueo ahora es nico para todas las situaciones,
se ha empaquetado en sendas funciones bloqueo() y desbloqueoGeneral(). Estas
funciones seran los equivalentes de las operaciones wait() y broadcast() de las variables
condicin, o las operaciones wait() y notifyAll() de Java.
Puede apreciarse que el cdigo queda mucho ms compacto que el que obtuvimos en el
apartado anterior, aunque se paga el precio de ms desbloqueos innecesarios. Por ejemplo,
cuando un escritor termina, se desbloquea a todos los procesos, tanto lectores como
escritores, a pesar de que slo un tipo de procesos puede proseguir.

Cdigo de inicializacin
// Variables de estado
int nlectores = 0;
int nescritores = 0;

2007-2014 J.M. Santos Espino, A. Quesada, E. Rodrguez, F. Santana

10

Control de bloqueos y desbloqueos (cola nica)


// Variables globales
Semforo mutex = 1;
Semforo cola = 0;
int procesosEnCola = 0;
// Rutinas de bloqueo y desbloqueo
void bloqueo() {
procesosEnCola++;
V(mutex);
P(cola);
P(mutex);
}
void desbloqueoGeneral() {
while ( procesosEnCola > 0 ) {
procesosEnCola--;
V (cola);
}
}

Proceso lector
Lector() {
// bloquearse si hay algn escritor trabajando
P ( mutex );
while ( nescritores > 0 ) {
bloqueo();
}
nlectores++;
V ( mutex );
... leer de la base de datos ...
// actualizacin de variables de estado y desbloqueo general
P ( mutex );
nlectores--;
desbloqueoGeneral();
V ( mutex );
}

2007-2014 J.M. Santos Espino, A. Quesada, E. Rodrguez, F. Santana

11

Proceso escritor
Escritor() {
// bloquearse si hay otro proceso trabajando (lector o escritor)
P ( mutex );
while ( nlectores>0 or nescritores > 0 ) {
bloqueo();
}
nescritores++;
V ( mutex );
... escribir en la base de datos ...
// actualizacin de variables de estado y desbloqueo general
P ( mutex );
nescritores--;
desbloqueoGeneral();
V ( mutex );
}

5 Conclusin
Hemos presentado una tcnica general para resolver problemas de sincronizacin a travs
de semforos. La tcnica no garantiza una solucin ptima, pero s da la seguridad de que el
cdigo es correcto y que el procedimiento de resolucin siempre es el mismo, sin necesidad
de requerir ingenio, creatividad o inspiracin.
La estrategia ms simple requiere slo tres variables globales: un mutex, una cola y un
contador de esperas. Es la recomendable cuando lo que se quiere es obtener una solucin
correcta sin que preocupe la eciciencia.
Esta tcnica general es la que ha dado lugar a la creacin de herramientas de programacin
concurrente presentes en los actuales entornos de programacin. La tcnica nos puede
servir para aplicarla en cualquiera de esos entornos ( pthreads, Java, etc.).

2007-2014 J.M. Santos Espino, A. Quesada, E. Rodrguez, F. Santana

12

Você também pode gostar