Está en la página 1de 6

Tema 05 – Sincronización

Concepto de sección crítica y exclusión mutua

▪ Sección Crítica: son una secuencia de instrucciones que deben ejecutarse indivisiblemente.

▪ Exclusión mutua: Es la sincronización requerida para proteger una sección crítica. Si dos tareas no
comparten variables, entonces no es necesario usar exclusión mutua, aunque pueden necesitar
sincronización (una tarea solo puede ejecutarse cuando otra tarea se encuentra en un determinado
estado). Un ejemplo típico sería el problema del productor-consumidor, donde dos tareas intercambian
datos a través de un búfer; un productor no debe introducir un dato si el búfer está lleno, y un
consumidor no debe extraer un dato si el búfer está vacío.

Uso de variables compartidas, y el efecto de la condición de carrera


Las variables es un modo de comunicación entre tareas. Las variables compartidas son objetos que se
comparten entre las tareas.

Son relativamente fáciles de implementar si existe una memoria compartida, por lo que son la solución ideal
para sistemas multiprocesador con memoria compartida. Son accesibles por varias tareas para lectura y
escritura, requiriendo sincronización para asegurar una correcta interacción cuando varios procesos acceden a
información compartida por variables.

El acceso y modificación de las mismas se debe realizar de forma atómica e indivisible, es decir, ningún proceso
puede interrumpir o interactuar con una variable si se está llevando a cabo una operación, y, en caso contrario,
el resultado sería incorrecto.

La condición de carrera es la dependencia del resultado en el orden de ejecución de los procesos, ya que, en
cierta manera, existe una competición entre procesos para acceder a la variable compartida.

Definición de espera activa y espera ocupada

▪ Espera Activa: es uno de los mecanismos principales para la implementación de los mecanismos de
sincronización. El proceso debe comprobar continuamente el valor de la variable para detectar posibles
cambios en su estado.

Se utiliza una variable para representar la condición que debe satisfacerse para que el proceso pueda
continuar su ejecución; mientras tanto, éste utiliza su tiempo de ejecución para comprobar el valor de
la variable.

El proceso que señaliza la condición, únicamente modifica el valor de la bandera. Sin embargo, es un
método poco eficiente, ya que:
➢ El bucle de espera no realiza trabajo útil
➢ En sistemas distribuidos puede provocar un tráfico excesivo
➢ Es difícil utilizar colas de espera
➢ Existe posibilidad de bloqueo activo o livelock

Además, existen una serie de dificultades en el uso de la espera activa, como:


➢ Los protocolos son difíciles de entender, programar y demostrar su corrección.
➢ Las pruebas no examinan todas las posibles secuencias de ejecución.
➢ Es posible que existan secuencias que lleven a una violación de la exclusión mutua.
➢ Los bucles de espera activa son ineficientes.
➢ Una tarea que haga un mal uso de las variables compartidas puede corromper todo el sistema.
▪ Espera Ocupada: es un proceso que comprueba continuamente una condición para ver si ya es capaz
de continuar.

El uso de semáforos y sus distintos tipos


El semáforo es un mecanismo simple para programar la exclusión mutua y la sincronización de condición.
Aportan principalmente dos beneficios: simplifican los protocolos de sincronización y eliminan la necesidad de
espera activa.

Su funcionamiento se basa en una variable entera sobre la que se definen tres operaciones atómicas:

• Inicialización: asigna un valor inicial al semáforo y, tras ello, solo puede ser modificado únicamente por
las operaciones de espera (wait) y señalización (signal), pero sin poder leer su valor actual.

• Espera (wait): disminuye en uno el valor del semáforo, siempre y cuando la variable tenga un valor
mayor que 0. En caso contrario, la tarea se postpone.

• Señalización (signal): incrementa en uno el valor del semáforo.

El semáforo es útil para solucionar, entre otros, los siguientes problemas:

