Concurrencia con Haskell

Download Report

Transcript Concurrencia con Haskell

Concurrencia con Haskell
Antonio Francisco Burdallo Berrocal
1
Índice
Introduccion
Ideas Basicas
Procesos: forkIO()
Sincronizacion y Comunicación
Mvars
Canales y Mensajes
Planificacion
Implementacion
Recolector de basura
2
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
3
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
4
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 ()
5
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
6
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
7
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
8
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) }
9
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
10
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
11
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))
12
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) }
13
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 }
14
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
15
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
16
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
17
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
18
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
19
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
20