200 likes | 427 Views
Concurrencia con Haskell. Antonio Francisco Burdallo Berrocal. Índice. Introduccion Ideas Basicas Procesos: forkIO() Sincronizacion y Comunicación Mvars Canales y Mensajes Planificacion Implementacion Recolector de basura. Introducción.
E N D
Concurrencia con Haskell Antonio Francisco Burdallo Berrocal
Índice • Introduccion • Ideas Basicas • Procesos: forkIO() • Sincronizacion y Comunicación • Mvars • Canales y Mensajes • Planificacion • Implementacion • Recolector de basura
Introducción • Los sistemas informáticos de hoy día hacen un uso muy exhaustivo de los recursos, por lo que la concurrencia esta a la orden del día. • Concurrent Haskell es una extensión concurrente del lenguaje Haskell convencional. Con ello se trata de dar mayor expresividad a los programas que hacen uso de un sistema de entrada salida muy sofisticado. • La mayoría de los lenguajes hace uso de los efectos laterales, en las llamadas a funciones, para conseguir el efecto deseado, por ejemplo, imprimir un carácter por pantalla no es mas q el resultado de llamar a una función que tiene como efecto lateral la impresión de dicho carácter por pantalla. • En los lenguajes imperativos esto funciona sin problemas, pero cuando nos encontramos con la evaluación perezosa si que tenemos un problema con los efectos laterales, ya que estos solo se van a producir cuando la función se evalúe y no sabemos cuando va a suceder esto. La solución a todo esto se encuentra en la utilización de monadas
Ideas Básicas • Las ideas básicas de la concurrencia en haskell son 2: • Procesos y los mecanismos necesarios para lanzarlos ForkIO () • Cambio automático de contexto, que permita la comunicación y la cooperación de los procesos
Procesos: ForkIO () • Para crear nuevas hebras en Haskell hacemos uso de la primitiva forkIO • ForkIO :: IO a -> IO ThreadId • ForkIO toma un argumento como parámetro, una acción y genera un proceso que representa dicha acción. • Al Evaluar esta expresión se crea una nueva hebra que se ejecuta concurrentemente con el proceso padre. La llamada a forkIO devuelve el identificador del proceso creado como resultado. • Las hebras pueden ser dormidas utilizando threadDelay • ThreadDelay :: Int -> IO ()
Características ForkIO () • Debido a que la implementación de ForkIO hace uso de la evaluación perezosa de haskell, es necesaria la sincronización entre procesos., ya que un proceso podría intentar evaluar una computación aplazada (thunk) que ya esta siendo evaluada por otro proceso, en cuyo caso el primero debería bloquearse y esperar a que el segundo termine. • Desde el instante en que el proceso padre genera un proceso hijo, ambos pueden modificar el estado del sistema, introduciendo indeterminismo. • ForkIO es asimétrico, la ejecución de esta primitiva en el proceso padre genera un proceso hijo que se ejecuta concurrentemente con la continuación de la ejecución del proceso padre. • El nuevo proceso no tiene nombre. De modo que no podemos hacer que ninguna operación espere a su finalización o “matar” al proceso generado
Sincronización y Comunicación • En principio es fácil pensar que es suficiente la utilización de forkIO para crear programas concurrentes con Haskell • Podemos utilizar la evaluación perezosa de listas para la comunicación entre 2 procesos • Pero veamos ahora algunas razones por las que debemos introducir nuevos mecanismos para la comunicación y sincronización entre procesos: • Los procesos pueden necesitar de la exclusión mutua a la hora de acceder a un determinado recurso, como un fichero, por lo que se hace necesario introducir los semáforos • Necesitamos de otras primitivas que permitan operar con mas de un proceso de forma in determinista • Es necesario establecer una forma conveniente de comunicación entre procesos • Para dar solución a todo lo anterior se introduce un nuevo tipo, MVar
MVar • El valor del tipo MVar t, para cualquier tipo t es el nombre de una “mutable location” (variable) que o bien puede estar vacía o contiene un valor de tipo t. • Las operaciones básicas de MVar son : • newMVar :: IO (Mvar a) Crea un nuevo MVar • takeMVar :: MVar a -> IO (a) Esta primitiva se bloquea hasta que la monada tenga un valor, despues lee dicho valor y deja la monada vacía. • putMvar :: MVar a -> a -> IO () Encapsula un valor dentro de la monada • La utilidad de Mvar queda de manifiesto con un simple ejemplo
Ejemplo MVar • Supongamos que queremos que nuestros procesos actualicen el valor de un contador • Debemos sincronizar a los distintos procesos, tanto en escritura como en lectura, para que el valor de dicho contador sea siempre correcto acceptConnections :: Config -> Socket -> IO () acceptConnections config socket = do { count <- newEmptyMVar ; putMVar count 0 ; forever (do { conn <- accept socket ; forkIO (do { inc count ; serviceConn config conn ; dec count}) }) } inc,dec :: MVar Int -> IO () inc count = do { v <- takeMVar count; putMVar count (v+1) } dec count = do { v <- takeMVar count; putMVar count (v-1) }
Ejemplo Mvar • NewEmptyMVar crea un nuevo MVar vacio • PutMVar coloca un valor dentro de un MVar vacio y TakeMVar extrae dicho valor dejando el MVar vacio • Si se pretende introducir un dato dentro de un MVar lleno, el proceso se bloquea hasta que el MVar se vacíe • Si lo que se pretendo es sacar un dato de un MVar vacio, el proceso se bloquea hasta que se introduzca un dato • Con todo esto se podría decir que Mvar actúa como un semáforo
Canales y Mensajes • Tomemos ahora el caso de varios procesos ejecutándose a la vez en una maquina • en principio el proceso padre e hijo son independientes, pero pueden estar actuando sobre un mismo recurso, por ejemplo un fichero • Necesitaríamos poder hacer que cooperasen en la escritura del fichero • Podríamos hacer que un tercer proceso sea el único que escriba en el fichero lo que los otros dos procesos le envían • Esta ultima solución necesitaría de un método de envió de mensajes. • Utilizando MVars podemos definir un canal de comunicación • type Channel a = ...given later... • newChan :: IO (Channel a) • putChan :: Channel a -> a -> IO () • getChan :: Channel a -> IO a • El Channel permitiría la escritura y lectura en el por parte de varios procesos sin problemas
Canales • Una posible implementación de los canales es utilizando dos MVars que contengan la posición de escritura y de lectura del buffer • type Channel a = (MVar (Stream a), MVar (Stream a))
Canales • Los Mvars son necesarios porque tanto al escribir como al leer, se modifican las posición de lectura y escritura • Los datos dentro del buffer se guardan en un Stream, esto es un Mvar, que o esta vacio o contiene un Item • type Stream a = MVar (Item a) • Un Item es un par formado por el primer elemento del Stream junto a un Stream que contiene el resto del dato • data Item a = MkItem a (Stream a) • La creación de un nuevo canal no seria mas que la creación de 2 Mvars, uno para lectura y otro para escritura, mas otro vacio para el Stream • newChan = do { read <- newEmptyMVar ; write <- newEmptyMVar ; hole <- newEmptyMVar ; putMVar read hole ; putMVar write hole ; return (read,write) }
Canales • Introducir nuevos datos en el canal seria tan simple como crear un nuevo Stream seguido de un hueco, extraer el hueco antiguo, reemplazarlo por el nuevo hueco e insertar en el hueco antiguo el Item • putChan (read,write) val = do { new_hole <- newEmptyMVar ; old_hole <- takeMVar write ; putMVar write new_hole ; putMVar old_hole (MkItem val new_hole) } • Obtener datos del canal es similar, teniendo en cuenta que el segundo Mvar se bloquea si el canal esta vacio • getChan (read,write) = do { head_var <- takeMVar read ; MkItem val new_head <- takeMVar head_var ; putMVar read new_head ; return val }
Planificación • La idea básica es que cada proceso se quede bloqueado en un MVar diferente y sea el programa quien decida que proceso es el que debe ejecutarse en cada momento • En Concurrent Haskell no hay un proceso que se encargue de decidir que proceso es el próximo en despertar por varias razones: • En general la elección es rara o no se una nunca • Implementar el generador de elecciones puede ser muy costoso, especialmente si las guardas contienen lecturas y escrituras • MVars proporciona indeterminismo, por lo que puede ser utilizado para aplicaciones especificas de generador de elecciones • Resumiendo, al contrario de cómo se podría pensar en principio la “selección” es costosa, se utiliza en muy raras ocasiones y limita la abstracción • Veamos como podemos como se puede vivir sin “selección”, tenemos dos maneras de hacerlo utilizando “iterated choice” o “singular choce”, esta ultima es la mas utilizada con diferencia y es la que vamos a explicar
Single Choice • Queremos hacer una única elección entre todas las disponibles, además es obligación del programador hacer que las elecciones sean abortables, esto se consigue haciendo que las alternativas tengan tipo. • Type Alternative a = Commitment a -> IO () • Type Commitment a = IO ( Maybe (a -> IO () )) • Data Maybe a= Nothing | a • Una alternativa toma una acción de entrada/salida del tipo Commiment como argumento.Este Commitment devolverá nada si otra alternativa se ha adelantado y la nuestra tiene que ser abortada o, en caso contrario, devolverá la acción que se aplica como resultado de la ejecución de la alternativa • Solo una de las alternativas recibe una respuesta al llegar al Commitment, el resto recibe nada
Single Choice • La selección podría quedar así: Select :: [Alternative a] -> IO a Select arms = newMVar >>=\result_var -> newMVar >>=\commited -> putMVar commited (Just (putMVar result_var)) >> let commit = swapMvar commited Nothing do_arm arm = forkIO (arm commit) in mapIO (do_arm commited result) arms >> takeMvar result_var
Implementación • Concurrent Haskell es una pequeña ampliación de GHC • Internamente Concurrent Haskell se ejecuta como un proceso Unix. Cada invocación de ForkIO crea un nuevo proceso son su propia pila. • El Scheduler ejecuta los procesos durante un intervalo determinado de tiempo o hasta que el proceso se bloquea • Un “thunk” (proceso retardado) es una pila con un puntero y el valor de las variables libres del proceso. • Un MVar se implementa con un puntero a una variable mutable. Dicha variable incluye un flag que indica si el estado del Mvar es vacio o lleno, junto con otro con valor igual a si mismo o a una cola de procesos bloqueados
Recolector de Basura • ¿Como podemos determinar cuando un proceso debe ser recogido por el recolector de basura? • Un proceso será eliminado por el recolector de basura si no puede generar efectos laterales • Un proceso no se recogerá si puede generar mas entradas/salidas • Un proceso bloqueado en un Mvar puede ser eliminado si ese Mvar no es accesible por otro proceso que no sea el propio recolector de basura
Bibliografia • The implementation of functional programming languages , Simon Peyton Jones, Prentice Hall, 1987 • Concurrent Haskell, Simon Peyton Jones, Andrew Gordon, Sigbjorn Finne, Conference record of POPL’96 Symposium of Principles of Programming Languages, 1996 • Tackling the Awkward Squad: monadic input/output, concurrency, exceptions, and foreign-language calls in Haskell. Simon Peyton Jones.”Engineering theories of software construction” 2001 • Using Concurrent Haskell, http://www.haskell.org/ghc/docs/5.02/set/sec-using-concurrent.html, 1999 • Composable Memory Transactions, Tim Harris, Simon Marlow, Simon Peyton Jones, and Maurice Herlihy. 2005 • Concurrency Basics, http://www.haskell.org/ghc/docs/5.02/set/sec-concurrency-basics.html 1999 • The Glorious Glasgow Haskell Compilation System User's Guide, The GHC Team. 1999 • Haskell on a Shared-Memory Multiprocessor, Tim Harris Simon Marlow Simon Peyton Jones. Sept 2005 • Control Concurrent, http://www.haskell.org/ghc/docs/latest/html/libraries/base/Control-Concurrent.html#1