Exclusión Mutua y Semáforos
-
Upload
sammy-manuel-dominguez -
Category
Documents
-
view
786 -
download
1
Transcript of Exclusión Mutua y Semáforos
Exclusión Mutua, Semáforos, Monitores
Hoy
• Soporte de hardware para la sincronización• Abstracciones de sincronización de alto nivel
– Semáforos, monitores, y variables de condición
• Paradigmas de programación de programas concurrentes
Problema general
• La abstracción de los hilos es buena:– Mantienen el modelo de ejecución secuencial – Permiten una implementación simple de
paraleleismo• Desafortunadamente, resuslta complicado
accesar estados compartidos entre los hilos – Ejemplo “Demasiada Leche”– Implementar un programa concurrente usando
solo carga y almacenamiento atómicos a memoria es unmétodo truculento y propenso a errores
• Ahora veremos como implementar operaciones de mas alto nivel encima de las operaciones atómicas provistas por el hardware– Creación herramientas para sincronización– Exploración de paradigmas de programación
A donde vamos con la sincronización?
• Es necesario implementar primitivas de sincronización de más alto nivel– Es muy difícil si las únicas primitivas atómicas
son cargar de memoria y almacenar en memoria (load and store)
– Es necesario tener primitivas útiles a nivel de usuario
Load/Store Disable Ints Test&Set Comp&Swap
Locks Semáforos Monitores Send/Receive
Programas compartidos
Hardware
API de mas alto
nivel
Programas
Como implementar Locks?
• Lock: evita que alguien haga algo– Se usa un lock antes de entrar a una sección crítica y
antes de usar datos compartidos– Se remueve el lock al salir, y luego de acceder a los
datos– Si se encuentra un lock se debe esperar
» Toda sincronización implica esperar• Carga/almacenamiento atómicas: solición Leche #3
– Muy compleja y propensa a errores• Instrucción de Lock en Hardware
– Buena idea?– Complejo?
» Cada nueva función hace al hardware mas complejo y lento
– Como se pondría un hilo a dormir desde el hardware?» Como sería el interface entre el hardware y el
programador?
• Como construir operaciones atómicas de multiples instruciones?– Recordar: El despachador toma el control de dos maneras.
» Interna: El hilo hace algo para renunciar al CPU» Externa: Las interrupciones hacen al despachador tomar control
– En un ambiente uniprocesador, se evita el switcheo de hilo:» Evitando eventos internos (difícil con memoria virtual, swap)» Evitando los eventos externos al deshabilitar la interrupciones
• Veamos una implementacion ingenua de locks:LockAcquire { disable Ints; }LockRelease { enable Ints; }
• Problemas con este enfoque:– No se puede permitir al usuario hacer esto! Consideremos
esto:LockAcquire();While(TRUE) {;}
– Sistemas de Tiempo Real — no hay garantías de tiempo! » Las secciones críticas pueden ser arbitrariamente largas
– Que pasa con las E/S y otros eventos importantes?» “Peligro alerta roja. Ayuda!”
Implementación ingenua usando habilitar/deshabilitar interrupciones
Una mejor implementación de Locks Deshabilitando Interrupciones
• Idea clave: mantener una variable de lock e imponer exclusión mutua solo en los accesos a esa variable
int value = FREE;
Acquire() {disable interrupts;if (value == BUSY) {
poner hilo en espera;ir a sleep();// Habilitar interrup?
} else {value = BUSY;
}enable interrupts;
}
Release() {disable interrupts;if (anyone on wait queue) {
sacar hilo de esperaPoner en cola ready;
} else {value = FREE;
}enable interrupts;
}
Implementacion de nuevo Lock: Discusion
• Por que tenemos que deshabilitar las interrupciones?– Evitar interrupciones entre el chequeo y seteo del el valor de
lock– De otra manera ambos hilos podrían pensar que tienen el lock
• Nota: a diferencia de la solución anterior, la sección crítica (dentro de Acquire()) es bien corta– El usuario del lock puede durar lo que desee en su
seción crítica sin impactar al sistema– Interrupciones críticas pueden atenderse a tiempo!
Acquire() {disable interrupts;if (value == BUSY) {
poner hilo en espera;ir a sleep();// Habilitar interrup?
} else {value = BUSY;
}enable interrupts;
}
SecciónCrítica
Habilitación de interrupciones en “going to sleep”
• Como habilitar interrupciones cuando se va a dormir?
• Antes de colocar el hilo en la cola de espera?– Release puede verificar la cola y no encontrar el hilo
• Luego de colocar el hilo en la cola de espera?– Release pone al hilo en la lista de ready, pero el hilo
sigue pensando que debe ir a dormir– Se pierde el wakeup y se queda con el lock (deadlock!)
• Ponerlo después de el sleep(). Pero – como?
Acquire() {disable interrupts;if (value == BUSY) {
poner hilo en espera;ir a sleep();
} else {value = BUSY;
}enable interrupts;
}
Habilitar IntHabilitar IntHabilitar Int
Como habilitar despues de ir a dormir - Sleep()?
– Es responsabilidad del próximo hilo habiltar las interrupciones
– Cuando un hilo dormido despierta, retorna a Acquire() y habilita las interruciones
Hilo A Hilo B..
deshabilitar intssleep
sleep return enable ints
. . .
disable int sleep
sleep returnenable ints
.
.
switchcontexto
switch
contexto
Instruciones atómicas Lectura-Modificación-Escritura
• Problemas con esta solución:– No se puede permitir uso a nivel de usuario– No trabaja bien en ambiente multiprocesador
» Deshabilitar las int en todos los proc. requiere envio de mensajes y consume mucho tiempo
• Alternativa: Secuencias de instruciones atómicas– Estas instruciones leen un valor de memoria y
escriben otro valor de manera atómica– La implementación se hace a nivel de hardware
» tanto en ambiente uniprocesador (not too hard) » como en multiprocesador (requiere de protocolos
de coherencia de las memoria cache de los proc.)
– A diferencia de la técnica de deshabilitar int., esta solución funciona en ambientes uniprocesador y multiprocesador
Ejemplo de secuencias Leer-Modificar-Escribir
• test&set (&address) { /* Mayoría de arquitecturas */result = M[address];M[address] = 1;return result;
}• swap (&address, register) { /* x86 */
temp = M[address];M[address] = register;register = temp;
}• compare&swap (&address, reg1, reg2) { /* 68000 */
if (reg1 == M[address]) {M[address] = reg2;return success;
} else {return failure;
}}
• load-linked&store conditional(&address) { /* R4000, alpha */
loop:ll r1, M[address];movi r2, 1; /*Puede hacer comp
arbitrarias*/sc r2, M[address];beqz r2, loop;
}
Implementación de Locks con test&set
• Otra solución incompleta, pero simple:int value = 0; // LibreAcquire() {
while (test&set(value)); // while busy}Release() {
value = 0;}
• Explicación simple:– Si lock esta libre, test&set lee 0 y hace value=1, de
esta manera lockcambia a ocupado. Retorna 0, por lo que “while” termina.
– Si lock esta ocupado, test&set lee 1 y hace value=1 (no hay cambio). Retorna 1, entonces el loop “while” continúa
– Cuando hacemos value = 0, otro hilo puede tomar el lock
• Espera ocupada (Busy-Waiting): el hilo consume ciclos del reloj mientras espera (implementación deficiente)
Problema: Espera ocupada por Lock• Lo positivo en esta solución
– La maquina puede recibir interrupciones– Se puede usar a nivel de usuario– Funciona bien en ambiente multiprocesador
• Lo negativo– Muy ineficiente la espera ocupada consume ciclos
del reloj mientras espera– El hilo en espera podría estar usando los ciclos
que necesita el hilo que tiene el lock (todos pierden!)
– Inversión de prioridad: Si el que espera tiene mayor prioridad que el que tiene el lock no hay progreso!
• El Martian Rover original sufrió un problema de inversión de prioridad
• La espera ocupada puede durar un lapso arbitrario de tiempo!– Aún si esta solución funcionara para locks no
resultaría para otras primitivas de sincronización como son los semáforos y monitores
Mejores locks usando test&set• Se pueden construir locks sin espera ocupada
usando test&set?– No del todo , pero se puede minimizar!– Idea: solo permitir espera ocupada al verificar lock
• Nota: sleep debe asegurarse resetear variable guard– Pro que no hacerlo justo antes o después de dormir ?
Release() {// Tiempo de espera cortowhile (test&set(guard));if alguien en cola espera{
sacar hilo de esperaPoner en cola ready;
} else {value = FREE;
}guard = 0;
int guard = 0;int value = FREE;
Acquire() {// Tiempo de espera cortowhile (test&set(guard));if (value == BUSY) {
poner hilo en espera;ir a sleep() & guard = 0;
} else {value = BUSY;guard = 0;
}}
Primitivas de mayor nivel de los Locks
• Cual es la meta:– Cual es la abstracción correcta para permitir la
interacción entre hilos concurrentes?– Es deseable tener primitivas del mayor nivel
posible
• Las buenas primitivas y la buena práctica es clave!– La ejecución no es enteramente secuencial y los
bugs se hacen difíciles de encontrar; son intermitentes.
• La sincronización es una manera de coordinar multiples actividades concurrentes que comparten informaciones de estado– Veremos algunas formas en que se pueden
estructurar estas interacciones
Semáforos
• Los semáforos son un tipo de lock generalizado– Fueron definidos por Dijkstra a finales de los 60s– Fue la principal primitiva de sincronización usada
en UNIX
• Definición: u Semáforo consta de un valor entero no negativo e implementa las sgtes. dos operaciones :– P(): una operación atómica que espera que el
semáforo se haga positivo, entonces lo decrementa en 1 » Se puede ver como la operación wait()
– V(): una operación atómica que incrementa el semáforo en 1, despertando cualquier hilo que duerma en P, si es que existe alguno» Se puede ver como la operación signal()
– La P en P() viene de “proberen” (probar) y V() viene de “verhogen” (incrementar) en Alemán
Value=2Value=1Value=0
Los semáforos son como Enteros, solo que...
• Los semáforos son como Enteros, solo que...– No tienen valores negativos– Solo permiten las operaciones P y V – no
permiten que se lea o escriba su valor, exepto al momento de inicializarlos
– Las operaciones son atómicas» Nunca dos P’s seguidas pueden decrementar el
value por debajo de cero» De manera similar un hilo que duerma en P nunca
perderá una llamada de “weakup” (despertar) de V – aún si ambos eventos sucedieran al mismo tiempo
• Semaforó: analogía del ferrocarril– Este es un semáforo inicializado a 2 para control
de recursos:
Value=1Value=0Value=2
Dos usos de los semáforos• Exclusión mutua (inicio value = 1)
– También llamado “Semaforo binario”.– Puede usarse para exclusión mutua:
semaphore.P();// Sección crítica va aquísemaphore.V();
• Restricción de Organización (initial value = 0)– Los locks son buenos para exclusión mutua,
pero como hacer si queremos que un hilo espere por cierta condición?
– Ejemplo: implementar un ThreadJoin que deba esperar que otro hilo termine:
valor inicial de semaforo = 0ThreadJoin { semaphore.P();}ThreadFinish { semaphore.V();}
Producer-consumer with a bounded buffer
• Problema– Productores llenan buffer– Consumidores vacían el buffer– Se necesita sicronizacion productor/consumidor
• Se evita una interacción rígida entre productores y consumidores agregando un buffer de tamaño fijo entre ellos– El acceso al buffer debe sincronizarse– Si el buffer esta lleno los productores deben esperar– Si el buffer esta vacío los consumidores deben esperar
• Ejemplo 1: GCC compiler– cpp | cc1 | cc2 | as | ld
• Ejemplo 2: Máquina expendedora de soda– Los productores pueden poner un número limitado de
refrescos en la máquina– Los consumidores no pueden obtener refrescos si la
máquina esta vacía
Productor ConsumidorBuffer
Condiciones de corrección de esta solución
• Condiciones de corrección :– Los consumidores deben esperar por que los
productores llenen las posiciones del buffer, si todas están vacías (restricción del orden de ejecución)
– Los productores deben esperar que los consumidores vacíen los buffers, si todos están llenos (restricción del orden de ejecución)
– Solo un hilo puede manipular la cola del buffer en determinado momento (exclusión mutua)
• Pro que necesitamos la exclusión mutua?– Las computadoras son estúpidas– Si nos vamos a la vida real, nadie intentaría sacar
comprar refrescos de una máquina a la vez que el encargado esta cergando refrescos en la máquina.
• Regla general: Usar un semáforo para cada Restricción que deba observarse– Semáforo fullBuffers; // Restricción a consumidores– Semáforo emptyBuffers;// Restricción a productores– Semáforo mutex; // exclusión mutua
Solución completa para buffers acotados
Semáforo fullBuffer = 0; // Inicialmente, no refrescosSemáforo emptyBuffers = numBuffers;
//Inicialmente,# slots vacíosSemáforo mutex = 1; // nadie usa la máquina
Productor(item) {emptyBuffers.P(); // Esperar hasta espaciomutex.P(); // Esperar máquina libreEnqueue(item);mutex.V();fullBuffers.V(); // Decir a los consumidores
// que hay mas refresco}Consumidor() {
fullBuffers.P(); // Verificar si hay refrescomutex.P(); // Esperar máquina libreitem = Dequeue();mutex.V();emptyBuffers.V(); // Decir a los productores
que hay buffers vacíosreturn item;
}
Discusión sobre Solucion
• Por que asimétrica?– Productores: emptyBuffer.P(), fullBuffer.V()– Consumidores: fullBuffer.P(), emptyBuffer.V()
• Es importante el orden de las P’s?– Si! el orden incorrecto puede provocar
deadlock
• Es importante el orden de las V’s?– No, a los umo podría afectar la eficiencia
• Que pasa si fueran dos productores y dos consumidores?– Habría que cambiar algo en el código?
Monitores y Variables de Condición
• Los semáforos son un gran avance; solo hay que imaginar solucionar el problema de buffer acotado usando solamente lectura y escritura a memoria– El problema es que los semáforos tienen doble
propósito:» Se usan tanto para Mutex como para Restricciones de
Orden de Ejecución» El hecho de que invirtiendo las P’s en la solución de los
buffers acotados produzca un deadlock no es imediatamente evidente. Esto dificulta probar la exactitud
• Una idea más calra: Usar locks para exclusion mutua y Variables de Condición para restricciones de orden de ejecución
• Definicion: Monitor: Un lock y cero o más variables de condición para manejar el acceso concurrente a datos compartidos– Algunos lenguajes como Java proveen esto
intrísicamente– La mayoría de los demás lenguajes ofrecen
implementaciones de locks y variables de condición
Monitores
• Lock: El lock provee exclusión mutua para datos– Siempre se debe adquirir antes de accesar a datos comp.– Siempre se debe liberar al terminar de usar datos comp.– El lock esta inicialmente libre
• Variable de Condición: una cola de hilos esperando por algo dentro de una sección crítica– Idea clave: posibilitar el dormir en una sección crítica
mediante una operación atómica que libera el lock y pone a dormir el hilo
– Esto a diferencia de los semáforos que no permiten dormir dentro de una sección crítica
Ejemplo simple de monitor• Aqui vemos una cola (infinita) sincronizada
Lock lock;Condicion dataready;Queue queue;
AddToQueue(item) { //Función agregar item lock.Acquire(); // Adquirir Lock
queue.enqueue(item); // Agregar itemdataready.signal(); // Avisar al que esperalock.Release(); // Liberar Lock
}
RemoveFromQueue() { //Función remover itemlock.Acquire(); // Adquirir Lock while (queue.isEmpty()) {
dataready.wait(&lock); // No items, dormir}item = queue.dequeue(); // Tomar itemlock.Release(); // Liberar Lockreturn(item);
}
Resumen• Concepto Importante: Operaciones atómicas
– Corren completas o no corren– Son las primitivas sobre las cuales se construyen
las primitivas de sincronización• Primitivas atómicas por hardware:
– Deshabilar interrupciones, test&set, swap, comp&swap, load-linked/store conditional
• Implementaciones de Locks– Se debe tener cuidado de no malgastar ni acaparar
los recursos de la máquina» No se deben deshabilitar la int. por mucho timepo» No se debe esperar activamente por mucho tiempo
– Idea clave: Variable de lock separada, usar mecanismos de harware para protejer las modificaciones de esa variable
• Semáforos, Monitores, and Variables de Ccondición– Implementaciones de más alto nivel, más seguras