Capítulo I Definición de concurrencia y exclusión mutua



Documentos relacionados
4. Programación Paralela

Concurrencia. Primitivas IPC con bloqueo

1 HILOS (THREADS) EN JAVA

Tema 4. Gestión de entrada/salida

CDI Exclusión mutua a nivel alto. conceptos

Hilos, comunicación y competencia entre procesos. Dr. Alonso Ramírez Manzanares 2-Sep-2010

Modelo de Objetos Distribuidos

Concurrencia: deberes. Concurrencia: Exclusión Mutua y Sincronización. Concurrencia. Dificultades con la Concurrencia

Estructuras de Sistemas Operativos

Java nos ofrece la clase Thread y la interfaz Runable que permiten que varios procesos estén funcionando de forma concurrente.

No se requiere que los discos sean del mismo tamaño ya que el objetivo es solamente adjuntar discos.

Unidad II: Administración de Procesos y del procesador

Threads. La plataforma JAVA soporta programas multhreading a través del lenguaje, de librerías y del sistema de ejecución. Dos.

Capítulo 1 Introducción a la Computación

Capítulo 5. Cliente-Servidor.

Tema 1. Conceptos fundamentales de los Sistemas Operativos

Concurrencia: Exclusión mutua y Sincronización

un programa concurrente

Capitulo V Administración de memoria

ARQUITECTURA DE DISTRIBUCIÓN DE DATOS

SIMM: TEORÍA DE LOS S.O. I.E.S. JUAN DE LA CIERVA CURSO 2007/2008

CAPÍTULO 2 Sistemas De Base De Datos Multiusuarios

Los mayores cambios se dieron en las décadas de los setenta, atribuidos principalmente a dos causas:

Seminario Electrónico de Soluciones Tecnológicas sobre VPNs de Extranets

SISTEMAS OPERATIVOS AVANZADOS

INTRODUCCIÓN. Que es un sistema operativo? - Es un programa. - Funciona como intermediario entre el usuario y los programas y el hardware

Procesos. Planificación del Procesador.

Sistemas Operativos. Curso 2016 Procesos

Una computadora de cualquier forma que se vea tiene dos tipos de componentes: El Hardware y el Software.

UNIDAD 3 MEMORIA COMÚN. El problema de exclusión mutua

Unidad 1: Conceptos generales de Sistemas Operativos.

Tema 1 Introducción. Arquitectura básica y Sistemas Operativos. Fundamentos de Informática

UNIDADES FUNCIONALES DEL ORDENADOR TEMA 3

Java y JVM: programación concurrente

1 (2 5 puntos) Responda con brevedad y precisión a las siguientes preguntas:

Mensajes. Interbloqueo

INTERRUPCIONES. La comunicación asíncrona de los sistemas periféricos con la CPU, en ambos sentidos, se puede establecer de dos maneras fundamentales:

Programación Orientada a Eventos

A continuación resolveremos parte de estas dudas, las no resueltas las trataremos adelante

COMO CONFIGURAR UNA MAQUINA VIRTUAL EN VIRTUALBOX PARA ELASTIX

Propuesta de Portal de la Red de Laboratorios Virtuales y Remotos de CEA

M.T.I. Arturo López Saldiña

Conceptos Básicos de Software. Clase III

Guía de uso del Cloud Datacenter de acens

Arquitectura de Aplicaciones

Introducción a la programación orientada a objetos

Ciclo de vida y Metodologías para el desarrollo de SW Definición de la metodología

DISCOS RAID. Se considera que todos los discos físicos tienen la misma capacidad, y de no ser así, en el que sea mayor se desperdicia la diferencia.

Análisis de aplicación: Virtual Machine Manager

Arquitectura de sistema de alta disponibilidad

UNIDAD 2: Abstracción del Mundo real Al Paradigma Orientado a Objetos

Sistemas Operativos. Características de la Multiprogramación. Interacción entre Procesos. Características de la Multiprogramación

Introducción a la Firma Electrónica en MIDAS

Introducción a las redes de computadores

CAPÍTULO I. Sistemas de Control Distribuido (SCD).

Clase 20: Arquitectura Von Neuman

TEMA 3. EL PROCESO DE COMPILACIÓN, DEL CÓDIGO FUENTE AL CÓDIGO MÁQUINA

Hardware y Estructuras de Control. Memoria Virtual. Ejecución de un Programa. Ejecución de un Programa

LiLa Portal Guía para profesores

SEGURIDAD Y PROTECCION DE FICHEROS

Ingº CIP Fabian Guerrero Medina Master Web Developer-MWD

Procesos. Bibliografía. Threads y procesos. Definiciones

Centro de Capacitación en Informática

CAPÍTULO 3 Servidor de Modelo de Usuario

Informática 4º ESO Tema 1: Sistemas Informáticos. Sistemas Operativos (Parte 2)

Compiladores y Lenguajes de Programación. Maria de Guadalupe Cota Ortiz

1.1.- Objetivos de los sistemas de bases de datos Administración de los datos y administración de bases de datos Niveles de Arquitectura

Guia para examen de Sistemas Operativos Para primer parcial Febrero 2013 Revisión 2 Ing. Julio Cesar Gonzalez Cervantes

Benemérita Universidad Autónoma del Estado de Puebla

Gestión de Oportunidades

Fundamentos de Sistemas Operativos

EL MODELO DE ESTRATIFICACIÓN POR CAPAS DE TCP/IP DE INTERNET

Unidad 1: Conceptos generales de Sistemas Operativos.

Manual de uso de la plataforma para monitores. CENTRO DE APOYO TECNOLÓGICO A EMPRENDEDORES -bilib

CAPÍTULO 4. EL EXPLORADOR DE WINDOWS XP

El soporte del sistema operativo. Hace que un computador sea más fácil de usar. Permite que los recursos del computador se aprovechen mejor.

Computación Tercer Año

SOLUCION PARCIAL TASK SCHEDULER. Task Scheduler

Introducción a las Redes de Computadoras. Obligatorio

Capítulo 4. Requisitos del modelo para la mejora de la calidad de código fuente

WINDOWS : TERMINAL SERVER

Gestión de la Configuración

Base de datos en Excel

Elementos requeridos para crearlos (ejemplo: el compilador)

Multitarea en Java. Rafa Caballero - UCM

Acronis License Server. Guía del usuario

15. Arquitectura de los multiprocesadores. 16. Multiprocesadores de memoria compartida. 17. Multicomputadores.

Programación Orientada a Objetos con Java

ESPACIOS DE COMUNICACIÓN VIRTUAL

PRUEBAS DE SOFTWARE TECNICAS DE PRUEBA DE SOFTWARE

Utilidades de la base de datos

Sistemas Operativos. Curso 2014 Planificación

MACROS. Automatizar tareas a través del uso de las macros.

App para realizar consultas al Sistema de Información Estadística de Castilla y León

Sistemas Operativos. Curso 2015 Planificación

PROBLEMAS CON SU CLAVE? Cliente Nuevo Puedo solicitar acceso a la Banca en Línea (Contrato Uso de Canales de Autoatención) a través del Portal?

