Descargar

Sincronización entre procesos (página 2)


Partes: 1, 2, 3

Hilos de ejecución o thread

Un hilo de ejecución o thread , en sistemas operativos, es una característica que permite a una aplicación realizar varias tareas concurrentemente. Los distintos hilos de ejecución comparten una serie de recursos tales como el espacio de memoria, los archivos abiertos, situación de autenticación, etc. Esta técnica permite simplificar el diseño de una aplicación que debe llevar a cabo distintas funciones simultáneamente.

Los hilos de ejecución que comparten los mismos recursos, sumados a estos recursos, son en conjunto conocidos como un proceso. El hecho de que los hilos de ejecución de un mismo proceso compartan los recursos hace que cualquiera de estos hilos pueda modificar éstos. Cuando un hilo modifica un dato en la memoria, los otros hilos acceden e ese dato modificado inmediatamente a en la siguiente figura podemos observar un ejemplo de un hilo de ejecución.

Lo que es propio de cada hilo es el contador de programa, la pila de ejecución y el estado de la CPU (incluyendo el valor de los registros).

El proceso sigue en ejecución mientras al menos uno de sus hilos de ejecución siga activo. Cuando el proceso es terminado, todos sus hilos de ejecución también lo son. Asimismo en el momento en el que todos los hilos de ejecución finalizan, el proceso no existe más y todos sus recursos son liberados.

Algunos lenguajes de programación tienen características de diseño expresamente creadas para permitir a los programadores lidiar con hilos de ejecución (como Java). Otros (la mayoría) desconocen la existencia de hilos de ejecución y éstos deben ser creados mediante llamadas de biblioteca especiales que dependen del sistema operativo en el que estos lenguajes están siendo utilizados (como es el caso del C y del C++).

Un ejemplo de la utilización de hilos es tener un hilo atento a la interfaz gráfica (iconos, botones, ventanas), mientras otro hilo hace una larga operación internamente. De esta manera el programa responde de manera más ágil a la interacción con el usuario. También pueden ser utilizados por una aplicación servidora para dar servicio a múltiples clientes de procesos.

Diferencias entre hilos y procesos

Los hilos se distinguen de los tradicionales procesos en que los procesos son generalmente independientes, llevan bastante información de estados, e interactúan sólo a través de mecanismos de comunicación dados por el sistema. Por otra parte, muchos hilos generalmente comparten otros recursos directamente. En muchos de los sistemas operativos que proveen facilidades para los hilos, es más rápido cambiar de un hilo a otro dentro del mismo proceso, que cambiar de un proceso a otro. Este fenómeno se debe a que los hilos comparten datos y espacios de direcciones, mientras que los procesos al ser independientes no lo hacen. Al cambiar de un proceso a otro el sistema operativo (mediante el dispatcher) genera lo que se conoce como overhead, que es tiempo desperdiciado por el procesador para realizar un cambio de modo (mode switch), en este caso pasar del estado de Running al estado de Waiting o Bloqueado y colocar el nuevo proceso en Running. En los hilos como pertenecen a un mismo proceso al realizar un cambio de hilo este overhead es casi despreciable.

Sistemas operativos como Windows NT, OS/2 y Linux (2.5 o superiores) han dicho tener hilos 'baratos', y procesos 'costosos' mientras que en otros sistemas no hay una gran diferencia.

Funcionalidad de los hilos

Al igual que los procesos, los hilos poseen un estado de ejecución y pueden sincronizarse entre ellos para evitar problemas de compartimiento de recursos. Generalmente, cada hilo tiene una tarea especifica y determinada, como forma de aumentar la eficiencia del uso del procesador.

Estados de un hilo

Los principales estados de los hilos son: Ejecución, Listo y Bloqueado. No tiene sentido asociar estados de suspensión de hilos ya que es un concepto de proceso. En todo caso, si un proceso está expulsado de la memoria principal (ram), todos sus hilos deberán estarlo ya que todos comparten el espacio de direcciones del proceso.

Cambio de estados

  • Creación: Cuando se crea un proceso se crea un hilo para ese proceso. Luego, este hilo puede crear otros hilos dentro del mismo proceso. El hilo tendrá su propio contexto y su propio espacio de pila, y pasara a la cola de listas.
  • Bloqueo: Cuando un hilo necesita esperar por un suceso, se bloquea (salvando sus registros). Ahora el procesador podrá pasar a ejecutar otro hilo que esté en la cola de Listos mientras el anterior permanece bloqueado.
  • Desbloqueo: Cuando el suceso por el que el hilo se bloqueó se produce, el mismo pasa a la cola de Listos.
  • Terminación: Cuando un hilo finaliza se liberan tanto su contexto como sus pilas.

Ventajas de los hilos contra procesos

