Concurrent Haskell расширяет [1] Haskell 98 явным параллелизмом . Его две основные концепции:
- Примитивный тип,
MVar α
реализующий ограниченный / одноместный асинхронный канал , который либо пуст, либо содержит значение типаα
. - Возможность создавать параллельный поток через
forkIO
примитив.
Поверх этого построен набор полезных абстракций параллелизма и синхронизации [2], таких как неограниченные каналы , семафоры и выборочные переменные.
Потоки Haskell имеют очень низкие накладные расходы: создание, переключение контекста и планирование являются внутренними по отношению к среде выполнения Haskell. Эти потоки уровня Haskell отображаются на настраиваемое количество потоков уровня ОС, обычно по одному на ядро процессора .
Программная транзакционная память [ править ]
Расширение программной транзакционной памяти (STM) [3] для GHC повторно использует примитивы разветвления процессов Concurrent Haskell. Однако СТМ:
- избегает
MVar
s в пользуTVar
s. - Вводит
retry
иorElse
примитивы, позволяющие альтернативные атомные действия должны быть составлены вместе.
Монада STM [ править ]
Монада STM [4] - это реализация программной транзакционной памяти в Haskell. Он реализован в GHC и позволяет изменять изменяемые переменные в транзакциях .
Традиционный подход [ править ]
Рассмотрим в качестве примера банковское приложение и транзакцию в нем - функцию перевода, которая берет деньги с одного счета и переводит их на другой счет. В монаде ввода-вывода это может выглядеть так:
type Account = IORef Integertransfer :: Integer -> Account -> Account -> IO () переводить сумму из to = do fromVal <- readIORef from - (A) toVal <- readIORef to writeIORef from ( fromVal - amount ) writeIORef to ( toVal + amount )
Это вызывает проблемы в параллельных ситуациях, когда несколько переводов могут выполняться на одной и той же учетной записи одновременно . Если было два перевода, переводящие деньги со счета from
, и оба вызова на перевод выполнялись (A)
до того, как любой из них записал свои новые значения, возможно, что деньги будут переведены на два других счета, и только одна из переведенных сумм будет удален из учетной записи from
, что создает состояние гонки . Это оставит банковское приложение в несовместимом состоянии.
Традиционное решение такой проблемы - запирание. Например, могут быть наложены блокировки на модификации учетной записи, чтобы гарантировать, что кредиты и дебетования происходят атомарно. В Haskell блокировка выполняется с помощью MVars:
тип Account = MVar Integercredit :: Integer -> Account -> IO () кредитная сумма account = do current <- takeMVar account putMVar account ( current + amount )debit :: Integer -> Account -> IO () дебетовая сумма account = do current <- takeMVar account putMVar account ( current - amount )
Использование таких процедур гарантирует, что деньги никогда не будут потеряны или получены из-за неправильного чередования операций чтения и записи в любой индивидуальной учетной записи. Однако, если кто-то пытается составить их вместе, чтобы создать такую процедуру, как передача:
transfer :: Integer -> Account -> Account -> IO () переводить сумму из в = делать дебетовую сумму из суммы кредита в
состояние гонки все еще существует: первая учетная запись может быть списана, затем выполнение потока может быть приостановлено, в результате чего учетные записи в целом находятся в несогласованном состоянии. Таким образом, должны быть добавлены дополнительные блокировки для обеспечения правильности составных операций, а в худшем случае может потребоваться просто заблокировать все учетные записи, независимо от того, сколько из них используется в данной операции.
Атомарные транзакции [ править ]
Чтобы избежать этого, можно использовать монаду STM, которая позволяет писать атомарные транзакции. Это означает, что все операции внутри транзакции полностью завершены, без каких-либо других потоков, изменяющих переменные, которые использует наша транзакция, или происходит сбой, и состояние откатывается до того места, где оно было до того, как транзакция была начата. Короче говоря, атомарные транзакции либо завершаются полностью, либо вообще не выполняются. Приведенный выше код на основе блокировки переводится относительно просто:
тип Account = TVar Целое числоcredit :: Integer -> Account -> STM () кредитная сумма account = do current <- readTVar account writeTVar account ( current + amount )debit :: Integer -> Account -> STM () дебетовая сумма account = do current <- readTVar account writeTVar account ( current - amount )перевод :: Integer -> Account -> Account -> STM () перевод сумма от к = сделать дебетовую сумму от кредитной суммы до
Типы возвращаемых значений STM ()
могут использоваться для обозначения того, что мы составляем сценарии для транзакций. Когда приходит время фактически выполнить такую транзакцию, используется функция atomically :: STM a -> IO a
. Вышеупомянутая реализация гарантирует, что никакие другие транзакции не влияют на переменные, которые она использует (от и до) во время ее выполнения, позволяя разработчику быть уверенным, что условия гонки, подобные описанным выше, не встречаются. Можно внести дополнительные улучшения, чтобы убедиться, что в системе поддерживается какая-то другая « бизнес-логика », т.е. что транзакция не должна пытаться забрать деньги со счета, пока на нем не будет достаточно денег:
перевод :: Integer -> Account -> Account -> STM () перевод сумма от к = сделать fromVal <- readTVar от если ( fromVal - сумма ) > = 0 , то сделать дебетовую сумму от кредитной суммы к другому повторить попытку
Здесь retry
была использована функция, которая откатит транзакцию и попробует ее снова. Повторная попытка в STM является разумной тем, что она не будет пытаться снова запустить транзакцию, пока одна из переменных, на которые она ссылается во время транзакции, не будет изменена каким-либо другим транзакционным кодом. Это делает монаду STM достаточно эффективной.
Пример программы, использующей передаточную функцию, может выглядеть так:
модуль Main гдеimport Control.Concurrent ( forkIO ) import Control.Concurrent.STM import Control.Monad ( навсегда ) import System.Exit ( exitSuccess )тип Account = TVar Целое числоОсновные = сделать боб <- NewAccount 10000 Jill <- NewAccount 4000 repeatIO 2000 $ forkIO $ атомарно $ трансферт 1 боб Jill навсегда $ сделать bobBalance <- атомарно $ readTVar боб jillBalance <- атомарно $ readTVar Jill putStrLn ( "Баланс Боба:" ++ show bobBalance ++ ", баланс Джилл:" ++ show jillBalance ) если bobBalance == 8000, то exitSuccess else putStrLn " Повторная попытка ".repeatIO :: Integer -> IO a -> IO a repeatIO 1 m = m repeatIO n m = m >> repeatIO ( n - 1 ) mnewAccount :: Integer -> IO Account newAccount amount = newTVarIO amountперевод :: Integer -> Account -> Account -> STM () перевод сумма от к = сделать fromVal <- readTVar от если ( fromVal - сумма ) > = 0 , то сделать дебетовую сумму от кредитной суммы к другому повторить попыткуcredit :: Integer -> Account -> STM () кредитная сумма account = do current <- readTVar account writeTVar account ( current + amount )debit :: Integer -> Account -> STM () дебетовая сумма account = do current <- readTVar account writeTVar account ( current - amount )
который должен распечатать «Баланс Боба: 8000, баланс Джилл: 6000». Здесь atomically
функция использовалась для запуска действий STM в монаде ввода-вывода.
Ссылки [ править ]
- ^ Саймон Пейтон Джонс, Эндрю Д. Гордон и Сигбьорн Финн. Параллельный Haskell . Симпозиум ACM SIGPLAN-SIGACT по принципам языков программирования (PoPL). 1996. (Некоторые разделы устарели относительно текущей реализации.)
- ^ Haskell Иерархическая Библиотеки , Control.Concurrent архивации 2012-08-02 в Archive.today
- ^ Тим Харрис, Саймон Марлоу, Саймон Пейтон Джонс и Морис Херлихи . Составные транзакции памяти . Симпозиум ACM по принципам и практике параллельного программирования 2005 г. (PPoPP'05). 2005 г.
- ^ Control.Concurrent.STM