MANUAL DE AYUDA TAREA PROGRAMADA COPIAS DE SEGURIDAD

DE VIDA PARA EL DESARROLLO DE SISTEMAS

Metodologías de diseño de hardware

SEMANA 12 SEGURIDAD EN UNA RED

Transcripción:

Capítulo I Definición de concurrencia y exclusión mutua Una computadora es una máquina que permite procesar la información de forma rápida y automática. Sin embargo, la utilización de una computadora no es una tarea sencilla, ya que el modo en que podemos comunicarnos con ella, es decir, su interfaz, es extraña y compleja al pensamiento humano. Se entiende por interfaz de un objeto la parte del objeto accesible desde su exterior, que nos permite utilizarlo y consultar su estado interno. La interfaz de una computadora viene determinada por un conjunto, normalmente pequeño, de instrucciones máquina que permiten utilizar los dispositivos físicos o hardware (CPU, memoria y periféricos) de los que se compone. Si los usuarios de una computadora tuvieran que utilizar el hardware a través de sus instrucciones máquina se escribirían muy pocos programas, y estos no podrían resolver tareas excesivamente complejas, pues el uso directo de los dispositivos físicos de la computadora mediante estas instrucciones es complejo, tedioso, y está lleno de detalles. La solución que se ha ido adoptando con el tiempo para salvar esta complejidad es la de escribir capas o niveles de software. Una capa de software de nivel i es un conjunto de programas que trabajan directamente con la interfaz de su nivel inferior (i - 1), y presentan a su nivel superior (i + 1) una interfaz que permite utilizar el nivel i -1 de una forma más sencilla. Se dice que una capa software abstrae a su nivel superior de los detalles del nivel inferior, siendo, por tanto, un mecanismo de abstracción. Una capa de nivel i puede permitir a su capa superior utilizar las interfaces de sus niveles inferiores (i -1, i - 2,...), o puede obligar a utilizar solamente la interfaz de la capa i, asegurándose así la utilización directa del nivel inferior. El sistema operativo es una de las capas de software más importantes de un sistema de información. 1.1. Definición de sistema operativo Se puede definir un sistema operativo como un conjunto de programas que controlan directamente los recursos hardware o físicos de una computadora ( CPU, memoria principal y periféricos ) proporcionando una máquina virtual más fácil de utilizar que el hardware subyacente. El sistema operativo es la capa de software más baja de una computadora, como se refleja en la Figura 1.1.

En esta figura se observa cómo el sistema operativo es la única capa que trabaja directamente con el hardware. Por encima del sistema operativo se encuentra un nivel formado por traductores, editores de texto e intérpretes de órdenes. Los dos primeros tipos de programas, junto con ligadores y depuradores, son útiles para crear un nivel de abstracción cómodo para el desarrollo de programas. La unión de los programas de las dos capas intermedias de la figura conforman el software de sistemas de una computadora. Por último, está el nivel constituido por los programas de aplicación, estos programas no dan servicio a otros programas, su finalidad es resolver problemas concretos. Son los programas que suele ejecutar un usuario común. Pertenecen a esta capa los procesadores de texto, hojas de cálculo, agendas electrónicas, juegos, etc. En general, puede decirse que los sistemas operativos realizan dos funciones: 1. Constitución de una máquina virtual o extendida El sistema operativo pone al servicio del usuario una máquina virtual cuyas características son distintas (y más fáciles de abordar) que las de la máquina real subyacente. Algunas áreas en las que es frecuente que la máquina virtual difiera de la máquina real que la soporta son: Entrada/salida (E/S). La capacidad de E/S de un hardware básico puede que sea extremadamente compleja y que requiera sofisticados programas para su utilización. Un sistema operativo evita al usuario el problema de tener que comprender el funcionamiento de este hardware, poniendo a su alcance una máquina virtual mucho más sencilla de usar. Memoria. Muchos sistemas operativos presentan la imagen de una máquina virtual cuya memoria difiere en tamaño de la de la máquina real subyacente. Así, por ejemplo, un sistema operativo puede emplear memoria secundaria (discos magnéticos) para crear la ilusión de una memoria principal mucho más extensa de la que se dispone en realidad. Alternativamente, puede repartir la memoria principal entre varios usuarios, de forma que cada uno de ellos "vea" una máquina virtual cuya memoria sea menor que la de la máquina real. Sistema de archivos.

La mayoría de las máquinas virtuales incluyen un sistema de archivos para el almacenamiento a largo plazo tanto de programas como de datos. El sistema de archivos está basado en la capacidad de almacenamiento sobre cinta o disco de la máquina real. El sistema operativo, sin embargo, permite al usuario acceder a la información almacenada a través de nombres simbólicos en lugar de hacerlo a través de su posición física en el medio de almacenamiento. Protección y tratamiento de errores. Desde el momento en que la mayoría de las computadoras son compartidas por un determinado número de usuarios, es esencial que cada uno de ellos esté protegido de los efectos de los errores o de la mala fe de los demás. Las computadoras varían considerablemente por lo que respecta al grado de protección que proporciona su hardware básico, siendo misión del sistema operativo el constituir una máquina virtual en la que ningún usuario pueda afectar de manera negativa al trabajo de los demás. Interacción a nivel de programa. Una máquina virtual puede posibilitar la interacción entre distintos programas de los usuarios de forma que, por ejemplo, la salida de uno de ellos se emplee como entrada de otro. La naturaleza concreta de una máquina virtual dependerá de la aplicación particular a la que se dedique. Así, por ejemplo, las características de una máquina virtual que controle un sistema de tiempo real serán distintas de las de una máquina virtual que se utilice para el desarrollo de programas. 2. Utilización compartida de recursos Un sistema operativo debe lograr que se compartan los recursos de una computadora entre un cierto número de usuarios que trabajen de forma simultánea. La finalidad de esto está en incrementar la disponibilidad de la computadora con respecto a sus usuarios y, al mismo tiempo, maximizar la utilización de recursos tales como procesadores centrales, memoria y dispositivos de E/S. La importancia de la utilización eficiente de estos recursos depende de su costo. Los sistemas operativos necesitan del concepto de proceso. El sistema operativo debe entremezclar la ejecución de un número de procesos para maximizar la utilización de los recursos de la computadora. Al mismo tiempo, los sistemas de tiempo compartido deben proporcionar un tiempo de respuesta razonable. El sistema operativo debe asignar recursos a los procesos de acuerdo a una política específica (ciertas funciones o aplicaciones son de mayor prioridad), mientras impide los interbloqueos. Por último, el sistema operativo debe ofrecer un soporte para llevar a cabo la comunicación entre procesos. El concepto de proceso es clave en los sistemas operativos modernos. 1.2 Definición de concurrencia Definición de sincronización Realización simultánea de dos procesos o fenómenos: la sincronización de ambos lanzamientos fue perfecta.