Si bien los hilos son generados a partir de la creación de un proceso, podemos decir que un proceso es un hilo de ejecución, conocido como Monohilo. Pero las ventajas de los hilos se dan cuando hablamos de Multihilos, que es cuando un proceso tiene múltiples hilos de ejecución los cuales realizan actividades distintas, que pueden o no ser cooperativas entre sí. Los beneficios de los hilos se derivan de las implicaciones de rendimiento.

  1. Se tarda mucho menos tiempo en crear un hilo nuevo en un proceso existente que en crear un proceso. Algunas investigaciones llevan al resultado que esto es así en un factor de 10.
  2. Se tarda mucho menos en terminar un hilo que un proceso, ya que cuando se elimina un proceso se debe eliminar el BCP del mismo, mientras que un hilo se elimina su contexto y pila.
  3. Se tarda mucho menos tiempo en cambiar entre dos hilos de un mismo proceso
  4. Los hilos aumentan la eficiencia de la comunicación entre programas en ejecución. En la mayoría de los sistemas en la comunicación entre procesos debe intervenir el núcleo para ofrecer protección de los recursos y realizar la comunicación misma. En cambio, entre hilos pueden comunicarse entre sí sin la invocación al núcleo. Por lo tanto, si hay una aplicación que debe implementarse como un conjunto de unidades de ejecución relacionadas, es más eficiente hacerlo con una colección de hilos que con una colección de procesos separados.

Sincronización de hilos

Todos los hilos comparten el mismo espacio de direcciones y otros recursos como pueden ser archivos abiertos. Cualquier modificación de un recurso desde un hilo afecta al entorno del resto de los hilos del mismo proceso. Por lo tanto, es necesario sincronizar la actividad de los distintos hilos para que no interfieran unos con otros o corrompan estructuras de datos.

Una ventaja de la programación multihilo es que los programas operan con mayor velocidad en sistemas de computadores con múltiples CPUs (sistemas multiprocesador o a través de grupo de máquinas) ya que los hilos del programa se prestan verdaderamente para la ejecución concurrente. En tal caso el programador necesita ser cuidadoso para evitar condiciones de carrera (problema que sucede cuando diferentes hilos o procesos alteran datos que otros también están usando), y otros comportamientos no intuitivos. Los hilos generalmente requieren reunirse para procesar los datos en el orden correcto. Es posible que los hilos requieran de operaciones atómicas para impedir que los datos comunes sean cambiados o leídos mientras estén siendo modificados, para lo que usualmente se utilizan los semáforos. El descuido de esto puede generar interbloqueo.

Formas de multihilos

Los sistemas operativos generalmente implementan hilos de dos maneras:

  • Multihilo apropiativo: permite al sistema operativo determinar cuándo debe haber un cambio de contexto. La desventaja de esto es que el sistema puede hacer un cambio de contexto en un momento inadecuado, causando un fenómeno conocido como inversión de prioridades y otros problemas.
  • Multihilo cooperativo: depende del mismo hilo abandonar el control cuando llega a un punto de detención, lo cual puede traer problemas cuando el hilo espera la disponibilidad de un recurso.

El soporte de hardware para multihilo desde hace poco se encuentra disponible. Esta característica fue introducida por Intel en el Pentium 4, bajo el nombre de HyperThreading.

Usos más comunes

Trabajo interactivo y en segundo plano

Por ejemplo, en un programa de hoja de cálculo un hilo puede estar visualizando los menús y leer la entrada del usuario mientras que otro hilo ejecuta las órdenes y actualiza la hoja de cálculo. Esta medida suele aumentar la velocidad que se percibe en la aplicación, permitiendo que el programa pida la orden siguiente antes de terminar la anterior.

Procesamiento asíncrono

Los elementos asíncronos de un programa se pueden implementar como hilos. Un ejemplo es como los softwares de procesamiento de texto guardan archivos temporales cuando se está trabajando en dicho programa. Se crea un hilo que tiene como función guardar una copia de respaldo mientras se continúa con la operación de escritura por el usuario sin interferir en la misma.

Aceleración de la ejecución

Se pueden ejecutar, por ejemplo, un lote mientras otro hilo lee el lote siguiente de un dispositivo.

Estructuración modular de los programas

Puede ser un mecanismo eficiente para un programa que ejecuta una gran variedad de actividades, teniendo las mismas bien separadas mediante a hilos que realizan cada una de ellas.

Implementaciones

Hay dos grandes categorías en la implementación de hilos:

  • Hilos a nivel de usuario
  • Hilos a nivel de Kernel

También conocidos como ULT (User Level Thread) y KLT (Kernel Level Thread)

Hilos a nivel de usuario (ULT)

