Concurrent Haskell расширяет [1] Haskell 98 явным параллелизмом . Его две основные концепции:
MVar α
реализующий ограниченный/одноместный асинхронный канал , который либо пуст, либо содержит значение типа α
.forkIO
примитив.На его основе построен набор полезных абстракций параллелизма и синхронизации [2], таких как неограниченные каналы , семафоры и переменные выборки.
Потоки Haskell имеют очень низкие накладные расходы: создание, переключение контекста и планирование являются внутренними для среды выполнения Haskell. Эти потоки уровня Haskell отображаются на настраиваемое количество потоков уровня ОС, обычно по одному на ядро процессора .
Расширение программной транзакционной памяти (STM) [3] для GHC повторно использует примитивы разветвления процесса Concurrent Haskell. Однако STM:
MVar
s в пользу TVar
s.retry
и , позволяющие объединять альтернативные orElse
атомарные действия .Монада STM [4] — это реализация программной транзакционной памяти в Haskell. Она реализована в GHC и позволяет изменять изменяемые переменные в транзакциях .
Рассмотрим банковское приложение в качестве примера и транзакцию в нем — функцию перевода, которая берет деньги с одного счета и кладет их на другой счет. В монаде IO это может выглядеть так:
Тип учетной записи = IORef Integer transfer :: Integer -> Account -> Account -> IO () перевести сумму из в = do fromVal <- readIORef из -- (A) toVal <- readIORef в writeIORef из ( fromVal - сумма ) writeIORef в ( toVal + сумма )
Это вызывает проблемы в параллельных ситуациях, когда несколько переводов могут происходить на одном и том же счете в одно и то же время. Если было два перевода, переводящих деньги со счета from
, и оба вызова перевода выполнялись (A)
до того, как любой из них записал свои новые значения, возможно, что деньги будут зачислены на два других счета, и только одна из переведенных сумм будет удалена со счета from
, тем самым создавая состояние гонки . Это оставит банковское приложение в несогласованном состоянии.
Традиционным решением такой проблемы является блокировка. Например, блокировки могут быть установлены вокруг изменений в счете, чтобы гарантировать, что кредиты и дебеты происходят атомарно. В Haskell блокировка выполняется с помощью MVars:
Тип учетной записи = MVar Integer кредит :: Целое число -> Счет -> IO () сумма кредита счет = сделать текущий <- takeMVar счет putMVar счет ( текущий + сумма ) дебет :: Целое число -> Счет -> IO () дебет сумма счет = сделать текущий <- takeMVar счет putMVar счет ( текущий - сумма )
Использование таких процедур гарантирует, что деньги никогда не будут потеряны или получены из-за неправильного чередования чтения и записи на любой отдельный счет. Однако, если попытаться объединить их вместе, чтобы создать процедуру, подобную передаче:
перевод :: Целое число -> Счет - > Счет -> IO () перевод суммы с на = дебетовая сумма с кредитовая сумма на
состояние гонки все еще существует: первый счет может быть дебетован, затем выполнение потока может быть приостановлено, оставляя счета в целом в несогласованном состоянии. Таким образом, необходимо добавить дополнительные блокировки для обеспечения корректности составных операций, а в худшем случае может потребоваться просто заблокировать все счета независимо от того, сколько из них используются в данной операции.
Чтобы избежать этого, можно использовать монаду STM, которая позволяет писать атомарные транзакции. Это означает, что все операции внутри транзакции полностью завершаются, без каких-либо других потоков, изменяющих переменные, которые использует наша транзакция, или она терпит неудачу, и состояние откатывается к тому, где оно было до начала транзакции. Короче говоря, атомарные транзакции либо завершаются полностью, либо выглядят так, как будто они вообще не запускались. Код на основе блокировки выше транслируется относительно простым способом:
Тип учетной записи = TVar Integer кредит :: Целое число -> Счет -> STM () сумма кредита счет = сделать текущий <- readTVar счет writeTVar счет ( текущий + сумма ) дебет :: Целое число -> Счет -> STM () дебет сумма счет = сделать текущий <- readTVar счет writeTVar счет ( текущий - сумма ) перевод :: Целое число -> Счет - > Счет -> STM () сумма перевода с на = дебетовая сумма с кредитовая сумма на
Типы возвращаемых значений STM ()
могут быть использованы для указания того, что мы составляем скрипты для транзакций. Когда приходит время фактически выполнить такую транзакцию, atomically :: STM a -> IO a
используется функция. Вышеуказанная реализация гарантирует, что никакие другие транзакции не будут мешать переменным, которые она использует (from и to), во время ее выполнения, позволяя разработчику быть уверенным, что условия гонки, подобные приведенным выше, не возникнут. Можно внести дополнительные улучшения, чтобы убедиться, что в системе поддерживается некоторая другая « бизнес-логика », то есть транзакция не должна пытаться снять деньги со счета, пока на нем не будет достаточно денег:
transfer :: Integer -> Account -> Account -> STM () перевести сумму с на = do fromVal <- readTVar from если ( fromVal - amount ) >= 0 тогда do списать сумму с кредита сумму на иначе повторить попытку
Здесь retry
была использована функция, которая откатит транзакцию и попробует ее снова. Повтор в STM разумен тем, что не будет пытаться запустить транзакцию снова, пока одна из переменных, на которые она ссылается во время транзакции, не будет изменена каким-либо другим транзакционным кодом. Это делает монаду STM довольно эффективной.
Пример программы, использующей передаточную функцию, может выглядеть так:
модуль Главный где импорт Control.Concurrent ( forkIO ) импорт Control.Concurrent.STM импорт Control.Monad ( forever ) импорт System.Exit ( exitSuccess ) Тип учетной записи = TVar Integer main = do bob <- newAccount 10000 jill <- newAccount 4000 repeatIO 2000 $ forkIO $ атомарно $ transfer 1 bob jill навсегда $ do bobBalance <- атомарно $ readTVar bob jillBalance <- атомарно $ readTVar jill putStrLn ( "Баланс Боба: " ++ show bobBalance ++ ", Баланс Джилл: " ++ show jillBalance ) if bobBalance == 8000 then exitSuccess else putStrLn "Повторная попытка." repeatIO :: Целое число -> IO a -> IO a repeatIO 1 m = m repeatIO n m = m >> repeatIO ( n - 1 ) m newAccount :: Integer -> IO Account newAccount сумма = newTVarIO сумма transfer :: Integer -> Account -> Account -> STM () перевести сумму с на = do fromVal <- readTVar from если ( fromVal - amount ) >= 0 тогда do списать сумму с кредита сумму на иначе повторить попытку кредит :: Целое число -> Счет -> STM () сумма кредита счет = сделать текущий <- readTVar счет writeTVar счет ( текущий + сумма ) дебет :: Целое число -> Счет -> STM () дебет сумма счет = сделать текущий <- readTVar счет writeTVar счет ( текущий - сумма )
который должен вывести "Баланс Боба: 8000, баланс Джилл: 6000". Здесь функция atomically
была использована для запуска действий STM в монаде IO.