Haskell concurrente extiende [1] Haskell 98 con concurrencia explícita . Sus dos principales conceptos subyacentes son:
- Un tipo primitivo que
MVar α
implementa un canal asíncrono limitado / de un solo lugar , que está vacío o tiene un valor de tipoα
. - La capacidad de generar un hilo concurrente a través de la
forkIO
primitiva.
Construido sobre esto hay una colección de abstracciones útiles de concurrencia y sincronización [2] tales como canales ilimitados , semáforos y variables de muestra.
Los subprocesos de Haskell tienen una sobrecarga muy baja: la creación, el cambio de contexto y la programación son todos internos del tiempo de ejecución de Haskell. Estos subprocesos a nivel de Haskell se asignan a un número configurable de subprocesos a nivel del sistema operativo, generalmente uno por núcleo de procesador .
Memoria transaccional de software
La extensión de memoria transaccional de software (STM) [3] para GHC reutiliza el proceso de bifurcación primitivas de Haskell concurrente. STM sin embargo:
- evita
MVar
s a favor deTVar
s. - introduce el
retry
yorElse
primitivas, lo que permite alternativas acciones atómicas que se componen juntos.
Mónada STM
La mónada STM [4] es una implementación de memoria transaccional de software en Haskell. Está implementado en GHC y permite modificar variables mutables en las transacciones .
Enfoque tradicional
Considere una aplicación bancaria como ejemplo y una transacción en ella: la función de transferencia, que toma dinero de una cuenta y lo coloca en otra cuenta. En la mónada IO, esto podría verse así:
type Account = IORef Integertransfer :: Integer -> Account -> Account -> IO () transferir monto de a = do fromVal <- readIORef from - (A) toVal <- readIORef to writeIORef from ( fromVal - amount ) writeIORef to ( toVal + amount )
Esto causa problemas en situaciones simultáneas en las que se pueden realizar varias transferencias en la misma cuenta al mismo tiempo. Si hubiera dos transferencias transfiriendo dinero desde la cuenta from
, y ambas llamadas para transferir se ejecutaron (A)
antes de que cualquiera de ellos hubiera escrito sus nuevos valores, es posible que se coloque dinero en las otras dos cuentas, y que solo se transfiera una de las cantidades. eliminado de la cuenta from
, creando así una condición de carrera . Esto dejaría la aplicación bancaria en un estado inconsistente.
Una solución tradicional a este problema es el bloqueo. Por ejemplo, se pueden colocar bloqueos alrededor de las modificaciones a una cuenta para garantizar que los créditos y débitos se produzcan de forma atómica. En Haskell, el bloqueo se logra con MVars:
type Account = MVar Enterocrédito :: Entero -> Cuenta -> IO () de crédito cantidad de cuentas = do actual <- takeMVar cuenta putMVar cuenta ( corriente + cantidad )débito :: Entero -> Cuenta -> IO () de débito cantidad de cuentas = do actual <- takeMVar cuenta putMVar cuenta ( corriente - cantidad )
El uso de dichos procedimientos garantizará que nunca se pierda ni se gane dinero debido a la intercalación incorrecta de lecturas y escrituras en una cuenta individual. Sin embargo, si uno intenta componerlos juntos para crear un procedimiento como transferencia:
transferencia :: Entero -> Cuenta -> Cuenta -> IO () de transferencia de cantidad de a = hacer débito cantidad de crédito cantidad de
todavía existe una condición de carrera: se puede debitar la primera cuenta, luego se puede suspender la ejecución del hilo, dejando las cuentas en su conjunto en un estado inconsistente. Por lo tanto, se deben agregar bloqueos adicionales para garantizar la corrección de las operaciones compuestas y, en el peor de los casos, es posible que simplemente se necesite bloquear todas las cuentas independientemente de cuántas se utilicen en una operación determinada.
Transacciones atómicas
Para evitar esto, se puede usar la mónada STM, que permite escribir transacciones atómicas. Esto significa que todas las operaciones dentro de la transacción se completan por completo, sin ningún otro hilo que modifique las variables que nuestra transacción está usando, o falla, y el estado se revierte a donde estaba antes de que comenzara la transacción. En resumen, las transacciones atómicas se completan completamente o es como si nunca se hubieran ejecutado. El código de bloqueo anterior se traduce de una manera relativamente sencilla:
type Account = TVar Enterocrédito :: Entero -> Cuenta -> STM () cantidad de crédito cuenta = hacer actual <- readTVar account writeTVar account ( actual + monto ) débito :: Entero -> Cuenta -> STM () monto de débito cuenta = hacer actual <- readTVar account writeTVar account ( actual - monto ) transferencia :: Entero -> Cuenta -> Cuenta -> STM () de transferencia de cantidad de a = hacer débito cantidad de crédito cantidad de
Los tipos de retorno de STM ()
pueden tomarse para indicar que estamos componiendo scripts para transacciones. Cuando llega el momento de ejecutar realmente una transacción de este tipo, atomically :: STM a -> IO a
se utiliza una función . La implementación anterior se asegurará de que ninguna otra transacción interfiera con las variables que está usando (desde y hacia) mientras se está ejecutando, lo que permite al desarrollador asegurarse de que no se encuentren condiciones de carrera como las anteriores. Se pueden realizar más mejoras para asegurarse de que se mantenga alguna otra " lógica comercial " en el sistema, es decir, que la transacción no debería intentar tomar dinero de una cuenta hasta que haya suficiente dinero en ella:
transferencia :: Entero -> Cuenta -> Cuenta -> STM () de transferencia de cantidad de a = hacer fromVal <- readTVar de si ( fromVal - cantidad ) > = 0 entonces hacer débito cantidad de crédito cantidad a otro reintento
Aquí retry
se ha utilizado la función, que revertirá una transacción y volverá a intentarlo. Reintentar en STM es inteligente ya que no intentará ejecutar la transacción nuevamente hasta que una de las variables a las que hace referencia durante la transacción haya sido modificada por algún otro código transaccional. Esto hace que la mónada STM sea bastante eficiente.
Un programa de ejemplo que utiliza la función de transferencia podría verse así:
módulo Principal dondeimportación Control.Concurrent ( forkIO ) importación Control.Concurrent.STM importación Control.Monad ( para siempre ) la importación System.exit ( exitSuccess )type Account = TVar Enteromain = do bob <- newAccount 10000 jill <- newAccount 4000 repeatIO 2000 $ forkIO $ atomically $ transfer 1 bob jill forever $ do bobBalance <- atomically $ readTVar bob jillBalance <- atomically $ readTVar jill putStrLn ( "Bob's balance:" ++ show bobBalance ++ ", saldo de Jill:" ++ show jillBalance ) si bobBalance == 8000 entonces exitSuccess else putStrLn "Intentando de nuevo".repeatIO :: Entero -> IO a -> IO a repeatIO 1 m = m repeatIO n m = m >> repeatIO ( n - 1 ) mnewAccount :: Integer -> Cuenta IO newAccount amount = newTVarIO amount transferencia :: Entero -> Cuenta -> Cuenta -> STM () de transferencia de cantidad de a = hacer fromVal <- readTVar de si ( fromVal - cantidad ) > = 0 entonces hacer débito cantidad de crédito cantidad a otro reintentocrédito :: Entero -> Cuenta -> STM () cantidad de crédito cuenta = hacer actual <- readTVar account writeTVar account ( actual + monto ) débito :: Entero -> Cuenta -> STM () monto de débito cuenta = hacer actual <- readTVar account writeTVar account ( actual - monto )
que debería imprimir "Saldo de Bob: 8000, saldo de Jill: 6000". Aquí, la atomically
función se ha utilizado para ejecutar acciones STM en la mónada IO.
Referencias
- ^ Simon Peyton Jones, Andrew D. Gordon y Sigbjorn Finne. Haskell concurrente . Simposio ACM SIGPLAN-SIGACT sobre principios de lenguajes de programación (PoPL). 1996. (Algunas secciones están desactualizadas con respecto a la implementación actual).
- ^ Las bibliotecas jerárquicas de Haskell , Control.Concurrent Archivado el 2 de agosto de 2012 en archive.today
- ^ Tim Harris, Simon Marlow, Simon Peyton Jones y Maurice Herlihy . Transacciones de memoria componibles . Simposio ACM sobre Principios y Práctica de la Programación Paralela 2005 (PPoPP'05). 2005.
- ^ Control.Concurrent.STM