▪ Tiempo de espera de una Notificación: Las notificaciones son necesarias cuando un proceso tiene que
esperar a un evento o a que se ejecute una sección de código, antes de seguir con su ejecución. Ejemplo:
se tiene un proceso A en espera (wait) de un evento de un proceso B, y el evento B, llegado el momento,
despierta al proceso A tras haber aumentado (signal) el valor del semáforo.

▪ Encuentro entre procesos: Es una generalización de la notificación. Dos procesos se esperarán


mutuamente antes de continuar con su ejecución. El primero que llegue al punto de encuentro, deberá
esperar al otro para poder proseguir con la ejecución.

▪ Mutex: son semáforos usados para resolver el problema de la exclusión mutua. Un ejemplo se da en la
Figura 1, donde se inicia el semáforo a 1.

Luego, cada proceso que quiera entrar en la sección crítica deberá esperar, así como señalizar al salir
de la misma. El primer proceso que encuentre un valor positivo podrá seguir ejecutándose, y los demás
procesos encontrarán un valor cero manteniéndose a la espera sin ejecutar su código.

El mutex será creado en estado desbloqueado, pudiendo ser adquirido por una tarea. Cuando se
adquiera, pasa al estado bloqueado.

Muchas implementaciones de mutex también soportan el bloqueo recursivo, el cual permite que, la
tarea que es dueña, lo adquiera múltiples veces en estado bloqueado. Algunas implementaciones de
mutex también disponen de una eliminación segura de tareas.
Figura 1: Semáforo Mutex

▪ Barrera: Es una generalización del encuentro entre procesos. Varios procesos deben esperar a reunirse
en un punto de la ejecución antes de poder continuar.

Existen varios tipos de semáforos, los cuales son:

▪ Semáforos Binarios: son semáforos que toman los valores 0 o 1. Cuando el valor es 0, el semáforo se
encuentra en estado no disponible, y si es 1, su estado es disponible o lleno.

▪ Semáforos Contadores: son semáforos donde las operaciones sobre ellos aumentan o disminuyen en
uno a un contador.

Cualquier tipo de semáforo puede ser borrado desde cualquier tarea especificando su identificador, y haciendo
la correspondiente llamada de ejecución para ello. Cuando se borra un semáforo, las tareas que están
bloqueadas en su lista de espera de desbloquean y pasan al estado preparado o al estado de ejecución, si la
tarea tiene la máxima prioridad. Si alguna tarea intenta adquirir un semáforo ya borrado, se le devolverá un
error al no existir este.

Citar que liberar un semáforo no es lo mismo que borrarlo. Cualquier tarea puede liberar un semáforo, pero
mutex solo puede liberarlo aquella tarea que lo adquirió.

Formas de llamadas para solicitar o adquirir un semáforo

▪ Mantenerse esperando: la tarea se queda bloqueada hasta poder adquirir el semáforo.

▪ Esperar con tiempo límite (timeout): la tarea queda bloqueada hasta que es capaz de adquirir el
semáforo o hasta que pasa un determinado intervalo de tiempo. En este caso, la tarea es eliminada de
la lista de espera del semáforo y pasa al estado listo o de ejecución.

▪ Sin espera: la tarea hace una solicitud al semáforo, pero no se bloquea si no está disponible.

Borrado, vaciado y limpieza de las tareas asignadas a un semáforo


Para borrar, vaciar, o limpiar todas las tareas, de una lista de tareas en espera de un semáforo, algunos kernels
soportan una operación de vaciado (flush).

La operación de vaciado es útil para transmitir señales a un grupo de tareas. Cuando la última tarea termine
de hacer lo que necesita, la tarea puede ejecutar una operación de vaciado en el semáforo común. Esta
operación libera todas las tareas que esperan en la lista de espera del semáforo.

El escenario de sincronización que acabamos de describir también se denomina encuentro o cita de hilos o
procesos, cuando las ejecuciones de múltiples tareas deben encontrarse en algún momento para sincronizar
el control de la aplicación.