Por concurrencia se entiende la existencia de varias actividades simultáneas o paralelas. Ejemplo de ello lo constituyen la superposición de las operaciones de E/S con el proceso de computación. Otro ejemplo lo constituye la concurrencia de varios programas que se conmutan en un procesador. Aunque esta concurrencia no es real en un instante dado (si sólo existe un procesador), sí es real en un intervalo más amplio de tiempo. Puede verse la concurrencia de procesos como la ejecución simultánea de varios procesos. Si tenemos un multiprocesador o un sistema distribuido la concurrencia parece clara, en un momento dado cada procesador ejecuta un proceso. Se puede ampliar el concepto de concurrencia si entendemos por procesado concurrente (o procesado paralelo) la circunstancia en la que de tomar una instantánea del sistema en conjunto, varios procesos se vean en un estado intermedio entre su estado inicial y final. Los distintos procesos dentro de una computadora no actúan de forma aislada. Por un lado, algunos procesos cooperan para lograr un objetivo común. Por otro lado, los procesos compiten por el uso de unos recursos limitados, como el procesador, la memoria o los archivos. Estas dos actividades de cooperación y competición llevan asociada la necesidad de algún tipo de comunicación entre los procesos. Ejemplo: Supongamos que en un entorno LINUX se ejecuta la instrucción cat tema1 tema2 tema3 wc -l Esta instrucción va a provocar que el intérprete de comandos cree dos procesos concurrentes, el primero ejecutará el programa cat, que concatenará el contenido de los archivos de texto tema1, tema2 y tema3. El segundo ejecutará el programa wc, que contará el número de líneas de la salida producida por cat. Estos dos procesos cooperan, y será preciso algún tipo de sincronización entre ellos, concretamente hasta que cat no produzca alguna salida wc debería bloquearse. Podemos definir la programación concurrente como el conjunto de notaciones y técnicas utilizadas para describir mediante programas el paralelismo potencial de los problemas, así como para resolver los problemas de comunicación y sincronización que se presentan cuando varios procesos que se ejecutan concurrentemente comparten recursos. Como es lógico, cada problema presenta un tipo distinto de paralelismo. Implementar este paralelismo es una cuestión muy ligada a la arquitectura de la computadora en cuestión. Para poder trabajar a un nivel independiente de la arquitectura, se requiere un modelo abstracto de la concurrencia que permita razonar sobre la corrección de los programas que implementen ese paralelismo, con independencia de la máquina en que esos programas se ejecuten. Así por ejemplo, un lenguaje de programación es una abstracción que le sirve al programador para comunicarse con la máquina sin tener que considerar aspectos ligados a la misma como las instrucciones máquina o la arquitectura. La programación concurrente, vista como una abstracción, está diseñada para permitirnos razonar sobre el comportamiento dinámico de los programas concurrentes cuando se ejecutan sobre un procesador único, utilizando por tanto un modelo de paralelismo abstracto.

1.3 Arquitecturas paralelas La historia de la programación concurrente ha seguido las mismas etapas como otras áreas experimentales de las ciencias de la computación. Los SO fueron los primeros ejemplos de programas concurrentes. La tecnología ha sido envuelta para producir una variedad de sistemas multiprocesador. En un multiprocesador de memoria compartida, los procesadores comparten una memoria común; el tiempo de acceso es también uniforme (UMA) o no uniforme (NUMA). En una multicomputadora, varios procesadores llamados nodos son conectados por una red. En un sistema de red, uno o varios nodos multiprocesador comparten una red de comunicación, es decir, una Ethernet. También hay combinaciones híbridas, redes de estaciones de trabajo multiprocesador. Los sistemas operativos para multiprocesadores son programas concurrentes en el cual al menos un proceso puede ejecutarse en paralelo. Existen dos tipos básicos de arquitecturas paralelas: multiprocesadores y multicomputadoras. A continuación se proporciona una breve descripción de ellos: 1.3.1 Multiprocesadores Los multiprocesadores son computadoras de múltiples CPU's los cuales comparten memoria; cada procesador es capaz de ejecutar su propio programa. En un multiprocesador de acceso de memoria uniforme (UMA) la memoria compartida es centralizada, en un multiprocesador de acceso a memoria no uniforme (NUMA) la memoria compartida es distribuida. En un sistema de memoria compartida cualquier dirección de memoria es accesible por cualquier procesador. En los multiprocesadores UMA, la memoria física es uniformemente compartida por todos los procesadores, los cuales tienen el mismo tiempo para acceder a la memoria; por este motivo se le llama memoria de acceso uniforme. Los periféricos también pueden llegar a compartirse. Cuando todos los procesadores tienen el mismo tiempo para acceder a los periféricos, al sistema se le da el nombre de multiprocesador simétrico. En caso de que sólo uno o un subconjunto de procesadores pueden acceder a los periféricos se le conoce como multiprocesador asimétrico. El patrón más simple de intercomunicación asume que todos los procesadores trabajan a través de un mecanismo de interconexión para comunicarse con una memoria compartida centralizada (vea la Figura 1.2). Existen una gran variedad de formas para implementar este mecanismo, por ejemplo un bus, una red conectada, etc.

Figura 1.2. Modelo de multiprocesador de acceso a memoria uniforme (UMA). Todos los procesadores acceden a un mecanismo de interconexión para comunicarse con la memoria compartida global y dispositivos de entrada/salida (E/S). Los multiprocesadores de acceso a memoria no uniforme (NUMA) están caracterizados por un espacio de direcciones compartido. Sin embargo, la memoria se encuentra físicamente distribuida. Cada procesador tiene memoria local, y el espacio de direcciones compartido está formado por la combinación de estas memorias locales. El tiempo necesario para acceder a una localidad de memoria particular en un procesador NUMA depende de si esa localidad es local al procesador. Toda la memoria en el sistema es alcanzable y cada procesador tiene una memoria que puede alcanzar directamente. Sin embargo, cada procesador debe hacer una petición a través de la red para hacer una conexión a otra memoria. La Figura 1.3. muestra una red de árbol aplicada a un sistema de memoria compartida NUMA. Interruptores Procesadores de memoria compartida M P M P M P M P M P M P M P M P Figura 1.3. Red de árbol NUMA Como el modelo de programación utiliza un solo espacio de direccionamiento, la comunicación es implícita por lo que es necesario tener un mecanismo de sincronización (implícito o explícito). Los multiprocesadores presentan ciertas desventajas y se describen a continuación: No tan fácilmente se pueden adaptar para extenderse a un gran número de procesadores dado que el bus de interconexión es de capacidad limitada. La competencia por el acceso a la memoria puede reducir significativamente la velocidad del sistema. Las técnicas de sincronización son necesarias para controlar el acceso a variables compartidas y el programador debe incorporar tales operaciones de manera implícita o explícita en las aplicaciones. 1.3.2 Multicomputadoras

