Unidad 3 Sistemas con memoria común Definiciones Por concurrencia se entiende la existencia de varias actividades simultáneas o paralelas. La concurrencia de procesos puede verse como la ejecución simultánea de varios procesos. Programación concurrente es 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. Programación de procesos concurrentes 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. Estados de un proceso El estado actual de un proceso puede ser: Nuevo: (new) 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. Concurrencia 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. Ventajas 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, no existen tiempos muertos para el procesador. Procesos en multiprogramación Vemos que cuando el proceso A está efectuando una operación de entrada/salida, el proceso B accede al procesador. 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í. Hilos 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. 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: Creación, suspensión y eliminación de hilos, Asignación de prioridades y otros atributos 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. Hilos, ventajas Los hilos ofrecen las siguientes ventajas: 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. 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. 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. Múltiples hilos pueden manejar eventos (como clicks del mouse) en varias ventanas en un entorno GUI. 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. 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: Clase Thread 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 } } Implementar Runnable 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) Estados de un proceso/hilo en Java y los eventos que provocan las transiciones de estado I/O completes new() sleep() expires notify(), notifyAll() start() New Runnable join completes resume() scheduled Suspended suspend() yield() timeslice ends Running Blocket sleep() wait() join() blocking I/O queue stop() dead Estados de un hilo en Java 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). Estados de un hilo en Java 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). Estados de un hilo en Java Blocked un hilo ejecutándose puede bloquearse como consecuencia a los eventos siguientes: Cuando invoca a su propio método sleep() 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 Cuando invoca al método join() en un objeto cuyo hilo aún no termina 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. Estados de un hilo en Java 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(). Estados de un hilo en Java 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. Pérdida de actualización 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). Ejemplo Supongamos que actualmente n=1; y cada hilo va a ejecutar Hilo A: n:= n+1; Hilo B: 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 add a, R store R, n carga el valor en la dirección n en el registro R sumar a a R i.e. r <- R+ a 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. 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. Alteren a una o más variables que están siendo referenciadas o consultadas por otro hilo. Usen una estructura de datos (como una lista enlazada) mientras parte de ella está siendo alterada por otro hilo. Alteren alguna parte de una estructura de datos mientras está siendo a su vez utilizada por otro hilo. 2. 3. 4.