Estructuras de Datos Concurrentes

Anuncio
Estructuras de Datos Concurrentes Ingeniería en Informá6ca, Universidad de Tarapacá, Iquique Yadran Eterovic 14 agosto 2014 Los procesadores de múl6ples núcleos … No se trata sólo de una nueva clase de máquinas de múl6ples procesadores corriendo programas paralelos } 
2 como se ha venido haciendo por casi 30 años … están revolucionando las estructuras de datos En parte, por los cambios que estos procesadores implican para las arquitecturas paralelas Principalmente, por los cambios en las aplicaciones que van a ser paralelizadas: } 
3 los procesadores de múl6ples núcleos llevan el paralelismo a la computación que podemos llamar “mainstream” El paralelismo se ha usado en computación cienQfica y en gráfica computacional Los patrones de comunicación y coordinación son regulares y cambian poco 4 Hoy hay múl6ples núcleos en todo 6po de disposi6vos Teléfonos, laptops, computadores de escritorio, servidores Por lo tanto, … gran can6dad de aplicaciones con interacciones e intercambios de datos complejos y rápidamente cambiantes Lo que nos presenta un desaWo 5 ¿Por qué nos presenta un desaWo? Cuánto podemos aumentar la velocidad de un proceso computacional complejo … está limitado por cuánto del proceso debe ser ejecutado secuencialmente Esta idea se ve reflejada en la ley de Amdahl 6 La ley de Amdahl Definamos el aumento de velocidad (o speedup) S de un proceso computacional: cociente entre el 6empo que toma a un computador realizar la computación … y el 6empo que toma a n procesadores concurrentes realizar la misma computación La ley de Amdahl caracteriza el máximo speedup S que puede lograrse si la fracción del proceso computacional que puede ser ejecutada en paralelo es p 7 La ley de Amdahl S=
1
1" p+
8 p
n
S no crece linealmente con n Supongamos que podemos paralelizar el 90% de la aplicación (p = 0.9): si la máquina 6ene n = 10 procesadores, … el aumento de velocidad es de 5 veces si la máquina 6ene n = 20 procesadores, … el aumento es de (casi) 7 veces si la máquina 6ene infinitos procesadores, … el aumento es de 10 veces 9 ¿A qué se debe el otro 10%? Interacción y coordinación entre threads … que en el caso de computadores de múl6ples núcleos … significa acceso concurrente a estructuras de datos compar<das 10 ¿Qué nos sugiere la Ley de Amdahl al respecto? Por lo tanto, … vale la pena inver6r esfuerzo para obtener tanto paralelismo como sea posible de este 10% … para lo cual es clave tener estructuras de datos altamente concurrentes 11 El problema productor/consumidor Tenemos dos procesos, Producer y Consumer: } 
Producer 6ene un arreglo local a[n] de enteros, que está inicializado } 
Consumer 6ene un arreglo local b[n] de enteros } 
12 el obje6vo es copiar el contenido de a a b Los procesos usan variables compar6das y 6enen acceso al buffer alternadamente Los procesos 6enen que usar variables compar6das para comunicarse entre ellos: } 
sea buffer un entero que sirve como buffer de comunicación Los procesos 6enen que alternar el acceso a buffer: } 
13 Producer deposita el primer ítem de a en buffer, luego Consumer lo lee, luego Producer deposita el segundo ítem de a, etc. Establecemos un requisito de sincronización entre los procesos … Sean p y c variables compar6das que cuentan el número de ítemes que han sido depositados y leidos, respec6vamente: } 
inicialmente, p y c son ambos 0 Requisito de sincronización entre Producer y Consumer: 14 } 
PC: c ≤ p ≤ c+1 } 
Los valores de p y c pueden diferir a lo más en 1: Producer ha depositado a lo más un ítem más que los que Consumer ha leido … que depende de los valores de los contadores Los procesos usan p y c para sincronizar el acceso a buffer: 15 } 
usan sentencias atómicas await para esperar hasta que el buffer esté vacío o lleno } 
cuando p == c el buffer está vacío (el ítem depositado previamente ha sido leido) } 
cuando p > c el buffer está lleno El 6po de sincronización resultante se llama espera ocupada Este 6po de sincronización produce procesos que están ocupados esperando: 16 } 
el proceso está ocupado probando la condición en su sentencia await, pero todo lo que hace es “dar vueltas” en un ciclo hasta que la condición sea verdadera } 
esta sincronización es común —de hecho, necesaria— en los niveles más bajos de los sistemas de somware: sistemas opera6vos, protocolos de red int buffer, p = 0, c = 0 Process Producer int a[n] while (p < n) 〈 await (p == c) 〉 buffer = a[p] p++ Process Consumer int b[n] while (c < n) 〈 await (p > c) 〉 b[c] = buffer c++ 17 Es diWcil diseñar estructuras de datos concurrentes Hay una tensión entre corrección y desempeño: mientras más tratamos de mejorar el desempeño, … más diWcil se hace razonar acerca de la corrección del algoritmo resultante Algunos culpan al modelo de programación: } 
threads que se comunican mediante objetos compar6dos … pero, por ahora, ese es el modelo que tenemos 18 La corrección en el mundo concurrente 6ene dos aspectos Seguridad —garan6zar que nada malo pase Progreso —garan6zar que algo bueno finalmente pase 19 Con respecto a la seguridad en estructuras de datos concurrentes Se ve complicada debido a las múl6ples intercalaciones posibles de los métodos llamados por los diversos threads El enfoque estándar: } 
especificar las propiedades de la estructura secuencialmente … y encontrar una forma de hacer corresponder las ejecuciones concurrentes a estas ejecuciones secuenciales correctas … p.ej., linealizabilidad 20 Con respecto al progreso en estructuras de datos concurrentes Lo que queremos es que las llamadas a los métodos finalmente se completen (retornen) Condiciones de progreso, p.ej., } 
ausencia de deadlock } 
ausencia de inanición … muchas veces dependen del sistema de despacho (scheduling) subyacente 21 El problema de múl6ples productores y consumidores Varios procesos productores envían mensajes y varios procesos consumidores los reciben Se comunican usando un buffer compar6do, manejado por dos operaciones: 22 } 
deposit, usada por los productores para poner (enviar) un mensaje en el buffer } 
fetch, usada por los consumidores para recibir (leer) el mensaje que está en el buffer Las ejecuciones de deposit y fetch deben alternarse Un mensaje en el buffer no debe ser sobreescrito (antes de que sea leído) Cada mensaje es recibido (leído) sólo una vez deposit debe ejecutarse primero 23 Los semáforos fueron inventados por Edsger Dijkstra en los 60’s Permiten implementar exclusión mutua … y señalizar la ocurrencia de eventos } 
p.ej., interrupciones Un semáforo S es manejado por dos operaciones atómicas: P(S) V(S) Dijkstra recibió el AM Turing Award en 1972 24 En el problema de los productores y consumidores necesitamos dos semáforos binarios Un único semáforo binario garan6za exclusión mutua, … pero no puede garan6zar además alternancia En el caso del problema de los productores y consumidores: 25 } 
los puntos de ejecución crí6cos son iniciar y terminar operaciones deposit y fetch } 
los cambios en el estado del buffer son que se llene y que se vacíe Asociamos un semáforo con cada estado —lleno y vacío— del buffer Sean empty y full dos semáforos que indican si el buffer está vacío o lleno: 26 } 
empty es inicialmente 1; full , 0 } 
un productor que quiere ejecutar deposit debe esperar primero a que el buffer esté vacío } 
cuando un productor pone un ítem, el buffer se llena } 
un consumidor que quiere ejecutar fetch debe esperar primero a que el buffer esté lleno } 
cuando un consumidor recibe un ítem, el buffer se vacía bufferType buffer semaphore empty = 1, full = 0 Process Producer[i = 1 … m] while (true) produce data P(empty) buffer.deposit(data) V(full) Process Consumer[j = 1 … n] while (true) P(full) result = buffer.fetch() V(empty) consume result 27 Generalicemos: Un buffer finito 6ene capacidad para múl6ples (> 1) mensajes El buffer con6ene una cola con los mensajes depositados que aún no han sido recibidos: 28 } 
lo representamos por el arreglo buffer[n], con n > 1 } 
front es el índice del mensaje al comienzo de la cola } 
rear es el índice del primer casillero vacío después del mensaje al final de la cola } 
inicialmente, front y rear valen 0 Veamos primero la solución para un productor y un consumidor Para el buffer de capacidad 1, las ejecuciones de deposit y fetch deben alternarse Cuando el buffer 6ene capacidad > 1, 29 } 
deposit puede ejecutarse si hay (al menos) un casillero vacío } 
fetch puede ejecutarse si hay (al menos) un mensaje almacenado } 
deposit y fetch pueden ejecutarse concurren-­‐
temente si al mismo 6empo hay un casillero vacío y un mensaje almacenado —6enen acceso a casilleros diferentes y no interfieren entre ellas 30 bufferType[n] buffer int front = 0, rear = 0 semaphore empty = n, full = 0 Process Producer —un único productor
while (true) produce data P(empty) buffer[rear].deposit(data) rear = (rear+1)%n V(full) Process Consumer —un único consumidor
while (true) P(full) result = buffer[front].fetch() front = (front+1)%n V(empty) consume result Tres observaciones Los requisitos de sincronización son idén6cos a los del buffer de capacidad 1 : } 
la diferencia es que empty es inicializado en n en lugar de 1 Los semáforos son contadores de recursos: } 
empty cuenta el número de casilleros vacíos que hay en buffer } 
full cuenta el número de casilleros llenos que hay en buffer deposit y fetch son ejecutadas como acciones atómicas 31 Cuando hay múl6ples productores y consumidores … Si hay dos (o más) productores, … podrían ejecutar deposit al mismo 6em-­‐
po, suponiendo que hay dos casilleros vacíos, … y podrían tratar de poner el mensaje en el mismo casillero: } 
si ambos ejecutan buffer[rear].deposit
(data) antes que cualquiera de ellos incremente rear Análogamente, si hay dos o más consumidores 32 … deposit y fetch se vuelven secciones crí6cas Las ejecuciones de deposit deben ocurrir bajo exclusión mutua: } 
podrían tener acceso al mismo casillero … y las ejecuciones de fetch deben ocurrir bajo exclusión mutua: } 
podrían tener acceso al mismo casillero Pero deposit puede ejecutar concurrentemente con fetch: } 
33 6enen acceso a casilleros diferentes bufferType[n] buffer int front = 0, rear = 0 semaphore empty = n, full = 0 semaphore mutexD = 1, mutexF = 1 Process Producer[i = 1 … m] while (true) produce data P(empty) P(mutexD) buffer[rear].deposit(data) rear = (rear+1)%n V(mutexD) V(full) 34 Process Consumer[j = 1 … n] while (true) P(full) P(mutexF) result = buffer[front].fetch() front = (front+1)%n V(mutexF) V(empty) consume result 35 El problema involucra dos 6pos de sincronización, ya resueltos por separado 1) La sincronización entre un productor y un consumidor 2) La sincronización entre múl6ples productores (y otra similar entre múl6ples consumidores) Combinamos las soluciones a estos dos sub-­‐
problemas para solucionar el nuevo problema Sugerencia. Cuando hay varios 6pos de sin-­‐
cronización, es ú6l implementarlos por sepa-­‐
rado primero y luego combinar las soluciones 36 
Descargar