Una computadora von Neumann está constituida por una unidad de procesamiento central (CPU) que ejecuta un programa que desempeña una secuencia de operaciones de lectura y escritura en una memoria adjunta. Una multicomputadora consiste de un conjunto de computadoras von Neumann, o nodos ligados por una red de interconexión. Cada nodo de una multicomputadora es una computadora autónoma constituida por un procesador, memoria local y algunas veces con discos adjuntos o periféricos E/S; esta arquitectura se muestra en la Figura 1.4. Cada nodo ejecuta su propio programa, el cual puede acceder a la memoria local y puede enviar y recibir mensajes sobre toda la red. Los mensajes se usan para comunicarse con otras computadoras o, para leer o escribir en memoria remota. Un atributo de este modelo es que accede a la memoria local (mismo nodo) y es menos caro que acceder a la memoria remota (de un nodo diferente), es decir la lectura y escritura a memoria es menos costoso que el enviar y recibir un mensaje. Por lo tanto, es preferible acceder a datos locales que a datos remotos. Por tanto, en las aplicaciones para multicomputadoras es necesario localizar los datos cerca de los procesadores que los utilizan (localidad de datos). Figura 1.4. La multicomputadora, un modelo de computadora paralela idealizado. Cada nodo consiste de una computadora von Neumann: un CPU y memoria. Un nodo puede comunicarse con otro nodo por el envío y recibo de mensajes sobre una red de interconexión. Las multicomputadoras son un ejemplo de las computadoras MIMD (multiple instruction multiple data) de memoria distribuida. MIMD significa que cada procesador puede ejecutar un flujo de instrucciones en su propia área de datos local; la memoria es distribuida entre los procesadores, en lugar de colocarlos de manera central. La principal diferencia entre una multicomputadora y una computadora de memoria distribuida MIMD es que en la última, el costo de enviar un mensaje entre dos nodos puede no ser independiente de la localización del nodo y del tráfico de la red. Ejemplos de esta clase de computadoras incluyendo la IBM SP, Intel Paragon, Thinking Machines CM5, Cray T3D, Meiko CS-2 y ncube. Los algoritmos de multicomputadora no pueden ejecutarse eficientemente en una computadora SIMD (single instruction multiple data); en éstas todos los procesadores ejecutan el mismo flujo de instrucciones sobre una pieza diferente de datos. La MasPar MP es un ejemplo de esta clase de computadoras.

En los sistemas de memoria distribuida, cada procesador tiene acceso a su propia memoria, entonces la programación es más compleja ya que cuando los datos a usar por un procesador están en el espacio de direcciones de otro, es necesario solicitarla y transferirla a través de mensajes. De esta forma, es necesario impulsar la localidad de los datos para minimizar la comunicación entre procesadores y obtener un buen rendimiento. La ventaja que proporcionan estos tipos de sistemas es la escalabilidad, es decir, el sistema puede crecer un número mayor de procesadores que los sistemas de memoria compartida y, por lo tanto, es más adecuado para las computadoras paralelas con un gran número de procesadores. Sus principales desventajas son que el código y los datos tienen que ser transferidos físicamente a la memoria local de cada nodo antes de su ejecución, y esta acción puede constituir un trabajo adicional significativo. Similarmente, los resultados necesitan transferirse de los nodos al sistema host. Los datos son difíciles de compartir. Las arquitecturas de multicomputadoras son generalmente menos flexibles que las arquitecturas de multiprocesadores. Por ejemplo los multiprocesadores podrían emular el paso de mensajes al usar localidades compartidas para almacenar mensajes, pero las multicomputadoras son muy ineficientes para emular operaciones de memoria compartida. Los programas desarrollados para multicomputadoras pueden ejecutarse eficientemente en multiprocesadores, porque la memoria compartida permite una implementación eficiente de paso de mensajes. Ejemplos de esta clase de computadoras son la Silicon Graphs Challenge, Sequent Symmetry, y muchas estaciones de trabajo con multiprocesadores. 1.4 Ejemplos de programas concurrentes Hay muchos ejemplos de programas concurrentes además de los sistemas operativos. Estos surgen si la implementación de una aplicación involucra real o paralelismo aparente. Por ejemplo, los programas concurrentes son usados para implementar: Sistemas de ventanas en computadoras personales o estaciones de trabajo Procesamiento de transacciones en sistemas de base de datos multiusuario Servidores de archivos en una red, y Cómputo científico que manipule gran cantidad de datos. El último tipo de programa concurrente es llamado programa paralelo que es típico ejecutarlo en un multiprocesador. Un programa distribuido es un programa concurrente (o paralelo) en el cual la comunicación de procesos es por paso de mensajes. 1.5 Programación de procesos concurrentes 1.5.1Programa, procesos e hilos Ahora distinguiremos entre programa y proceso. Un programa es una secuencia de instrucciones escrita en un lenguaje dado. Un proceso es una instancia de ejecución de

un programa, caracterizado por su contador de programa, su estado, sus registros del procesador, su segmento de texto, pila, datos, etc. Un programa es un concepto estático, mientras que un proceso es un concepto dinámico. Es posible que un programa sea ejecutado por varios usuarios en un sistema multiusuario, por cada una de estas ejecuciones existirá un proceso, con su contador de programa, registros, etc. Es decir, un proceso es un programa que se encuentra en algún estado de ejecución. El estado actual de un proceso puede ser: Nuevo: el proceso ha sido creado por un usuario al lanzar un programa, por el sistema operativo al abrir un nuevo shell de usuario o por cualquier otra circunstancia. Ejecutándose: (running) está haciendo uso del CPU. Listo: (runnable, ready) está esperando en una lista a ser despachado para utilizar el procesador (CPU). Bloqueado: (blocked) está esperando por algún servicio que solicitó (por ejemplo, un servicio de un dispositivo de I/O) y hasta no ser atendido no será elegido para usar el CPU Suspendido o dormido: (suspended) ha recibido una señal suspend y hasta que no reciba una señal resume no será elegido para usar el CPU. Finalizado: el proceso ha terminado y todos los recursos que ha utilizado para ejecutarse son liberados. Diremos que en un sistema existe concurrencia, o que el sistema es concurrente, cuando se tengan varios procesos en ejecución simultánea. La concurrencia puede ser: 1. Real: es el caso de un multiprocesador o de un multicomputadora (sistema distribuido), donde cada proceso dispone de un procesador físicamente independiente para ejecutarse, y donde el problema fundamental es mantener a los procesos comunicados entre sí. 2. Simulada o abstracta: es el caso de un monoprocesador controlado por su sistema operativo de tiempo compartido, donde la conmutación entre distintos procesos a muy alta velocidad proporciona la ilusión de una ejecución concurrente. Esto es posible gracias a la presencia de un elemento del sistema operativo conocido como planificador, que se encarga de efectuar la conmutación efectiva entre los distintos procesos, mediante algún esquema de prioridades, y asignando a cada uno un máximo de tiempo de procesador por vez, conocido como cuanto de ejecución. Independientemente del tipo de concurrencia de que dispongamos, y a efectos prácticos, deberemos considerar que contamos con los medios para tener a varios procesos en ejecución concurrente. En consecuencia, deberemos considerar qué ventajas e inconvenientes introduce esta situación. La principal ventaja, en el caso de un modelo de concurrencia abstracta, será la posibilidad de dedicar el tiempo de procesador a otro proceso, si aquel que actualmente está siendo atendido comienza a realizar una operación de entrada/salida o pasa estado