En una aplicación ULT pura, todo el trabajo de gestión de hilos lo realiza la aplicación y el núcleo o kernel no es consciente de la existencia de hilos. Es posible programar una aplicación como multihilo mediante una biblioteca de hilos. La misma contiene el código para crear y destruir hilos, intercambiar mensajes y datos entre hilos, para planificar la ejecución de hilos y para salvar y restaurar el contexto de los hilos.

Todas las operaciones descritas se llevan a cabo en el espacio de usuario de un mismo proceso. El kernel continua planificando el proceso como una unidad y asignándole un único estado (Listo, bloqueado, etc.).

Ventajas de los ULT

  • El intercambio de los hilos no necesita los privilegios del modo kernel, por que todas las estructuras de datos están en el espacio de direcciones de usuario de un mismo proceso. Por lo tanto, el proceso no debe cambiar a modo kernel para gestionar hilos. Se evita la sobrecarga de cambio de modo y con esto el sobrecoste o overhead.
  • Se puede realizar una planificación específica. Dependiendo de que aplicación sea, se puede decidir por una u otra planificación según sus ventajas.
  • Los ULT pueden ejecutar en cualquier sistema operativo. La biblioteca de hilos es un conjunto compartido.

Desventajas de los ULT

  • En la mayoría de los sistemas operativos las llamadas al sistema (System calls) son bloqueantes. Cuando un hilo realiza una llamada al sistema, se bloquea el mismo y también el resto de los hilos del proceso.
  • En una estrategia ULT pura, una aplicación multihilo no puede aprovechar las ventajas de los multiprocesadores. El núcleo asigna un solo proceso a un solo procesador, ya que como el núcleo no interviene, ve al conjunto de hilos como un solo proceso.

Una solución al bloqueo mediante a llamadas al sistema es usando la técnica de jacketing, que es convertir una llamada bloqueante en no bloqueante.

Hilos a nivel de núcleo (KLT)

En una aplicación KLT pura, todo el trabajo de gestión de hilos lo realiza el kernel. En el área de la aplicación no hay código de gestión de hilos, únicamente un API (interfaz de programas de aplicación) para la gestión de hilos en el núcleo. Windows 2000, Linux y OS/2 utilizan este método. Linux utiliza un método muy particular en que no hace diferencia entre procesos e hilos, para linux si varios procesos creados con la llamada al sistema "clone" comparten el mismo espacio de direcciones virtuales el sistema operativo los trata como hilos y lógicamente son manejados por el kernel.

Ventajas de los KLT

  • El kernel puede planificar simultáneamente múltiples hilos del mismo proceso en múltiples procesadores.
  • Si se bloquea un hilo, puede planificar otro del mismo proceso.
  • Las propias funciones del kernel pueden ser multihilo

Desventajas de los KLT

  • El paso de control de un hilo a otro precisa de un cambio de modo.

Combinaciones ULT y KLT

Algunos sistemas operativos ofrecen la combinación de ULT y KLT, como Solaris.

La creación de hilos, así como la mayor parte de la planificación y sincronización de los hilos de una aplicación se realiza por completo en el espacio de usuario. Los múltiples ULT de una sola aplicación se asocian con varios KLT. El programador puede ajustar el número de KLT para cada aplicación y máquina para obtener el mejor resultado global.

Sincronización y Comunicación entre procesos

La comunicación entre procesos: necesaria si se desea que varios procesos puedan colaborar para realizar una misma tarea. Sincronización === funcionamiento coordinado en la resolución de una tarea encomendada.

El SO ofrece mecanismos básicos de comunicación, que permiten transferir cadenas de bytes. Deben ser los procesos que se comunican quienes interpreten el significado de las cadenas transferidas para su labor coordinada.

Los mecanismos de comunicación y sincronización son dinámicos. Es decir, cuando se necesita un mecanismo de este estilo, se crea, usa y destruye, de forma que no se establezca de forma definitiva ningún mecanismo de comunicación, ya que ellos podrían producir efectos indeseados. Es decir, la comunicación es algo puntual.

Los servicios básicos de comunicación son:

  1. crear: el proceso solicita la creación del mecanismo
  2. enviar o escribir: el proceso emisor envía información al proceso receptor
  3. recibir o leer: el proceso receptor recibe información
  4. destruir: el proceso solicita la destrucción del mecanismo de comunicación

La comunicación puede ser sincrona y asíncrona:

  1. síncrona: los dos procesos han de ejecutar servicios de forma simultánea. El emisor ha de ejecutar el servicio enviar mientras el receptor ejecuta recibir.
  2. asíncrona: el emisor hace el envío y prosigue su ejecución. El SO ofrece un almacenamiento intermedio para guardar la información enviada, hasta que el receptor la solicite.

Esquema de Sincronización Sincrona