También, se suele disponer de algún mecanismo que permite a los desarrolladores obtener información del
semáforo. Estos casos suelen tener operaciones del tipo show info, que muestran información de los semáforos,
o show blocked tasks, que obtienen una lista de los identificadores de tareas que están bloqueadas por un
semáforo.

Usos de los semáforos

▪ Sincronización con wait y signal: Dos tareas se pueden comunicar para sincronizarse sin necesidad de
intercambio de datos.

▪ Sincronización de múltiples tareas con wait y signal: Cuando se sincronizan varias tareas se puede
requerir una operación de vaciado como se ha indicado anteriormente.
▪ Sincronización en el seguimiento de la ejecución: A veces, la velocidad a la que se ejecuta la tarea de
señalización es mayor que la de la tarea señalada. En ese caso, se necesita de un mecanismo para contar
cada ocurrencia de señalización, proporcionado por un semáforo contador.

▪ Sincronización única de acceso a recursos compartidos: Esta es una de las utilidades típicas de
utilización de los semáforos, proporcionar exclusión mutua en el acceso a recursos compartidos. Un
recurso compartido puede ser una localización de memoria, estructura de datos o dispositivo de E/S.

▪ Sincronización múltiple de acceso a recursos compartidos: En este caso se utilizaría un semáforo


contador. Hay que considerar que es para recursos equivalentes, ya que el semáforo se inicializa en
función al número de recursos disponibles.

Inconvenientes del uso de los semáforos


Podemos reseñar que el semáforo es una primitiva de sincronización de bajo nivel simple y elegante, pero un
programa construido sólo con semáforos es propenso a errores. Una ocurrencia del semáforo ausente o mal
colocado, provoca que todo el programa falle.

El semáforo permite garantizar el acceso con exclusión mutua, y existen soluciones de más alto nivel que
proporcionan directamente esta exclusión mutua.

El problema del interbloqueo y del aplazamiento indefinido


Un problema relevante en el control de recursos es el de la condición de carrera (cuando dos o más tareas
acceden a más de un recurso compartido, existe la posibilidad de que se genere una situación indeseable en la
que, los procesos involucrados, queden incapacitados para seguir su ejecución).

De esta manera, se produce una espera mutua en la que, si no se dispone de los mecanismos adecuados, el
sistema puede quedar inmerso en un bloqueo indefinido, denominado interbloqueo.
Las regiones críticas condicionales
Una región crítica es una sección de código que tiene garantizada la ejecución en exclusión mutua, e intentan
solventar los problemas asociados con los semáforos. Una región crítica condicional es un bloque de código
del que se garantiza su ejecución en exclusión mutua en relación con una variable de condición.

Así, antes de entrar en dicha región, se comprueba la condición. En caso de que la evaluación sea cierta, se
permite la entrada, y en caso contrario, la tarea queda bloqueada.

Monitores: forma y uso


Los monitores son una solución de alto nivel basada en las regiones críticas, tratándose de una solución más
estructurada y eficiente, y se definen como un tipo abstracto de datos. Contienen los datos y procedimientos
necesarios para la asignación y control de recursos compartidos, proporcionando exclusión mutua sin tener
que codificar la sincronización.

Puesto que pueden requerir otro tipo de sincronización, se utilizan variables de condición similares a los
semáforos: wait, para que un proceso espere una condición y signal, para despertar un proceso.

Las instancias de tipo monitor solo pueden ejecutarse mediante un proceso a la vez, y un proceso no puede
acceder directamente a las variables de datos compartidos, requiriéndose procedimientos para permitir que
un solo proceso acceda a las variables de datos compartidos.

En la práctica, esto se consigue, de diferentes formas, asegurando que las llamadas se ejecuten en exclusión
mutua (Ejemplo: disponer de un bloqueo asociado con el monitor, y requerir que cada procedimiento o función
adquiera el bloqueo antes de que pueda continuar su ejecución).

