Descargar la presentación
La descarga está en progreso. Por favor, espere
Publicada porLeonor Monsanto Meneses Modificado hace 6 años
1
Sistemas Operativos Tema 3. Concurrencia
© José Miguel Santos – Alexis Quesada – Francisco Santana
2
Contenidos Programación concurrente El problema de la sección crítica
Sincronización: espera por condiciones Mutex (cerrojos) y variables condición
3
Referencias bibliográficas
Fundamentos de Sistemas Operativos S. Candela, C.R. García, A. Quesada, F.J. Santana, J.M. Santos. Thomson, 2007 Capítulo 3 Programación Concurrente J.T. Palma, M.C. Garrido, F. Sánchez, A. Quesada. Paraninfo, 2003 Capítulos 1, 2, 3, 4, 5 y 6 Principles of Concurrent and Distributed Programming M. Ben-Ari. Prentice Hall, 1990 Concurrent Programming A. Burns, G. Davies. Addison-Wesley, 1993 Capítulo 7
4
Modelo del sistema Conjunto de procesos cooperativos
Red de cajeros automáticos Sistema de reserva de billetes Servidor de impresión Navegador web (pestañas, plugins…) ...
5
¿Qué es concurrencia? Definición de diccionario: coincidir en el espacio o en el tiempo dos o más personas o cosas. En Informática, se habla de concurrencia cuando hay una existencia simultánea de varios procesos en ejecución. Ojo, concurrencia existencia simultánea no implica ejecución simultánea.
6
Paralelismo y concurrencia
El paralelismo es un caso particular de la concurrencia: cuando hay ejecución simultánea de instrucciones en varios procesadores. Computación distribuida: paralelismo en sistemas distribuidos o en red.
7
Programación concurrente
¿Cómo expresamos la concurrencia al programar? Una API (ej. pthreads) Objetos concurrentes (ej. Java) Sentencias concurrentes (ej. Ada, Go…)
8
Sentencia concurrente
En estas diapos, expresaremos las actividades concurrentes con una sentencia concurrente: void desayunar() { preparar_ingredientes(); CONCURRENTE { preparar_café(); calentar_tostadas(); calentar_leche(); } comer();
9
Procesos cooperativos
Necesidades de sincronización y comunicación Los procesos concurrentes tendrán necesidad de comunicarse información Si son hilos, pueden comunicarse mediante variables compartidas. Además, será necesario en ocasiones detener a un proceso hasta que se produzca un determinado evento o se den ciertas condiciones sincronización
10
Ejemplo 1: operaciones bancarias
Variable compartida por dos procesos int saldo; void ingreso(int N) { saldo = saldo + N; } void reintegro(int N) { if (saldo >= N) saldo = saldo - N; else ERROR(); main() { saldo = 0; CONCURRENTE { ingreso(100); reintegro(50); } Dependiendo de cómo se ejecuten las dos sentencias concurrentes, saldo puede acabar con el valor 50 o con valor 100.
11
Ejemplo 2: sensor de paso de vehículos
main() { CONCURRENTE { cuenta_coches(); imprime_contador(); } int contador = 0; void cuenta_coches() { while (true) { …espera que pase un coche… contador++; } void imprime_contador() { sleep(3600); // espera una hora printf("coches que han pasado: %d\n",contador); contador = 0; Enunciado: Nos encargan programar un sistema que cuente el número de vehículos que pasan por un determinado punto cada hora. Para ello disponemos de un sensor que cruza la carretera en el punto que nos interesa y cada vez que pasa un coche produce una señal. Al cabo de la hora imprime la cuenta y la pone a 0 de nuevo. Escenarios erróneos: 1.- En un instante t dado, el contador vale 19. 2.- P1 detecta que pasa un coche y pone el valor del contador a 20. 3.- P2 detecta que pasó la hora y comienza a escribir el valor del contador (20) 4.- P1 detecta que pasó otro coche y pone contador a 21. 5.- P2 termina de imprimir el valor 20 y pone contador a 0. El resultado de este escenario es que se ha perdido la cuenta de un coche. Otro escenario: 2.- P1 detecta que pasa un coche y ejecuta Cargar contador en acumulador. 3.- P2 detecta que pasó una hora y escribe el valor de contador (19) y a continuación lo pone a cero. 4.- P1 suma 1 a su acumulador y almacena su valor en contador (20) El resultado de este escenario es que se han contabilizado dos veces los mismos coches.
12
Ejemplo 3: búfer finito Productor Consumidor const int N=...;
struct Item {...}; Item* buffer [N]; int entra=0, sale=0; int pendientes=0; Productor Consumidor while (true) { Item* cosa = producir_algo(); while (pendientes==N) {} buffer[entra] = cosa; entra = (entra+1)%N; pendientes++; } while (true) { while (pendientes==0) {} Item* cosa = buffer[sale]; sale = (sale+1)%N; pendientes--; consumir(cosa); }
13
Problema al modificar datos compartidos
Ambas rutinas son correctas si se ejecutan por separado pero podrían NO funcionar si se ejecutan de manera concurrente Supongamos que las instrucciones de incremento y decremento se ejecutan al mismo tiempo. Veamos el código máquina que el compilador ha generado para cada instrucción: contador = contador + 1 contador=contador-1 (A) LW $3,contador (D) LW $4,contador (B) ADD $3,$3,1 (E) ADD $4,$4,1 (C) SW $3,contador (F) SW $4,contador Si el contador vale inicialmente 5 y se ejecutan las instrucciones en este orden: (A) » (B) » (D) » (E) » (F) » (C), el contador acaba valiendo 4. Dependiendo del orden de ejecución de las instrucciones, el contador puede acabar valiendo 4, 5 o 6 esto no es un comportamiento deseable
14
El problema de la sección crítica
15
Sección crítica: modelo del sistema
Varios procesos intentan acceder continuamente a un recurso compartido: while (true) { Sección_No_Crítica (SNC); Pre_Protocolo(); Sección_Crítica (SC); Post_Protocolo(); } Sección crítica: segmento de código donde se accede a datos compartidos con otros procesos. Requisito: nunca debe haber más de un proceso dentro de la sección crítica (exclusión mutua).
16
Requisitos de la solución
Exclusión mutua Progreso: si ningún proceso está en sección crítica y hay procesos que desean entrar en su s.c., sólo estos últimos participarán en la decisión y ésta se tomará en un tiempo finito. Espera limitada: hay un límite para el número de veces que otros procesos pueden adelantarse a un proceso que quiere entrar en s.c.
17
Importante No podemos suponer nada sobre la velocidad relativa de los procesos, ni el orden de ejecución.
18
Solución primitiva: inhibir las interrupciones
Antes de que un proceso entre en su sección crítica, se inhiben las interrupciones Así es imposible que el proceso sea expulsado de la CPU mientras está accediendo al dato compartido Al salir de la SC, se rehabilitan las interrupciones
19
Inhibir las interrupciones: inconvenientes
Mientras un proceso está en SC, se suspende toda la concurrencia en el sistema perjudica a los procesos que no están intentando entrar en SC NO sirve en multiprocesadores (no se puede enviar una señal simultánea a todos los procesadores) NO garantiza espera limitada
20
Soluciones software con espera activa (busy waiting)
La sincronización se basa en que un proceso espera mediante la comprobación continua de una variable. while (no_puedo_seguir) { } Ojo, la CPU se mantiene ocupada mientras el proceso espera (ineficiente).
21
Intento ingenuo: usar un indicador de disponibilidad
// variable global // indica si la sección crítica está libre bool ocupado = false; // código que ejecuta cada proceso while (true) { … sección no crítica … while (ocupado) {} ocupado = true; … sección crítica … ocupado = false; }
22
Primer intento serio: variable turno
(solución para dos procesos) int turno = 1; while (true) { SNC1; while (turno!=1) {} SC1; turno=2; } while (true) { SNC2; while (turno!=2) {} SC2; turno=1; } Como ejemplo considerar que los dos procesos están, representados por dos esquimales. Existe un iglú, dentro del cual hay una pizarra. La entrada al iglú es tan estrecha que solo cabe un esquimal por ella. De la misma manera cuando un esquimal esté dentro del iglú, el otro no puede entrar. El recurso compartido por los dos esquimales es un agujero en el hielo para pescar. El agujero está a medio día de marcha del iglú. Cuando el esquimal 1 quiere utilizar este agujero, va primero al iglú y mira que hay anotado en la pizarra. Si hay un 2, significa que el otro esquimal está utilizando el agujero. Por ello empieza a pasearse y de vez en cuando entra al iglú a comprobar el valor de la pizarra. Cuando se encuentre que en ella hay un 1, significa que ya puede pescar, porque el otro esquimal ha dejado de hacerlo. Este actúa de forma similar y así sucesivamente. Los esquimales son los procesos, el agujero de pesca el recurso compartido y la pizarra una variable que los procesos utilizan para saber si pueden utilizar o no el recurso. El iglú es una manera de indicar que existe un árbitro de memoria para controlar los accesos simultáneos a la variable. En el caso de que los dos procesos intentasen acceder simultáneamente a la variable, la entrada al iglú permitirá que pase uno u otro, pero no los dos. proceso 1 proceso 2
23
Discusión del primer intento
¿Exclusión mutua? ¿Espera limitada? ¿Progreso? Peterson: No garantizaría el requisito de progreso Problema es la alternancia. Oso se come al esquimal que espera. Incluso aunque esto no ocurriera nos es una buena solución si los procesos no acceden al recurso compartido con la misma frecuencia.
24
Segundo intento: avisadores
bool quiere1=false, quiere2=false; while (true) { SNC1; quiere1=true; while (quiere2) {} SC1; quiere1=false; } while (true) { SNC2; quiere2=true; while (quiere1) {} SC2; quiere2=false; } Equivale a emplear en el ejemplo dos igloos con una pizarra cada uno de ellos. No se garantiza la exclusión mutua. proceso 1 proceso 2
25
Algoritmo de Peterson – Funciona (para 2 procesos)
bool quiere1=false, quiere2=false; int turno = 1; while (true) { SNC1; quiere1 = true; turno = 2; while (quiere2 && turno==2) {} SC1; quiere1 = false; } while (true) { SNC2; quiere2 = true; turno = 1; while (quiere1 && turno==1) {} SC2; quiere2 = false; }
26
Solución para N procesos: Algoritmo de la panadería (Lamport)
bool cogiendonumero [N] = { [0..N-1] = false }; int num[N] = { [0..N-1] = 0 }; // código del proceso "i" while (true) { Sección no crítica (i); // cojo número cogiendonumero[i] = true; num[i] = 1 + MAX(num(1) ... num(n)); cogiendonumero[i] = false; // Comparo mi número con los de los demás procesos for (int j=0;j<N;j++) { if (i==j) continue; // no me comparo conmigo mismo while (cogiendonumero[j]) {} // por si lo he pillado cogiendo número if (num[j]==0) continue; // si no está interesando, sigo con otro // me mantengo en espera si j ha cogido un número menor que el mío // o tiene el mismo número que yo, pero j<i while ( num[j]<num[i] || (num[j]==num[i] && j<i) ) {} } Sección crítica (i); num[i] = 0;
27
Instrucciones hardware atómicas
Inventadas en los años 60. Permiten evaluar y asignar un valor a una variable de forma atómica. test-and-set(B): Pone B a true y devuelve el antiguo valor de B. SWAP(A,B): Intercambia los valores de A y B. Si disponemos de estas instrucciones, se simplifica muchísimo el problema de la sección crítica.
28
Algoritmo básico con test-and-set
bool ocupado = false; while (true) { Sección no crítica; while ( Test_and_Set(ocupado) ) {} Sección crítica; ocupado = false; } Problema: Espera limitada (inanición) OJO: no garantiza espera indefinida
29
Algoritmo básico con SWAP
bool ocupado = false; while (true) { Sección no crítica; bool aux=true; // variable local while (aux) { SWAP(ocupado,aux); } Sección crítica; ocupado=false;
30
Solución que resuelve la espera indefinida (test-and-set)
bool esperando[N] = { [0 … N] = false }; bool cerradura = false; while (true) { SNCi; esperando[i] = true; bool llave = true; while (esperando[i] && llave) { llave = test_and_set(cerradura); } esperando[i] = false; SCi; int j=(i+1)%N; while ( j!=i && !esperando[j] ) { j=(j+1)%N; if (j==i) cerradura = false; else esperando[j] = false;
31
Mutex (cerrojos) y variables condición
32
Necesidad de unas herramientas más generales
La sección crítica no es el único problema de sincronización que nos podemos encontrar. Sería bueno disponer de herramientas y técnicas de sincronización que nos permitan resolver nuevos problemas sin necesidad de estar elaborando complejos algoritmos cada vez.
33
Herramientas de sincronización
Basadas en memoria compartida: Semáforos (Dijkstra, 1965) Regiones críticas condicionales (Hansen, 1972) Monitores (Hoare, 1972) mutex, variables condición Basadas en el paso de mensajes: Canales (Hoare, 1978) Buzones
34
Mutex o cerrojo (mutex = “mutual exclusion”) Un objeto que sirve para adquirir en exclusiva el derecho de acceso a un recurso. Dos posibles estados: libre y adquirido. Inicialmente está libre. Operaciones: lock() Adquiere el mutex. Si el mutex ya estaba adquirido, el proceso se bloquea hasta que lo consigue adquirir. unlock() Libera el mutex. Si hay procesos en espera por el mutex, uno de ellos lo adquiere y se desbloquea. Las operaciones garantizan ejecución atómica.
35
Ejemplo: sección crítica resuelta con mutex
Utilizamos un mutex para controlar el acceso en exclusiva a la s.c. { Sección no crítica mutex.lock(); Sección crítica mutex.unlock(); }
36
Variables condición Una variable condición sirve para gestionar una cola de espera por un recurso o una condición lógica. La v.c. está siempre asociada a un mutex. Operaciones: wait() bloquea al proceso y lo mete en la cola de la v.c. Mientras el proceso está bloqueado, se libera el mutex. Cuando el proceso se desbloquea, debe volver a adquirir el mutex. signal() desbloquea a un proceso de la cola; si no hay procesos en cola, no se hace nada. broadcast() desbloquea a todos los procesos de la cola. La operación wait(x) suspende al proceso que la invoca hasta que otro proceso invoque signal(x). La operación signal(x) reanuda uno y sólo un proceso suspendido. Si no hay procesos suspendidos, la operación signal no tiene ningún efecto. Contrastar estas operaciones con las realizadas sobre semáforos.
37
Esquema típico de uso de mutex y variables condición
Mutex mutex = new Mutex(); Condition cond = new Condition(mutex); void EjemploEspera() { … haz cosas mutex.lock(); … hay que esperar … por alguna condición while (condición) { cond.wait(); } mutex.unlock(); void EjemploSeñal() { … haz cosas mutex.lock(); … se cumple la condición cond.signal(); mutex.unlock(); }
38
Ejemplo: esperar por un evento
Usamos una variable condición para encolar a los procesos que esperan a que suceda el evento. bool ocurrio = false; Mutex mutex = new Mutex(); Condition cola = new Condition(mutex); P1() { … haz cosas … señaliza el evento mutex.lock(); ocurrio = true; cola.broadcast(); mutex.unlock(); } P2() { … espera por el evento mutex.lock(); if (!ocurrio) { cola.wait(); } mutex.unlock(); … haz cosas
39
Qué ocurre tras un «signal»
¿Qué ocurre cuando un proceso P realiza una operación signal sobre una variable condición C y existe un proceso Q bloqueado en C? Estilo Hoare Q se reanuda inmediatamente (a P se le despoja del mutex) Estilo Mesa Q se desbloquea, pero espera a que P libere el mutex El «estilo Mesa» es el más utilizado en los lenguajes de programación actuales 1.- Se reanuda Q. P espera hasta que Q salga del monitor o espere otra condición (Hoare) 2.- Q espera hasta que P salga del monitor o espere otra condición 3.- Concurrent Pascal: Cuando P ejecuta la operación signal, sale inmediatamente del monitor y Q se reanuda de inmediato
40
Qué ocurre tras un «signal» (2)
Si varios procesos están bloqueados en una variable condición C y algún proceso ejecuta C.signal, ¿cuál de los procesos se reanuda? FIFO desbloqueamos al más antiguo Por prioridades conveniente en sistemas de tiempo real Al azar es lo que ocurre en implementaciones con espera activa Desbloqueamos a todos y que ellos se peleen por el mutex (operación «broadcast») 1.- FCFC 2.- Espera condicional: x.wait(c). A c se le denomina número de prioridad y se guarda junto con el proceso suspendido, de forma que cuando algún proceso realice una operación x.signal, se reanuda el proceso con número de prioridad más pequeño (por ejemplo).
41
Mutex: implementación con espera activa
También llamados spinlocks y futex class Mutex { private bool ocupado; public Mutex() { ocupado=false; } public void lock() { while (Test-And-Set(ocupado)) { …nada… } } public void unlock() { ocupado=false; Este tipo de semáforos se conocen también con el nombre de cerradura de giro. Comentar desventajas y ventajas en un entorno multiprocesador (evitar los cambios de contexto que podrían tardar un tiempo considerable). En los casos en los que se espera que las cerraduras se usen durante intervalos cortos, las cerraduras de giro son útiles. En entornos con un solo procesador, desperdicio.
42
Mutex: implementación sin espera activa
Suponemos que el SO proporciona unas operaciones para bloquear y desbloquear procesos class Mutex { private bool ocupado; private List<Process> cola; public Mutex() { ocupado=false; } public void lock() { if (ocupado) { var p=procesoActual(); cola.inserta(p); DORMIR(); } ocupado=false; public void unlock() { if (!cola.vacia()) { var proceso=cola.extrae(); DESPERTAR(proceso); } ocupado=false; Explicar la solución y relacionarla con los estados en los que pueden estar los procesos. En esta implementación el valor puede ser negativo (en la definición clásica de semáforo con espera activa el valor nunca es negativo). (número de procesos que esperan en el semáforo). Implementación de la lista. Puntero a BCP. Política de la cola: FIFO por ejemplo.
43
Variables condición: implementación sin espera activa
Suponemos que el SO proporciona unas operaciones para bloquear y desbloquear procesos class Condition { private Mutex mutex; private Lista<Process> cola; public Condition(Mutex m) { mutex=m; } public void wait() { // ojo: el proceso llamador debe // tener adquirido el mutex var p=procesoActual(); cola.inserta(p); mutex.unlock(); DORMIR(); mutex.lock(); } public void signal() { if (!cola.vacia()) { var proceso=cola.extrae(); DESPERTAR(proceso); } Explicar la solución y relacionarla con los estados en los que pueden estar los procesos. En esta implementación el valor puede ser negativo (en la definición clásica de semáforo con espera activa el valor nunca es negativo). (número de procesos que esperan en el semáforo). Implementación de la lista. Puntero a BCP. Política de la cola: FIFO por ejemplo.
44
ojo: la implementación debe garantizar atomicidad
Es crítico que las operaciones se ejecuten de forma atómica Entorno uniprocesador: Inhibir interrupciones Entorno multiprocesador: Instrucciones hardware especiales Aplicar un algoritmo de sección crítica con espera activa Se ha transferido la espera activa de las secciones de ingreso a las secciones críticas de los programas de aplicación. Sin embargo, hemos limitado la espera activa exclusivamente a las secciones críticas de las operaciones wait y signal, y estas secciones son cortas (si se codifican hábilmente no deberían requerir más de 10 instrucciones cada una). De este modo, la SC casi nunca se ocupa, y la espera activa es muy poco frecuente y dura poco tiempo. Se presenta una situación muy diferente en los programas de aplicación cuyas secciones críticas podrían ser largas (minutos o incluso horas) o estar casi siempre ocupadas. En estos casos la espera activa es un extremo ineficiente.
45
Problemas clásicos de sincronización
46
Problemas clásicos de sincronización
Problema del búfer finito Problema de los lectores y escritores 1er problema: prioridad para los lectores 2º problema: prioridad para los escritores Problema de los filósofos
47
Búfer finito (bounded buffer)
Procesos productores y consumidores que insertan/extraen elementos en una cola FIFO de tamaño limitado. apuntes.pdf foto.png diagrama.bmp productores consumidores búfer compartido
48
Búfer finito (bounded buffer)
const int N = …; struct Item { … }; Item* buffer[N]; int entra=0, sale=0, pendientes=N; Mutex mutex = new Mutex(); Condition cprod = new Condition(mutex); Condition ccons = new Condition(mutex); Productor Consumidor Item* cosa = producir_algo(); mutex.lock(); while (pendientes==0) { cprod.wait(); } buffer[entra] = cosa; entra = (entra+1)%N; pendientes++; ccons.signal(); mutex.unlock(); mutex.lock(); while (pendientes==N) { ccons.wait(); } Item* cosa = buffer[sale]; sale = (sale+1)%N; pendientes--; cprod.signal(); mutex.unlock(); consumir(cosa);
49
Lectores y escritores (Courtois, Heymans & Parnas, 1971)
Esquema útil para gestionar el acceso a una base de datos: Puede haber varios lectores accediendo a la BD de forma concurrente Sólo puede haber un escritor trabajando No puede haber lectores y escritores al mismo tiempo … y si se puede, que no haya inanición
50
Lectores y escritores Escritor Lector ¿cómo lo podemos resolver?
Lectura() { // esperar que no haya // ningún escritor LEER DE LA BD; } Escritura() { // esperar a que no haya // ningún otro proceso ESCRIBIR EN LA BD; } ¿cómo lo podemos resolver?
51
Lectores y escritores: dos variantes del problema
Primera variante: prioridad para los lectores Si un escritor está esperando, se le pueden adelantar otros lectores Ojo, riesgo de inanición para los escritores Segunda variante: prioridad para los escritores Si hay escritores esperando por la BD, los lectores que van llegando nuevos se deben esperar hasta que todos los escritores finalicen Ahora hay riesgo de inanición para los lectores
52
Los filósofos (the dining philosophers) (Dijkstra, 1965)
53
Los filósofos (the dining philosophers) (Dijkstra, 1965)
Cinco filósofos en torno a una mesa. Pasan el tiempo comiendo y meditando. Para comer necesita coger los dos palillos que están a su lado. Filósofo (i) { while (true) { coger palillo(i); coger palillo((i+1) mod 5); COMER(); soltar palillo(i); soltar palillo((i+1) mod 5); MEDITAR(); }
54
Filósofos: ojo al interbloqueo
Si los filósofos cogen los palillos sin control, se puede llegar a un estado de bloqueo mutuo entre todos los filósofos interbloqueo Posibles soluciones: Algoritmo asimétrico filósofos pares/impares Impedir a más de cuatro filósofos entrar a pedir los palillos Coger los dos palillos de forma atómica (o coges los dos, o no coges ninguno)
Presentaciones similares
© 2025 SlidePlayer.es Inc.
All rights reserved.