En un sistema operativo multiprogramado los procesos compiten por el acceso a los recursos compartidos o cooperan dentro de una misma aplicación para comunicar información. Ambas situaciones son tratadas por el sistema operativo mediante mecanismos de sincronización que permiten el acceso exclusivo de forma coordinada a los recursos y a los elementos de comunicación compartidos. Según el modelo de sistema operativo descrito anteriormente, basado en colas de procesos y transiciones de estados, los procesos abandonan la CPU para pasar a estado bloqueado cuando requieren el acceso a algún dispositivo, generalmente en una operación de E/S, pasando a estado preparado cuando la operación ha concluido y eventualmente volver a ejecución. La gestión de estos cambios de estado, es decir, los cambios de contexto, es un ejemplo de sección crítica de código dentro del sistema operativo que debe ser ejecutada por éste en exclusión mutua. Otros ejemplos de código que debe protegerse como sección crítica incluyen la programación de los dispositivos de E/S y el acceso a estructuras de datos y buffers compartidos.

Dentro del dentro del núcleo del sistema operativo, el espacio de direcciones es único, por lo que la comunicación se resuelve mediante el uso de variables de memoria compartida. Como contrapartida a la agilidad de este esquema, es necesario utilizar mecanismos explícitos de sincronización para garantizar acceso exclusivo a las variables compartidas. Si se definen buffers o colas compartidas a las que se proporciona acceso exclusivo, se pueden utilizar esquemas de comunicación más elaborados, como es el caso del productor-consumidor. El esquema clienteservidor es un caso particular del productor-consumidor donde los clientes producen peticiones que son consumidas por el servidor de un determinado recurso. Un sistema operativo con estructura cliente-servidor resulta atractivo por la claridad de su diseño. Cuando los procesos que se comunican mediante estos esquemas no comparten el espacio de direcciones, lo que sucede en particular en sistemas basados en micro núcleo, se requieren primitivas de comunicación por paso de mensajes, que, al gestionar implícitamente la sincronización, simplifican la programación de la comunicación.

En los apartados siguientes vamos a tratar el problema de la sección crítica y sus soluciones. La sección crítica no es el único problema a resolver en un sistema operativo. Aunque se utilicen esquemas de comunicación elaborados, las interacciones en el uso de los recursos pueden conducir a interbloqueos, problema que se tratará más adelante.

Sección crítica

En programación concurrente, se define como a la porción de código de un programa de computador el cual accede a un recurso compartido (estructura de datos ó dispositivo) que no debe de ser accedido por mas de un hilo en ejecución (thread). La sección crítica por lo general termina en un tiempo determinado y el hilo, proceso ó tarea solo tendrá que esperar un período determinado de tiempo para entrar. Se necesita de un mecanismo de sincronización en la entrada y salida de la sección crítica para asegurar la utilización exclusiva del recurso, por ejemplo un semáforo.

El acceso concurrente se controla teniendo cuidado de las variables que se modifican dentro y fuera de la sección crítica. La sección crítica se utiliza por lo general cuando un programa multihilo actualiza múltiples variables sin un hilo de ejecución separado que lleve los cambios conflictivos a esos datos. Una situación similar, la sección crítica puede ser utilizada para asegurarse de que un recurso compartido, por ejemplo, una impresora, puede ser accedida por un solo proceso a la vez.

La manera en como se implementan las secciones puede variar dependiendo de los diversos sistemas operativos.

Sólo un proceso puede estar en una sección crítica a la vez.

Modelo de sección crítica

El modelo de sección crítica que vamos a utilizar sigue el siguiente protocolo genérico:

Entrar_SC(esta_SC) /* Solicitud de ejecutar esta_SC */

/* código de esta_SC */

Dejar_SC(esta_SC) /* Otro proceso puede ejecutar esta_SC */

Es decir, cuando un proceso quiere entrar a la sección crítica:

(1) ejecuta Entrar_SC(), y si la sección crítica está ocupada el proceso espera;

(2) ejecuta la sección crítica;

(3) ejecuta Dejar_SC(), permitiendo que entre uno de los procesos en espera.

La decisión de qué proceso es el seleccionado para entrar en el paso (3) puede tener consecuencias importantes, como se comentará más adelante. En general, puede asumirse disciplina FIFO.

Un aspecto fundamental es cómo se realiza la espera en Entrar_SC(), lo que determina el tipo de mecanismo de sincronización que se debe utilizar. Esto dependerá del tiempo que el proceso deba esperar para entrar a la sección crítica. La Figura C muestra un ejemplo de utilización de una sección crítica para implementar sincronización productor-consumidor. Hay que advertir que este ejemplo sólo es válido para un productor y un consumidor. Con n productores y m consumidores se producirían condiciones de carrera. Se propone como ejercicio la modificación de este código para el caso general.

