Concurrent Haskell (also Control.Concurrent, or Concurrent and Parallel Haskell) is an extension to the
functional programming language
A programming language is a system of notation for writing computer programs.
Programming languages are described in terms of their Syntax (programming languages), syntax (form) and semantics (computer science), semantics (meaning), usually def ...
Haskell
Haskell () is a general-purpose, statically typed, purely functional programming language with type inference and lazy evaluation. Designed for teaching, research, and industrial applications, Haskell pioneered several programming language ...
, which adds explicit
primitive data type
In computer science, primitive data types are a set of basic data types from which all other data types are constructed. Specifically it often refers to the limited set of data representations in use by a particular processor, which all compiled ...
s for
concurrency. It was first added to
Haskell 98, and has since become a
library
A library is a collection of Book, books, and possibly other Document, materials and Media (communication), media, that is accessible for use by its members and members of allied institutions. Libraries provide physical (hard copies) or electron ...
named
Control.Concurrent
included as part of the
Glasgow Haskell Compiler
The Glasgow Haskell Compiler (GHC) is a native or machine code compiler for the functional programming language Haskell.
It provides a cross-platform software environment for writing and testing Haskell code and supports many extensions, libra ...
.
Its two main underlying concepts are:
* A
primitive data type
In computer science, primitive data types are a set of basic data types from which all other data types are constructed. Specifically it often refers to the limited set of data representations in use by a particular processor, which all compiled ...
MVar α
implementing a bounded/single-place
asynchronous channel, which is either empty or holds a value of type
α
.
* The ability to spawn a concurrent
thread via the
forkIO
primitive.
Built on this is a set of useful concurrency and
synchronizing abstractions
Abstraction is a process where general rules and concepts are derived from the use and classifying of specific examples, literal ( real or concrete) signifiers, first principles, or other methods.
"An abstraction" is the outcome of this process ...
such as
unbounded channels,
semaphores and sample
variables.
Haskell threads have very low overhead: creating, context-switching, and scheduling are all internal to the Haskell
runtime system
In computer programming, a runtime system or runtime environment is a sub-system that exists in the computer where a program is created, as well as in the computers where the program is intended to be run. The name comes from the compile time ...
. These Haskell-level threads are mapped onto a configurable number of
operating system
An operating system (OS) is system software that manages computer hardware and software resources, and provides common daemon (computing), services for computer programs.
Time-sharing operating systems scheduler (computing), schedule tasks for ...
(OS) level threads, usually one per
processor core
A central processing unit (CPU), also called a central processor, main processor, or just processor, is the primary Processor (computing), processor in a given computer. Its electronic circuitry executes Instruction (computing), instructions ...
.
Software transactional memory
The
software transactional memory
In computer science, software transactional memory (STM) is a concurrency control mechanism analogous to database transactions for controlling access to shared memory in concurrent computing. It is an alternative to lock-based synchronization. ST ...
(STM) extension to
Glasgow Haskell Compiler
The Glasgow Haskell Compiler (GHC) is a native or machine code compiler for the functional programming language Haskell.
It provides a cross-platform software environment for writing and testing Haskell code and supports many extensions, libra ...
(GHC) reuses the process forking primitives of Concurrent Haskell. STM however:
* avoids
MVar
in favour of
TVar
.
* introduces the
retry
and
orElse
primitives, allowing alternative
atomic actions to be ''
composed'' together.
STM monad
The STM
monadControl.Concurrent.STM
/ref> is an implementation of software transactional memory in Haskell. It is implemented in GHC, and allows for mutable variables to be modified in transactions.
Traditional approach
Consider a banking application as an example, and a transaction in it—the transfer function, which takes money from one account, and puts it into another account. In the IO monad, this might look like:
type Account = IORef Integer
transfer :: Integer -> Account -> Account -> IO ()
transfer amount from to = do
fromVal <- readIORef from -- (A)
toVal <- readIORef to
writeIORef from (fromVal - amount)
writeIORef to (toVal + amount)
This causes problems in concurrent situations where multiple transfers might be taking place on the ''same'' account at the ''same'' time. If there were two transfers transferring money from account from
, and both calls to transfer ran line (A)
before either of them had written their new values, it is possible that money would be put into the other two accounts, with only one of the amounts being transferred being removed from account from
, thus creating a race condition
A race condition or race hazard is the condition of an electronics, software, or other system where the system's substantive behavior is dependent on the sequence or timing of other uncontrollable events, leading to unexpected or inconsistent ...
. This would leave the banking application in an inconsistent state.
A traditional solution to such a problem is locking. For instance, locks can be placed around modifications to an account to ensure that credits and debits occur atomically. In Haskell, locking is accomplished with MVars:
type Account = MVar Integer
credit :: Integer -> Account -> IO ()
credit amount account = do
current <- takeMVar account
putMVar account (current + amount)
debit :: Integer -> Account -> IO ()
debit amount account = do
current <- takeMVar account
putMVar account (current - amount)
Using such procedures will ensure that money will never be lost or gained due to improper interleaving of reads and writes to any individual account. However, if one tries to compose them together to create a procedure like transfer:
transfer :: Integer -> Account -> Account -> IO ()
transfer amount from to = do
debit amount from
credit amount to
a race condition still exists: the first account may be debited, then execution of the thread may be suspended, leaving the accounts as a whole in an inconsistent state. Thus, additional locks must be added to ensure correctness of composite operations, and in the worst case, one might need to simply lock all accounts regardless of how many are used in a given operation.
Atomic transactions
To avoid this, one can use the STM monad, which allows one to write atomic transactions. This means that all operations inside the transaction fully complete, without any other threads modifying the variables that our transaction is using, or it fails, and the state is rolled back to where it was before the transaction was begun. In short, atomic transactions either complete fully, or it is as if they were never run at all.
The lock-based code above translates in a relatively straightforward way:
type Account = TVar Integer
credit :: Integer -> Account -> STM ()
credit amount account = do
current <- readTVar account
writeTVar account (current + amount)
debit :: Integer -> Account -> STM ()
debit amount account = do
current <- readTVar account
writeTVar account (current - amount)
transfer :: Integer -> Account -> Account -> STM ()
transfer amount from to = do
debit amount from
credit amount to
The return types of STM ()
may be taken to indicate that we are composing scripts for transactions. When the time comes to actually execute such a transaction, a function atomically :: STM a -> IO a
is used. The above implementation will make sure that no other transactions interfere with the variables it is using (from and to) while it is executing, allowing the developer to be sure that race conditions like that above are not encountered. More improvements can be made to make sure that some other "business logic
In computer software, business logic or domain logic is the part of the program that encodes the real-world business rules that determine how data can be created, stored, and changed. It is contrasted with the remainder of the software that might ...
" is maintained in the system, i.e. that the transaction should not try to take money from an account until there is enough money in it:
transfer :: Integer -> Account -> Account -> STM ()
transfer amount from to = do
fromVal <- readTVar from
if (fromVal - amount) >= 0
then do
debit amount from
credit amount to
else retry
Here the retry
function has been used, which will roll back a transaction, and try it again. Retrying in STM is smart in that it won't try to run the transaction again until one of the variables it references during the transaction has been modified by some other transactional code. This makes the STM monad quite efficient.
An example program using the transfer function might look like this:
module Main where
import Control.Concurrent (forkIO)
import Control.Concurrent.STM
import Control.Monad (forever)
import System.Exit (exitSuccess)
type Account = TVar Integer
main = 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 ++ ", Jill's balance: " ++ show jillBalance)
if bobBalance 8000
then exitSuccess
else putStrLn "Trying again."
repeatIO :: Integer -> IO a -> IO a
repeatIO 1 m = m
repeatIO n m = m >> repeatIO (n - 1) m
newAccount :: Integer -> IO Account
newAccount amount = newTVarIO amount
transfer :: Integer -> Account -> Account -> STM ()
transfer amount from to = do
fromVal <- readTVar from
if (fromVal - amount) >= 0
then do
debit amount from
credit amount to
else retry
credit :: Integer -> Account -> STM ()
credit amount account = do
current <- readTVar account
writeTVar account (current + amount)
debit :: Integer -> Account -> STM ()
debit amount account = do
current <- readTVar account
writeTVar account (current - amount)
which should print out "Bob's balance: 8000, Jill's balance: 6000". Here the atomically
function has been used to run STM actions in the IO monad.
References
External links
*
{{Haskell programming
Programming languages
Functional languages
Statically typed programming languages
Haskell programming language family
Free software programmed in Haskell
Cross-platform free software
Free and open source compilers
Programming languages created in 1996
1996 software
Articles with example Haskell code