de bloqueado por cualquier otro motivo. En el caso ideal, que mostramos en la Figura 1.5, no existen tiempos muertos para el procesador. Vemos que cuando el proceso A está efectuando una operación de entrada/salida, el proceso B accede al procesador. Figura 1.5 Procesos en multiprogramación Un proceso se denomina secuencial si un solo hilo o flujo de control regula su ejecución. Los procesos pueden crear nuevos procesos (cada uno con su propio hilo) generando así múltiples hilos de ejecución (en principio separados). Cada proceso tiene su propio espacio de direccionamiento. Entre 2 (o más) procesos algunos componentes son disjuntos (independientes) y pueden llevarse a cabo concurrentemente; otros podrían necesitar comunicarse o sincronizarse entre sí. Un proceso puede solicitar al SO por más de un hilo o flujo de ejecución, lo cual introduce un nivel más fino de concurrencia: que un proceso y sus subprocesos sean agrupados de tal forma que compartan el mismo espacio de direccionamiento pero cada uno tenga su propio estado local: tales subprocesos se denominan procesos de peso ligero o hilos (threads). Los procesos que contienen varios hilos de ejecución se denominan por tanto procesos de peso pesado. La creación de hilos puede ser dinámica ó estática mediante un proceso de control o a su vez por otros hilos. Existen dos ámbitos generales en la ejecución de hilos: En el espacio del usuario en la cual solamente los procesos (de peso pesado) son visibles al SO, la administración de hilos se realiza por un paquete o biblioteca para soporte de hilos, la cual ofrece primitivas para: o Creación, suspensión y eliminación de hilos, o Asignación de prioridades y otros atributos o Sincronización y comunicación La ventaja de un paquete de soporte es su portabilidad. A nivel de kernel, son administrados por el SO nativo directamente, ofreciendo soporte para creación y primitivas para sincronización con mucho mayor flexibilidad y eficiencia. Los hilos ofrecen las siguientes ventajas:

o Es menos caro y más eficiente en general crear varios hilos que compartan datos dentro de un proceso que crear varios procesos que compartan a su vez datos. o Las operaciones de I/O en dispositivos lentos (redes, terminales, discos) pueden ser realizadas por un hilo mientras que al mismo tiempo otro hilo lleva a acabo cómputos útiles. o Operaciones de coordinación y sincronización entre hilos puede hacerse de manera muy eficiente gracias a que se realiza de manera local, posiblemente evitando llamadas al kernel. o Múltiples hilos pueden manejar eventos (como clicks del mouse) en varias ventanas en un entorno GUI. o Un servidor puede crear hilos dinámicamente para atender a las solicitudes que recibe. A su vez los procesos clientes pueden tener varios hilos trabajando de manera concurrente, cada uno de los cuales se encarga de realizar una solicitud por servicios en particular. 1.5.2 Construyendo hilos en Java Existen dos formas para crear hilos adicionales (por default, cada objeto tiene un hilo de control implícito) en un programa Java. La primer forma consiste en extender la clase Thread y sobrecargar el método público run() con el código para el nuevo hilo (El método run() constituye propiamente el punto de entrada del hilo, análogo al método main() para un programa). A continuación se debe crear una instancia de tal clase e invocar a su método start(), la máquina virtual de Java (JVM) ejecutará al nuevo hilo: class A extends Thread{ public A(String name){ super(name); //invoca al constructor padre public void run(){ System.out.println( Me llamo + getname()); class B{ public static void main(string[] args){ A a= new A( pepe ); a.start(); //El hilo esta listo para ejecutarse La segunda manera consiste en implantar la interfaz Runnable en una clase que contenga a su vez el método público run(), crean una instancia de la clase y pasa una referencia a éste objeto recién creado al constructor Thread:

class A extends Cualquier_Otra implements Runnable{ public void run(){ System.out.println( Me llamo + Thread.currentThread().getName());// * class B{ public static void main(string[] args){ A a= new A(); Thread t = new Thread(a, paco ); t.start(); En ésta segunda forma, el hilo se indica de manera explícita, pero es más flexible en el sentido que la clase A puede extender cualquier otra clase (ya sea del sistema o definida por el usuario), mientras que en la primera forma esto no es posible debido a la restricción de herencia sencilla en Java. Noté la expresión señalada con * : primero se invoca al método de clase currentthread() el cual regresa una referencia al hilo actual en ejecución, y a ese hilo se le invoca su método getname(). Un hilo en un programa Java puede estar en alguno de los siete estados: Nuevo (new), listo (ready/runnable), ejecutándose (running), suspendido (suspended), bloqueado (blocked), suspendido-bloqueado (suspended-blocked) y finalmente muerto (dead), ver Figura 1.6. new() New start() Runnable I/O completes sleep() expires notify(), notifyall() join completes resume() Suspended scheduled suspend() yield() timeslice ends Blocket queue dead stop() Running sleep() wait() join() blocking I/O queue Figura 1.6. Estados de un proceso/hilo en Java y los eventos que provocan las transiciones de estado. Las transiciones de estados ocurren como consecuencia de los eventos. A continuación describimos brevemente el significado de los estados:

New cuando el hilo es creado, i.e. Thread t = new Thread(a); Runnable/ready cuando se invoca el método start() de un nuevo hilo. Todos los hilos en este estado son organizados por la JVM en una estructura de datos denominada el conjunto de hilos elegibles (runnable set). Running el hilo está en ejecución, el código del método run() es el punto de entrada. Si el hilo invoca a su método yield() cede lo que le resta de tiempo de CPU a otro hilo elegible por el despachador de la JVM. Suspended el hilo en alguno de los dos estados anteriores se suspende cuando su método suspend() es invocado, ya sea por sí mismo ó por otro hilo. Para que pueda regresar al estado runnable, otro hilo debe invocar al método resume() (del hilo suspendido obviamente). Blocked un hilo ejecutándose puede bloquearse como consecuencia a los eventos siguientes: 1. Cuando invoca a su propio método sleep() 2. Cuando invoca a su método wait() dentro de un método indicado como synchronized (de uso exclusivo por un sólo hilo a la vez) de algún objeto 3. Cuando invoca al método join() en un objeto cuyo hilo aún no termina 4. Cuando realiza una operación de servicio no inmediata ( bloqueadora ), i.e. asociada a algún dispositivo de I/O. Nota: yield es una indicación puramente heurística que advierte a la JVM que si hay cualquier otro hilo ejecutable pero no hay ninguno en ejecución el planificador debería ejecutar uno o más de estos hilos en lugar del hilo actual. Join synchronized En cada caso, el hilo queda en una lista de espera hasta que ocurra el evento que lo ponga en la lista de hilos elegibles a ser ejecutados; por ejemplo, si el hilo se bloqueó como consecuencia de invocar wait() en un método synchronized, saldrá del estado bloqueado cuando otro hilo invoque notify() o notifyall(). Suspended-blocked es un estado intermedio: si un hilo bloqueado es suspendido por otro hilo, entra en éste estado si la operación de bloqueo se completa (i.e. sucede el evento para desbloqueo), entonces el hilo entra al estado suspendido, si por otra parte el hilo recibe la señal resume() por otro hilo antes que el evento de desbloqueo ocurra, entonces entra al estado bloqueado. Dead cuando termina la ejecución del método run() del hilo ó cuando se invoca a su método stop() (generalmente por otro hilo). Los mismos mecanismos para sincronización entre hilos (semáforos, monitores, etc.) serán usados con el mismo propósito para coordinar procesos, por tanto, a partir de éste momento nos referiremos de manera indistinta al manejo de procesos e hilos.

1.5.3 Condiciones de competencia Un aspecto importante en la programación concurrente es la solución al problema denominado pérdida de actualización que ocurre cuando 2 o más hilos comparten datos: si 2 hilos comparten el uso de una variable n, si ambos actualizan el valor de n casi al mismo tiempo en una arquitectura con instrucciones para carga/almacenamiento (load/store en registros, y si tales operaciones son alternadas, entonces una de las actualizaciones se perderá al ser re-escrita por la otra). Por ejemplo, supongamos que actualmente n=1; y cada hilo va a ejecutar Hilo A: Hilo B: n:= n+1; n:=n+2; Si los 2 hilos ejecutan sus enunciados anteriores casi a la vez, n podría terminar con valor 2 o 3 en lugar de su valor deseado, 4. Podríamos considerar que un enunciado de asignación n:= n+a al ser compilado se produce una secuencia de instrucciones en lenguaje de máquina (las instrucciones en lenguaje de máquina son atómicas, i.e. indivisibles) parecido a lo siguiente: load n, R carga el valor en la dirección n en el registro R add a, R sumar a a R i.e. r <- R+ a store R, n almacenar R en la dirección n A continuación se muestra una posible situación en la cual se alterna la ejecución del código asociado a los hilos A y B: A: load n, R A: add R,1 cambio de contexto de A a B B: load n, R B: add R, 2 B: store R, n cambio de contexto de B a A A: store R, n El valor final de n obedece a solamente uno de los incrementos, el otro se perdió pues fue encimado. Este es un ejemplo de condición de competencia la cual ocurre cuando 2 o más hilos comparten estructuras de datos y están leyendo/escribiendo sobre tales estructuras compartidas de manera concurrente, el resultado final, que podría ser erróneo en cuanto a lo deseado, depende de qué hilo hizo qué en qué momento (cuando), i.e. depende del cambio de contexto particular (alternando de instrucciones de lenguaje máquina) ejecutada por los hilos. Se puede provocar el problema de condición en competencia en Java pero no a nivel de granularidad tan fino (i.e. en la actualización dada por el incremento a una variable). Sin embargo, se puede alternar la ejecución de 2 ciclos en 2 hilos que comparten datos (race.java): class Racer extends MyObject implements Runnable { private int M = 0; hilos // esots campos se comparten entre los

private volatile long sum = 0; // el modificador volatile indica que la //var. Será accedida por más de un hilo a la vez public Racer(String name, int M) { super(name); this.m = M; System.out.println("age()=" + age() + ", " + getname() + " is alive, M=" + M); private long fn(long j, int k) { long total = j; for (int i = 1; i <= k; i++) total += i; return total; public void run() { System.out.println("age()=" + age() + ", " + getthreadname() + " is running"); for (int m = 1; m <= M; m++) /* * "N = N + 1" ocurre perdida de actualización (race condition) en el siguiente enunciado */ sum = fn(sum, m); System.out.println("age()=" + age() + ", " + getthreadname() + " is done, sum = " + sum); class RaceTwoThreads extends MyObject { private static int M = 10; private final static int numracers = 2; public static void main(string[] args) { boolean timeslicingensured = false; boolean forcesequential = false; // run the threads consecutively System.out.println("RaceTwoThreads: M=" + M + ", timeslicingensured=" + timeslicingensured + " forcesequential=" + forcesequential); // start the two threads, both in the same object // so they share one instance of its variable sum Racer racerobject = new Racer("RacerObject", M); Thread[] racer = new Thread[numRacers]; for (int i = 0; i < numracers; i++) racer[i] = new Thread(racerObject, "RacerThread" + i); for (int i = 0; i < numracers; i++) { racer[i].start(); if (forcesequential) try { racer[i].join(); // wait for it to terminate catch (InterruptedException e) { System.err.println("interrupted out of join"); System.out.println("age()=" + age() + ", all Racer threads started"); // wait for them to finish if not forced consecutive if (!forcesequential) try { for (int i = 0; i < numracers; i++) racer[i].join(); catch (InterruptedException e) { System.err.println("interrupted out of join");

// correct race-free final value of sum is 2*220 = 440 for M of 10 // and 2*1335334000 = 2670668000 for M of 2000 (so `long sum' needed) System.out.println("RaceTwoThreads done"); System.exit(0); Sólo se crea un objeto Racer y 2 hilos sobre ese mismo objeto, por tanto los hilos comparten sum. En el ciclo, cada hilo invoca repetidamente al método fn() el cual actualiza el valor de sum: el enunciado sum=fn(sum,m) está sujeto a la condición de competencia y por tanto de pérdida de la actualización si es que ocurren cambios de contexto entre los hilos durante la invocación a fn(). Se muestra uno de los posibles resultados en la ejecución del programa: /*... Example compile and run(s) D:\>javac race.java D:\>java RaceTwoThreads RaceTwoThreads: M=10, timeslicingensured=false forcesequential=false age()=10, RacerObject is alive, M=10 age()=10, all Racer threads started age()=10, RacerThread0 is running age()=10, RacerThread0 is done, sum = 220 age()=10, RacerThread1 is running age()=10, RacerThread1 is done, sum = 440 RaceTwoThreads done Para que un programa concurrente sea correcto, debe ser escrito de forma tal que no dependa en ninguna forma de cuando ocurran los cambios de contexto, ni de cuanto duren las rebanadas de tiempo ni de las velocidades relativas de CPU; es decir, debe de coordinarse o sincronizarse la ejecución de los hilos de tal forma que no intenten consultar-actualizar simultáneamente a una variable o estructura de datos compartida. En otras palabras, se debe evitar el cambio de contexto cuando se están realizando instrucciones que actualizan la variable o estructura compartida, tales acciones deben realizarse de manera atómica. Se denomina problema de Exclusión Mutua al proceso de eliminar tales alternamientos indeseables. Para evitar la presencia de condiciones de competencia y por tanto resultados erróneos se debe identificar en cada hilo las secciones críticas (SC), i.e., segmentos de código que: 1. Hagan referencia a una o más variables mediante operaciones consulta/actualización mientras alguna de tales variables está siendo alterada por otro hilo. 2. Alteren a una o más variables que están siendo referenciadas o consultadas por otro hilo. 3. Usen una estructura de datos (como una lista enlazada) mientras parte de ella está siendo alterada por otro hilo. 4. Alteren alguna parte de una estructura de datos mientras está siendo a su vez utilizada por otro hilo. Se describe en el siguiente capítulo varias técnicas para resolver este problema.

Capítulo 2 El problema de exclusión mutua Exclusión mutua significa asegurar que un recurso compartido (variable, estructura de datos, etc.) sea accedida por un solo hilo a la vez, el acceso se refiere a ejecutar la sección crítica correspondiente del hilo. Esto significa a su vez establecer un preprotocolo y un postprotocolo para evitar que 2 o más hilos estén ejecutando sus secciones críticas al mismo tiempo. Este modelo basado en secciones críticas permite además que los procesos concurrentes se comuniquen a través de ellas, utilizando variables comunes protegidas por secciones críticas como medio de comunicación. De acuerdo con ello, la notación para representar a un proceso que ejecuta una sección crítica bajo exclusión mutua será el que se presenta en la Figura 2.1. hilos Ti, i=0, 1, 2, while (true){ código de sección no crítica wanttoentercs(i); /* preprotocolo*/ código de la sección crítica finishedcs(i); /*postprotocolo*/ Figura 2.1 : Pre y Post protocolos para exclusión mutua. Existen una serie de propiedades deseables que cualquier solución al problema de la exclusión mutua debe cumplir: Seguridad: No deben ocurrir deadlocks, los abrazos mortales ocurren cuando dos hilos intentaron entrar a sus secciones críticas al mismo tiempo y ninguno tuvo éxito (ambos se bloquearon o se quedan en espera ocupada indefinidamente) aún cuando ningún otro hilo intente entrar a su región crítica. Existe una forma de deadlock denominada livelock en la cual los hilos no están bloqueados o en espera sino que ejecutan su código de preprotocolo indefinidamente. Ausencia de espera innecesaria: Ningún hilo fuera de su SC debe bloquear o estorbar a otros hilos que están fuera de sus SC y que quieran entrar.

Entrada asegurada: ningún hilo deberá esperar por siempre para poder entrar a su SC, o ser bloqueado indefinidamente cuando intente entrar; i.e., debe asegurarse ausencias de inanición. Se identifican dos formas de inanición: 1. En ausencia de contención: ocurre cuando un hilo T intentó entrar a su SC y aún cuando ningún otro hilo está en su correspondiente SC, T no puede entrar. 2. En presencia de contención: cuando 2 o más hilos intentaron entrar a sus SC pero uno nunca puede mientras que el otro entra una y otra vez. Se asume que los hilos no se detienen o colisionan en sus pre-post protocolos, ni en sus SC ni fuera de ellas. Las secciones críticas, por tanto, deben ejecutarse de forma tal que parecieran ser operaciones atómicas. La implementación de la exclusión mutua puede conseguirse: 1. por procesador: cuando un proceso está ejecutando su sección crítica, ejecuta un protocolo de entrada que desactiva las interrupciones, y uno de salida que las activa, lo cual basta para asegurar la exclusión mutua. Es un sistema de alto riesgo, porque deja el control de las interrupciones a cargo del usuario. En un gran proyecto, con varios programadores involucrados, alguien puede simplemente olvidarse de activar las interrupciones que otra persona desactivó previamente, derivándose efectos indeseables. 2. por memoria: se define una variable común a todos los procesos concurrentes que controla el acceso a la sección crítica. El protocolo de entrada consultará ahora su valor y en función de él dejará entrar o bloqueará al proceso que desea entrar en su sección crítica. El protocolo de salida modificará el valor de la variable para indicar que el recurso ya está libre. La variable común se utiliza como cauce de comunicación entre los distintos procesos, al objeto de acceder éstos a sus secciones críticas con cierto orden. Los algoritmos para resolver el problema de la exclusión mutua se dividen en: 1. Centralizados: utilizan variables compartidas entre los procesos que expresan el estado de los procesos ( se suele acceder a estas variables en los protocolos de E/S) 2. Distribuidos: no utilizan variables compartidas entre los procesos. Suelen utilizar paso de mensajes. Existen varias propuestas para resolver el problema de exclusión mutua: esperas ocupadas vs. bloqueos ( con soporte del SO), soluciones por hardware, etc. 2.1 Soluciones (en software) para 2 hilos La técnica utilizada en las propuestas de solución que se presentan en ésta sección reciben el nombre de espera ocupada, término que se aclarará conforme se describe el funcionamiento de cada algoritmo.

2.1.1 Primera aproximación La primera aproximación (clase att1.java) consiste en alternar una variable que indica el turno, así los dos hilos involucrados alternan la ejecución de sus SC: class Arbitrator { private static final int NUM_NODES = 2; // solo 2 hilos private volatile int turn = 0; // 1er. Intento:alternado estricto public Arbitrator(int numnodes) { if (numnodes!= NUM_NODES) { System.err.println("Arbitrator: numnodes=" + numnodes + " which is!= " + NUM_NODES); System.exit(1); private int other(int i) { return (i + 1) % NUM_NODES; public void wanttoentercs(int i) { // pre-protocol while (turn!= i) /* busy wait */ Thread.currentThread().yield(); public void finishedincs(int i) { turn = other(i); // post-protocol Este intento padece de inanición en ausencia de contención: puede ocurrir que un hilo quiera entrar a sus SC pero no sea su turno y deba por tanto esperar. 2.1.2 Segunda aproximación El segundo intento (clase att2.java) utiliza una variable como bandera para indicar el deseo del hilo por entrar a su SC: class Flag { public volatile boolean value = false;... // Second attempt: check other's flag. private Flag[] desirescs = new Flag[NUM_NODES];... for (int i = 0; i < NUM_NODES; i++) desirescs[i] = new Flag();... public void wanttoentercs(int i) { // pre-protocol while (desirescs[other(i)].value) // busy wait Thread.currentThread().yield(); desirescs[i].value = true; public void finishedincs(int i) { desirescs[i].value = false; // post-protocol Este tampoco funciona del todo: dos hilos entran a sus SC a la vez si ambos ejecutan sus ciclos de espera ocupada al verificar el estado de la bandera del otro hilo. Cada uno observa la bandera en falso y entran.

2.1.3 Tercera aproximación El tercer intento (clase att3.java) intenta corregir el 2do invirtiendo el orden de la verificación de la bandera asociada al vecino y señalando su propio deseo por entrar a su SC:... // Third attempt: set own flag.... public void wanttoentercs(int i) { // pre-protocol desirescs[i].value = true; while (desirescs[other(i)].value) // busy wait Thread.currentThread().yield(); public void finishedincs(int i) { desirescs[i].value = false; // post-protocol Esta modificación asegura exclusión mutua pero permite la posibilidad de deadlock, ya que ambos hilos se quedan indefinidamente en sus ciclos de espera si ambos intentan entrar casi al mismo tiempo. Cada uno observa que la bandera del otro es verdadera entonces esperan por siempre. Nótese que el uso del modificador volatile es importante en cómo este algoritmo fomenta la exclusión mutua. 2.1.4 Cuarta aproximación El cuarto intento (clase att4.java) intenta corregir al anterior haciendo que los hilos se retrocedan de entrar a sus SC si detectan que ambos están tratando de entrar a la vez:... // Fourth attempt: back off.... public void wanttoentercs(int i) { // pre-protocol desirescs[i].value = true; while (desirescs[other(i)].value) { desirescs[i].value = false; // back off Thread.currentThread().yield(); desirescs[i].value = true; public void finishedincs(int i) { desirescs[i].value = false; // post-protocol Desafortunadamente modifica el problema de deadlock a uno relacionado denominado livelock en el cual los hilos retroceden indefinidamente cuando coinciden al intentar entrar a sus SC. 2.1.5 Solución Dekker Esta propuesta fue desarrollada por Dekker en 1960: corrige el cuarto intento considerando que los hilos tomen turnos para retroceder (clase attd.java). // Dekker's solution: take turns backing off. public void wanttoentercs(int i) { // pre-protocol

desirescs[i].value = true; while (desirescs[other(i)].value) { if (turn!= i) { desirescs[i].value = false; // back off while (turn!= i) /* busy wait */ Thread.currentThread().yield(); desirescs[i].value = true; public void finishedincs(int i) { desirescs[i].value = false; turn = other(i); // post-protocol Asegura exclusión mutua, evita deadlocks y no permite inanición en ausencia de contención, pero permite inanición en presencia de contención. 2.1.5 Solución Peterson En 1981 Peterson descubrió una solución mejor: (clase attp.java) la variable compartida last registra qué hilo intentó entrar a su SC, a éste hilo se le retarda si ambos hilos intentan entrar a la vez. Esta propuesta elimina la inanición en presencia de contención:... private volatile int last = 0; // Peterson's solution.... public void wanttoentercs(int i) { // pre-protocol desirescs[i].value = true; last = i; while (desirescs[other(i)].value && last == i) // busy wait Thread.currentThread().yield(); public void finishedincs(int i) { desirescs[i].value = false; // post-protocol 2.2 Soluciones (en software ) para más de 2 hilos Cuando hay más de 2 hilos involucrados, ninguno de los algoritmos anteriores funcionan sin complicadas modificaciones. El algoritmo de la panadería de Lamport está diseñado para soportar múltiples hilos: un hilo que desea entrar a su SC calcula el número del siguiente boleto que indica el turno y espera, de manera análoga a los clientes cuando entran a una panadería. Cada hilo usa el valor máximo de los boletos anteriores y le suma 1. Dado que 2 hilos podrían calcular el mismo número, para asegurar unicidad se le identifica a cada hilo por un número y permitirá evitar empates. El algoritmo de Lamport para 2 hilos se muestra a continuación (clase back2.java): class Ticket{public volatile int value =0;... // Lamport's bakery ticket algorithm. private Ticket[] ticket = new Ticket[NUM_NODES];... for (int i = 0; i < NUM_NODES; i++) ticket[i] = new Ticket();... public void wanttoentercs(int i) { // pre-protocol ticket[i].value = 1;

ticket[i].value = ticket[other(i)].value + 1; // compute next ticket while (!(ticket[other(i)].value == 0 ticket[i].value < ticket[other(i)].value (ticket[i].value == ticket[other(i)].value // break a tie && i == 0))) /* busy wait */ Thread.currentThread().yield(); public void finishedincs(int i) { ticket[i].value = 0; // post-protocol Para un número arbitrario de hilos, el algoritmo se muestra a continuación (clase bakn.java). private int numnodes = 0; // Lamport's bakery ticket algorithm. private Ticket[] ticket = null; private int maxx(ticket[] ticket) { int mx = ticket[0].value; for (int i = 1; i < ticket.length; i++) if (ticket[i].value > mx) mx = ticket[i].value; return mx; public void wanttoentercs(int i) { // pre-protocol ticket[i].value = 1; ticket[i].value = 1 + maxx(ticket); // compute next ticket for (int j = 0; j < numnodes; j++) if (j!= i) while (!(ticket[j].value == 0 ticket[i].value < ticket[j].value // break a tie (ticket[i].value == ticket[j].value && i < j))) // busy wait Thread.currentThread().yield(); public void finishedincs(int i) { ticket[i].value = 0; // post-protocol 2.3 Soluciones (en hardware ) Con el apoyo de hardware, se pueden deshabilitar las interrupciones mascarables de tal forma que ningún otro proceso o hilo pueda ejecutarse hasta que las mismas interrupciones sean rehabilitadas. Una posible propuesta de solución al problema de exclusión mutua utilizando esta idea es: wanttoentercs(int i){ //pre-protocolo inhabilitar interrupciones finishedincs(int i){ //post-protocolo habilitar interrupciones Sin embargo, no resulta aceptable que cualquier hilo deshabilite las interrupciones o el bus interno del CPU, ya que en principio nada garantiza que los procesos de usuario estén bien programados, de tal suerte que puedan ocasionar una catástrofe si no se restauran convenientemente. Esta estrategia debe restringirse a nivel del kernel exclusivamente, ya que en ese nivel se pueden deshabilitar las interrupciones por períodos cortos de tiempo.

2.4 Soluciones mediante bloqueo Las propuestas de solución en software realizan una forma de espera ocupada: el hilo cede su tiempo de procesador restante y el despachador lo devuelve a la lista de hilos elegibles para ser ejecutados; sin embargo, esto tiene la desventaja de desperdiciar ciclos de procesador. En lugar de ello, en vista de lograr mejor desempeño, al hilo que deba esperar su turno para entrar a su SC se le puede colocar en una lista de espera en donde no sea elegible para usar el procesador pero pueda ser rehabilitado una vez que el recurso compartido haya sido liberado. Para ello pueden incorporarse una pareja de llamados al sistema delay() y wakeup(): cuando un hilo invoque a delay() el despachador lo colocará al final de la lista de espera y por tanto su estado se cambiará de listo a bloqueado. Cuando un hilo invoque wakeup() el hilo (si hay alguno) que se encuentre al inicio de la lista de espera será transferido a la lista de hilos elegibles por ser ejecutados y su estado se cambiará a listo ; si la lista se encuentra vacía, wakeup() no tendrá efecto. Desafortunadamente, este enfoque es susceptible a padecer condiciones de contención: si tenemos los siguientes fragmentos de hilos A: while (cond) delay(); B:wakeup(); Supongamos que el hilo A se ejecuta y encuentra que el valor de la condición es verdadero, en ese momento ocurre un cambio de contexto (justamente antes que A invocara al delay()) y el hilo B ejecuta su invocación a wakeup() la cual no tiene efecto en A pues el cambio de contexto ocurrió desafortunadamente antes, al continuar su ejecución A invoca al delay() y entonces se queda esperando indefinidamente. Como otro problema en específico, considérese el problema de interacción entre un productor (P) y un consumidor (C) que trabajan sobre un buffer de tamaño finito, //variables compartidas por el consumidor y el productor: int N, count=0; producer(){ 0: while(true){ 1: produce_elemento(); 2: if (count == N) delay(); //el buffer esta lleno 3: introduce_en_buffer(); 4: count++; 5: if (count == 1) wakeup(); consumer(){ 0: while(true){ 1: if (count == 0) delay(); //el buffer esta vacio 2: quita_elemento();