Figura C. Una implementación del productor-consumidor (sólo válido para un productor y un consumidor).

Si se va a esperar durante un tiempo que compense el coste de los cambios de contexto asociados a bloquear y desbloquear el proceso, la exclusión mutua se implementa como de largo plazo. La espera por bloqueado resulta entonces más eficiente que la espera activa o mucho menos restrictiva que la inhibición de interrupciones. Un ejemplo de exclusión mutua de largo plazo es el acceso a un driver de un dispositivo, como el de disco.

Vamos a abordar a continuación los mecanismos de sincronización en los sistemas operativos, tanto los de corto plazo, que se basan en la espera por ocupado (incluyendo inhibición de interrupciones y cerrojos de espera activa), como los de espera por bloqueado (primitivas de dormir/despertar y semáforos), además de un mecanismo de comunicación de alto nivel semántico como es el paso de mensajes. Existen otros mecanismos, como regiones críticas y monitores, que no vamos a desarrollar en la siguiente investigación.

Antes de ello es preciso revisar las propiedades que deben cumplir estos mecanismos.

Propiedades del acceso exclusivo a secciones críticas:

Como criterios de validez de un mecanismo de sincronización nos referiremos al cumplimiento de las siguientes condiciones enunciadas por Dijkstra para el acceso exclusivo a una sección crítica.

  1. SC.

  2. Exclusión mutua. No puede haber más de un proceso simultáneamente en la
  3. No interbloqueo. Ningún proceso fuera de la SC puede impedir que otro entre a la SC.
  4. No inanición. Un proceso no puede esperar por tiempo indefinido para entrar a la SC.
  5. Independencia del hardware. No se pueden hacer suposiciones acerca del número de procesadores o de la velocidad relativa de los procesos.

Suposición inicial adicional: las instrucciones del Lenguaje Máquina son atómicas y se ejecutan secuencialmente.

Además, existen otros criterios que determinan la calidad del mecanismo y que fundamentalmente se refieren a su rendimiento, como son la productividad (número de operaciones de sincronización por unidad de tiempo que el mecanismo es capaz de soportar) y el tratamiento equitativo entre los procesos (por ejemplo, siguiendo una política FIFO para entrar a la sección crítica).

Espera por ocupado

La espera por ocupado engloba una serie de mecanismos de sincronización que proporcionan acceso a secciones críticas de corto plazo. El más sencillo y más utilizado en los sistemas operativos clásicos es la inhibición de interrupciones. En multiprocesadores es necesario utilizar cerrojos de espera activa.

Inhibición de interrupciones

Las secciones críticas protegidas por interrupciones se especifican de la siguiente forma:

s= inhibir() /* Inhibe interrupciones */

/* Sección crítica */

desinhibir(s) /* Restaura estado anterior de interrupciones */

Este mecanismo lleva asociadas importantes restricciones. No cumple la condición de independencia del hardware, ya que en un multiprocesador sólo se inhiben las interrupciones en el procesador que ejecuta la inhibición, restringiéndose por tanto a las implementaciones monoprocesador. Por otra parte, en monoprocesadores, un proceso que utilizase la inhibición de interrupciones como forma de acceso exclusivo a secciones críticas de duración arbitrariamente larga impediría a los demás procesos ejecutar código alguno, aún ajeno a la sección crítica. En los sistemas UNIX clásicos todo el código de las llamadas al sistema se ejecuta como sección crítica, lo que hace a estos sistemas poco adecuados para aplicaciones de tiempo real, ya que es difícil acotar los tiempos de respuesta.

Cerrojos de espera activa

Un mecanismo más general que la inhibición de interrupciones es la utilización de una variable cerrojo para proteger la sección crítica. El proceso que quiere entrar a la sección crítica consulta el cerrojo. Si está libre (cerrojo==0), el proceso lo echa (cerrojo=1) y entra a la sección crítica. Si está echado, ejecuta una espera activa consultando su valor hasta que esté libre. Cuando un proceso deja la sección crítica, libera el cerrojo (cerrojo=0). Este esquema tan sencillo presenta importantes problemas de implementación. Como se puede comprobar, la operación de consulta y modificación del cerrojo constituye a su vez una sección crítica que hay que resolver previamente; si no dos procesos podrían leer simultáneamente un valor cero y ambos entrar a la sección crítica, violando la condición exclusión mutua. Existen algoritmos bastante sofisticados que permiten una implementación software (Decker y Lamport, véase el siguiente ejemplo), pero los procesadores actuales integran mecanismos a nivel de lenguaje máquina que permiten implementar consulta y modificación atómica sobre variables en memoria.

Algoritmo de Dekker (1965)

