Haskell concurrente extiende [1] Haskell 98 con concurrencia explícita . Sus dos conceptos subyacentes principales son:
MVar α
acotado/de un solo lugar , que está vacío o contiene un valor de tipo .α
forkIO
primitivo.Sobre esto se construye una colección de abstracciones útiles de concurrencia y sincronización [2], 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 todas tareas internas del entorno de ejecución de Haskell. Estos subprocesos de nivel Haskell se asignan a una cantidad configurable de subprocesos de nivel de SO, generalmente uno por núcleo de procesador .
La extensión de memoria transaccional de software (STM) [3] de GHC reutiliza las primitivas de bifurcación de procesos de Concurrent Haskell. Sin embargo, STM:
MVar
s en favor de TVar
s.retry
y , lo que permite componer juntas acciones atómicas alternativas .orElse
La mónada STM [4] es una implementación de la memoria transaccional de software en Haskell. Está implementada en GHC y permite modificar variables mutables en las transacciones .
Consideremos como ejemplo una aplicación bancaria y una transacción en ella: la función de transferencia, que toma dinero de una cuenta y lo coloca en otra. En la mónada IO, esto podría verse así:
tipo Cuenta = IORef Entero transferencia :: Entero -> Cuenta -> Cuenta -> IO () transferencia monto de a = hacer fromVal <- readIORef desde -- (A) toVal <- readIORef a writeIORef desde ( fromVal - monto ) writeIORef a ( toVal + monto )
Esto causa problemas en situaciones concurrentes donde pueden estar realizándose múltiples transferencias en la misma cuenta al mismo tiempo. Si hubiera dos transferencias transfiriendo dinero desde account from
y ambas llamadas a transfer ejecutaran line (A)
antes de que cualquiera de ellas hubiera escrito sus nuevos valores, es posible que el dinero se ponga en las otras dos cuentas, y que solo uno de los montos que se transfieren se elimine de account from
, creando así una condición de carrera . Esto dejaría a la aplicación bancaria en un estado inconsistente.
Una solución tradicional para este problema es el bloqueo. Por ejemplo, se pueden colocar bloqueos alrededor de las modificaciones de una cuenta para garantizar que los créditos y débitos se realicen de forma automática. En Haskell, el bloqueo se logra con MVars:
tipo Cuenta = MVar Entero crédito :: Entero -> Cuenta -> IO () crédito monto cuenta = do actual <- takeMVar cuenta putMVar cuenta ( actual + monto ) débito :: Entero -> Cuenta -> IO () débito importe cuenta = do actual <- takeMVar cuenta putMVar cuenta ( actual - importe )
El uso de estos procedimientos garantizará que nunca se pierda ni se gane dinero debido a una intercalación incorrecta de lecturas y escrituras en una cuenta individual. Sin embargo, si uno intenta combinarlas para crear un procedimiento como la transferencia:
transferencia :: Entero -> Cuenta -> Cuenta -> IO () importe de transferencia de a = debitar importe de abonar importe a
Todavía existe una condición de carrera: se puede debitar la primera cuenta y luego suspender la ejecución del hilo, lo que deja a 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 sea necesario simplemente bloquear todas las cuentas independientemente de cuántas se utilicen en una operación determinada.
Para evitar esto, se puede utilizar la mónada STM, que permite escribir transacciones atómicas. Esto significa que todas las operaciones dentro de la transacción se completan completamente, sin que ningún otro hilo modifique las variables que utiliza nuestra transacción, o falla y el estado se revierte al estado en el que se encontraba antes de que se iniciara la transacción. En resumen, las transacciones atómicas o se completan completamente o es como si nunca se hubieran ejecutado. El código basado en bloqueos anterior se traduce de una manera relativamente sencilla:
tipo Cuenta = TVar Entero crédito :: Entero -> Cuenta -> STM () crédito importe cuenta = do corriente <- readTVar cuenta writeTVar cuenta ( corriente + importe ) débito :: Entero -> Cuenta -> STM () débito importe cuenta = do actual <- readTVar cuenta writeTVar cuenta ( actual - importe ) transferencia :: Entero -> Cuenta -> Cuenta -> STM () importe de la transferencia de a = debitar importe de acreditar importe a
Los tipos de retorno de STM ()
pueden utilizarse 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á utilizando (from y to) mientras se está ejecutando, lo que permite al desarrollador estar seguro de que no se encontrarán condiciones de carrera como la anterior. Se pueden realizar más mejoras para asegurarse de que se mantenga otra " lógica empresarial " en el sistema, es decir, que la transacción no intente sacar dinero de una cuenta hasta que haya suficiente dinero en ella:
transferencia :: Entero -> Cuenta -> Cuenta -> STM () transferir monto de a = hacer fromVal <- readTVar desde si ( fromVal - monto ) >= 0 entonces debitar monto de acreditar monto a de lo contrario reintentar
Aquí se ha utilizado la retry
función que revertirá una transacción y la intentará nuevamente. Reintentar en STM es inteligente porque 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 donde importar Control.Concurrent ( forkIO ) importar Control.Concurrent.STM importar Control.Monad ( forever ) importar System.Exit ( exitSuccess ) tipo Cuenta = TVar Entero main = do bob <- newAccount 10000 jill <- newAccount 4000 repeatIO 2000 $ forkIO $ atómicamente $ transfer 1 bob jill forever $ do bobBalance <- atómicamente $ readTVar bob jillBalance <- atómicamente $ readTVar jill putStrLn ( "Saldo de Bob: " ++ show bobBalance ++ ", saldo de Jill: " ++ show jillBalance ) si bobBalance == 8000 entonces exitSuccess de lo contrario putStrLn "Intentando de nuevo." repeatIO :: Entero -> IO a -> IO a repeatIO 1 m = m repeatIO n m = m >> repeatIO ( n - 1 ) m newAccount :: Entero -> Cuenta IO monto de newAccount = monto de newTVarIO transferencia :: Entero -> Cuenta -> Cuenta -> STM () transferir monto de a = hacer fromVal <- readTVar desde si ( fromVal - monto ) >= 0 entonces debitar monto de acreditar monto a de lo contrario reintentar crédito :: Entero -> Cuenta -> STM () crédito importe cuenta = do corriente <- readTVar cuenta writeTVar cuenta ( corriente + importe ) débito :: Entero -> Cuenta -> STM () débito importe cuenta = do actual <- readTVar cuenta writeTVar cuenta ( actual - importe )
que debería imprimir "Saldo de Bob: 8000, saldo de Jill: 6000". Aquí atomically
se ha utilizado la función para ejecutar acciones STM en la mónada IO.