Problemas de los monitores


Uno de ellos es la llamada a un procedimiento monitor desde otro monitor (llamadas anidadas), que puede
causar problemas si el procedimiento anidado se suspende en una variable de condición.

La exclusión mutua, en la última llamada del monitor, será abandonada por la tarea debido a la semántica de
la espera y las operaciones equivalentes. Sin embargo, la exclusión mutua no será abandonada en los monitores
desde los que se realizó la llamada anidada. Los procesos que intentan invocar procedimientos en estos
monitores quedarán bloqueados, lo cual tiene implicaciones en el rendimiento.

Otro de los problemas es que no se manejan bien con las sincronizaciones de condición fuera del monitor,
recurriendo a primitivas de bajo nivel del tipo semáforo.

Sincronización en Java
Java dispone de una concurrencia integrada y un modelo orientado a objetos, lo que le proporciona un
mecanismo para que los monitores puedan implementarse en el contexto de clases y objetos. Asociado con
cada objeto, hay un bloqueo de exclusión mutua.

La aplicación no puede acceder directamente a este bloqueo, pero se ve afectado por el modificador del
método synchronized y por la sincronización del bloque. En el método con el modificador synchronized, el
acceso al mismo solo puede continuar una cuando se ha obtenido el bloque asociado al objeto.

Por ello, los métodos sincronizados tienen acceso mutuamente excluido al objeto encapsulado de datos. Los
métodos no sincronizados no requieren del bloqueo y, por tanto, se pueden llamar en cualquier momento.
Para obtener la exclusión mutua total, cada método que acceda a datos encapsulados debe estar etiquetado
como synchronized.

La sincronización de bloques proporciona un mecanismo por el cual un bloque de código puede etiquetarse
como sincronizado. La palabra clave synchronized toma como parámetro un objeto cuyo bloqueo se necesita
obtener antes de que se pueda continuar.

El bloque synchronized, en toda su generalidad, puede socavar una de las ventajas de los mecanismos tipo
monitor, es decir, la ventaja de encapsular las restricciones de sincronización asociadas con un objeto en un
solo lugar en el programa.

Esto se debe a que no es posible comprender la sincronización asociada con un objeto en particular,
simplemente mirando el objeto en sí mismo, cuando otros objetos pueden nombrar a ese objeto en una
declaración sincronizada. Sin embargo, con un uso cuidadoso, esta función aumenta el modelo básico y permite
programar restricciones de sincronización más expresivas.

Implementación de semáforos y monitores en Java


Java admite un modelo de comunicación y sincronización similar a un monitor. Sin embargo, también se
proporcionan varios paquetes estándar que admiten utilidades de concurrencia.

Uno de estos proporciona clases de propósito general para soportar diferentes enfoques de sincronización, en
donde se incluyen los semáforos, que pueden implementarse fácilmente en Java:
Las llamadas al método acquire se bloquean hasta que se pueda decrementar el semáforo (se podría agregar
un tiempo de espera a esta definición para evitar un bloqueo indefinido), y las llamadas a release incrementan
el semáforo, dando como como resultado que se libere un hilo bloqueado.

En cuanto a la implementación de monitores, es necesario definir una clase cuyos métodos públicos sean todos
etiquetados con la palabra clave synchronized.

Dispondrá de tres métodos: wait, que se trata de una invocación que provoca la suspensión del hilo actual;
notifiy, que despierta un solo hilo de los que se encuentren esperando; y notifyAll, que despierta a todos los
hilos esperando en el objeto. Estos métodos deben ser llamados por un hilo que haya obtenido el bloqueo
sobre una instancia de objeto, pudiéndolo hacer de tres formas:

▪ Ejecutando un método sincronizado del objeto


▪ Ejecutando un bloque de código que sincroniza con el objeto
▪ Para objetos de tipo Class, ejecutando un método sincronizado estático de la clase

También podría gustarte