Para 2 procesos {Pi, Pj}.

int pet[2]; /* inicialmente pet[i]=0 para todo i */

int turno;

Entrar_SC (para un proceso Pi):

pet[i]= 1;

while (pet[j])

if (turno==j) {

pet[i]= 0;

while (turno==j) NOP; /* espera activa */

pet[i]= 1;

}

Dejar_SC (para un proceso Pi):

turno= j;

pet[i]= 0;

  1. Proporciona exclusión mutua: Pi sólo entra si pet[i]. Si también pet[j], entonces uno de los dos procesos espera por turno.
  2. No interbloqueo y no inanición: Sólo un proceso quedará esperando por turno, y en este caso el otro estará en la SC, por lo que el primero entrará cuando el segundo cambie el turno al salir.
  3. No garantiza un orden FIFO.

Algoritmo de la panadería de Lamport (1974)

Para n procesos {P0, P1, …, Pn-1}.

int num[n]; /* inicialmente num[i]=0 para todo i */

int mirando[n]; /* inicialmente mirando[i]=0 para todo i */

Entrar_SC (para un proceso Pi):

mirando[i]= 1;

num[i]= maximo(num) + 1; /* coger numero */

mirando[i]= 0;

for (j=0; j<n; j++) {

while (mirando[j]) NOP; /* esperar */

while (num[j] && esta_antes(j, i)) NOP; /* esperar */

}

Dejar_SC (para un proceso Pi):

num[i]= 0;

  1. Proporciona exclusión mútua: Si Pi está esperando para entrar y existe algún Pk que
  2. ha mirado el número, entonces esta_antes(i, k).
  3. No interbloqueo y no inanición: Se sigue una disciplina FIFO.
  4. Dos procesos pueden haber cogido el mismo número. Entonces esta_antes() debe resolver. Por ejemplo, si num[i]==num[j], si i<j entonces esta_antes(i, j). Nótese que i y j no pueden ser iguales.

Las instrucciones máquina de consulta y modificación atómica (read-modify-write) proporcionan acceso exclusivo a memoria en una operación atómica de consulta y modificación que bloquea el acceso al bus. El ejemplo más simple es la instrucción máquina Test&Set, que ejecuta atómicamente la secuencia:

R <- cerrojo

cerrojo <- 1

Dejando en el registro R el valor previo de cerrojo. Representaremos esta instrucción como una función que devuelve el valor de cerrojo:

BOOL test_and_set(cerrojo)

El acceso a una sección crítica se implementa haciendo una espera activa (spin-lock) sobre el cerrojo, mediante primitivas de echar el cerrojo (lock) y liberar el cerrojo

(unlock):

lock (tipo_cerrojo cerrojo)

{

while (test_and_set(cerrojo)) NOP;4

}

unlock (tipo_cerrojo cerrojo)

{

cerrojo= 0;

}

Una sección crítica se protege de la siguiente forma:

lock(cerrojo);

/* Sección crítica */

unlock(cerrojo);

Los procesadores modernos cuentan con instrucciones máquina análogas a Test&Set que permiten implementar la espera activa más eficientemente, reduciendo la contención en el acceso al bus de memoria.

Algunos sistemas operativos utilizan cerrojos condicionales, proporcionando una primitiva de echar el cerrojo condicionalmente (cond_lock()) que, en vez de dejar al proceso esperando, devuelve un código de estado si el cerrojo está ocupado, lo que es útil para tratar de evitar interbloqueos, como se verá más adelante.

La espera activa es un mecanismo adecuado para multiprocesadores. En monopocesadores, sin embargo, una espera activa por entrar a la sección crítica podría impedir, dependiendo de la política de planificación, que el proceso que ocupa la sección crítica acceda al procesador para liberarla, y en el mejor de los casos retardaría su entrada6. En cualquier caso, aún en multiprocesadores, es adecuado combinar el mecanismo de espera activa con una planificación que incremente la prioridad del proceso que ejecuta la sección crítica.

Espera por bloqueado

La espera por bloqueado se utiliza para implementar exclusión mutua de largo plazo y se proporciona, en su forma más simple, mediante primitivas que producen un cambio de estado en el sistema operativo, dormir o sleep para bloquear al proceso que la ejecuta, y despertar o wake-up para desbloquear a un conjunto de procesos. Estas primitivas permiten manejar eventos que sirven para gestionar el acceso a recursos compartidos.

Primitivas de dormir y despertar

Un evento se implementa mediante una variable booleana o flag y la cola asociada de procesos bloqueados en él. Los procesos que ejecutan dormir sobre un flag activado pasan a estado bloqueado, provocando un cambio de contexto. Cuando otro proceso o el propio sistema operativo ejecuta despertar sobre ese flag, desbloquea a todos los procesos dormidos en ese flag.

Con dormir y despertar pueden construirse primitivas de exclusión mutua de largo plazo, lock_lp y unlock_lp. Una implementación de lock_lp y unlock_lp es la siguiente:

lock_lp (tipo_flag flag)

{

lock(mutex);

while (flag)

dormir(flag);

flag= 1;

unlock(mutex);

}

unlock_lp (tipo_flag flag)

{

lock(mutex);

flag= 0;

despertar(flag);

unlock(mutex);

}

El código es de acceso exclusivo y, como se observa, se protege con una sección crítica de corto plazo mediante un cerrojo ("mutex"; hemos utilizado un cerrojo de exclusión mutua único común, que llamamos mutex (de mutual exclusión) el cual explicaremos mas adelante). Pero un proceso no puede quedarse dormido dentro de la sección crítica de corto plazo en lock_lp, ya que no permitiría la ejecución de despertar por otro proceso. Sin embargo, como ocurre en la práctica en los sistemas operativos, dormir() puede encargarse de liberar los cerrojos de los procesos, y despertar() de restaurarlos. En otras palabras, los procesos en estado bloqueado se consideran fuera de toda sección crítica. Un problema del esquema dormir/despertar es que despertar desbloquea a todos los procesos dormidos en el flag y sólo uno de ellos accederá a la sección crítica. Esto es especialmente preocupante en multiprocesadores, ya que producirían contención en el acceso al cerrojo. Sin embargo, la limitación fundamental del manejo de eventos con este mecanismo deriva de que la primitiva de despertar no almacena el evento; lo que introduce la posibilidad de condiciones de carrera en una secuencia de dormir y despertar sobre un flag (importa el orden en que se ejecutan).

Modelo de Sincronización Mutex (Mutual EXCLUSIÓN Object) Exclusión Mutua

Los algoritmos de exclusión mutua (comúnmente abreviada como mutex por mutual exclusion) se usan en programación concurrente para evitar que fragmentos de código conocidos como secciones críticas accedan al mismo tiempo a recursos que no deben ser compartidos.

a mayor parte de estos recursos son las señales, contadores, colas y otros datos que se emplean en la comunicación entre el código que se ejecuta cuando se da servicio a una interrupción y el código que se ejecuta el resto del tiempo. Se trata de un problema de vital importancia porque, si no se toman las precauciones debidas, una interrupción puede ocurrir entre dos instrucciones cualesquiera del código normal y esto puede provocar graves fallos.

La técnica que se emplea por lo común para conseguir la exclusión mutua es inhabilitar las interrupciones durante el conjunto de instrucciones más pequeño que impedirá la corrupción de la estructura compartida (la sección crítica). Esto impide que el código de la interrupción se ejecute en mitad de la sección crítica.

En un sistema multiprocesador de memoria compartida, se usa la operación indivisible test-and-set sobre una bandera, para esperar hasta que el otro procesador la despeje. La operación test-and-set realiza ambas operaciones sin liberar el bus de memoria a otro procesador. Así, cuando el código deja la sección crítica, se despeja la bandera. Esto se conoce como spin lock o espera activa.

Algunos sistemas tienen instrucciones multioperación indivisibles similares a las anteriormente descritas para manipular las listas enlazadas que se utilizan para las colas de eventos y otras estructuras de datos que los sistemas operativos usan comúnmente.

La mayoría de los métodos de exclusión mutua clásicos intentan reducir la latencia y espera activa mediante las colas y cambios de contexto. Algunos investigadores afirman que las pruebas indican que estos algoritmos especiales pierden más tiempo del que ahorran.

A pesar de todo lo dicho, muchas técnicas de exclusión mutua tienen efectos colaterales. Por ejemplo, los semáforos permiten interbloqueos (deadlocks) en los que un proceso obtiene un semáforo, otro proceso obtiene el semáforo y ambos se quedan a la espera de que el otro proceso libere el semáforo. Otros efectos comunes incluyen la inanición, en el cual un proceso esencial no se ejecuta durante el tiempo deseado, y la inversión de prioridades, en el que una tarea de prioridad elevada espera por otra tarea de menor prioridad, así como la latencia alta en la que la respuesta a las interrupciones no es inmediata.

La mayor parte de la investigación actual en este campo, pretende eliminar los efectos anteriormente descritos. Si bien no hay un esquema perfecto conocido, hay un interesante esquema no clásico de envío de mensajes entre fragmentos de código que, aunque permite inversiones de prioridad y produce una mayor latencia, impide los interbloqueos.

Algunos ejemplos de algoritmos clásicos de exclusión mutua son:

  • El algoritmo de Dekker.

El algoritmo de Dekker es un algoritmo de programación concurrente para exclusión mutua, que permite a dos procesos o hilos de ejecución compartir un recurso sin conflictos. Fue uno de los primeros algoritmos de exclusión mutua inventados, implementado por Edsger Dijkstra.

Si ambos procesos intentan acceder a la sección crítica simultáneamente, el algoritmo elige un proceso según una variable turno. Si el otro proceso está ejecutando en su sección crítica, deberá esperar su finalización.

/* Definición de variables compartidas */

shared int bandera[2] = {0,0};

shared int turno = 0;

while (cierto)

{

bandera[proc_id] = cierto;

while (bandera[1-proc_id] == cierto)

{

if (turno == 1-proc_id)

{

bandera[proc_id] = 0;

while (turno == (1-proc_id)) /* espera a que sea su turno de intentar */;

bandera[proc_id] = 1;

}

/* Sección crítica */

turno = 1-proc_id; /* es el turno del otro proceso */

bandera[proc_id] = 0;

/* Sección no crítica */

}

}

  • El algoritmo de Peterson.

El algoritmo de Peterson es un algoritmo de programación concurrente para exclusión mutua, que permite a dos o más procesos o hilos de ejecución compartir un recurso sin conflictos, utilizando sólo memoria compartida para la comunicación.

Algoritmo

bandera[0] = 0

bandera[1] = 0

turno = 0

p0: bandera[0] = 1 p1: bandera[1] = 1

turno = 1 turno = 0

while( bandera[1] && turno == 1 ); while( bandera[0] && turno == 0 );

//no hace nada. espera. //no hace nada. espera.

// sección crítica // sección crítica

… …

// fin de la sección crítica // fin de la sección crítica

bandera[0] = 0 bandera[1] = 0

  • Algoritmo de la panadería de Lamport

Es un algoritmo de computación creado por el científico en computación Dr Leslie Lamport, para implementar la exclusión mutua de N procesos o hilos de ejecución.

Algoritmo

El algoritmo de la panadería toma su nombre de la costumbre de las panaderías y tiendas en general, donde las personas al entrar al local obtienen un número de turno (único) y lo utilizan para el dependiente les vaya atendiendo en orden de llegada. El cliente obtiene su número de turno usando una cinta de papel que ofrece boletos con números consecutivos.

El dependiente sólo puede atender a una persona al mismo tiempo, lo que concuerda con el uso de un recurso de forma exclusiva: el recurso es el dependiente y la sección crítica de un cliente es lo que realiza mientras es atendido.

El sistema mantiene un número (variable global) que refleja el número de turno del cliente que se está atendiendo en el instante actual. Los clientes deben esperar en una cola hasta que llega su turno. Una vez que se acaba con un cliente, la variable global se incrementa en uno y el cliente que tenga un boleto con ese número pasa a ser atendido. Cuando termina una compra, el cliente se desprende de su boleto y se dedica a realizar cualquier otra actividad libremente (guardar el dinero en la billetera, retirarse, etc.), ya que no se encuentra más en su sección crítica. Si más tarde quiere volver a comprar, tendrá que obtener un nuevo número.

El modelo algorítmico que se propone, cada cliente es un hilo, identificado con un número único i. Los hilos se deben coordinar para decidir en cada momento qué hilo tiene derecho a ejecutar su código de sección crítica.

En la vida real, el sistema de los boletos funciona perfectamente, pero en un sistema informático la obtención del boleto es problemática: varios hilos pueden obtener el mismo número de turno. En el algoritmo de Lamport se permite que varios hilos obtengan el mismo número. En este caso, se aplica un algoritmo de desempate, que garantiza que sólo un hilo entra en sección crítica. El desempate se realiza así: si dos o más hilos tienen el mismo número de turno, tiene más prioridad el hilo que tenga el identificador con un número más bajo. Como no puede haber dos hilos con el mismo identificador, nunca se da el caso de que dos hilos evalúen al mismo tiempo que tienen derecho a ejecutar su sección crítica.

Entrada en sección crítica

Cuando un hilo quiere entrar en su sección crítica, primero obtiene su número de turno, que calcula como el máximo de los turnos de los otros hilos, más uno. (Si varios hilos realizan el cálculo al mismo tiempo, puede ocurrir que dos o más hilos obtengan el mismo turno).

Antes de entrar en sección crítica, el hilo debe asegurarse de que tiene el número de turno más bajo. En caso de empate, decidirá el identificador de hilo más bajo.

Cuando el hilo abandona la sección crítica, pone su número de turno a un valor especial que indique su no intención de entrar en sección crítica (en este algoritmo, se usa el valor cero).

Implementación

Este es el pseudocódigo del algoritmo de la panadería.

Partes: 1, 2, 3
 Página anterior Volver al principio del trabajoPágina siguiente