Concurrent Haskell
Encyclopedia
Concurrent Haskell extends Haskell 98 with explicit concurrency. The two main concepts underpinning Concurrent Haskell are:
Built atop this is a collection of useful concurrency and synchronisation abstractions such as unbounded channels, semaphores
and sample variables.
Default Haskell threads have very low overheads: creation, context-switching and scheduling are all internal to the Haskell runtime. These Haskell-level threads are mapped onto a configurable number of OS-level threads, usually one per processor core.
(STM) extension to the Glasgow Haskell Compiler
reuses the process forking primitives of Concurrent Haskell. STM however:
in Haskell. It is implemented in the GHC compiler, and allows for mutable variables to be modified in transactions
.
An example of a transaction might be in a banking application. One function that would be needed would be a transfer function, which takes money from one account, and puts it into another account. In the IO monad, this might look like this:
This might work some of the time, but 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 the line 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
. This would leave the state of the banking application in an inconsistent state.
A traditional solution to such a problem is locking. For instance, one could place locks around modifications to an account to ensure that credits and debits occur atomically. In Haskell, locking is accomplished with MVars:
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:
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.
To avoid this, we can use the STM monad, which allows us 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 straight forward way:
The return types of
" 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:
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 and 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:
Which should print out "Bill's balance: 8000, Jill's balance: 6000". Here the atomically function has been used to run STM actions in the IO monad.
- A primitive type
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 atop this is a collection of useful concurrency and synchronisation abstractions such as unbounded channels, semaphores
Semaphore (programming)
In computer science, a semaphore is a variable or abstract data type that provides a simple but useful abstraction for controlling access by multiple processes to a common resource in a parallel programming environment....
and sample variables.
Default Haskell threads have very low overheads: creation, context-switching and scheduling are all internal to the Haskell runtime. These Haskell-level threads are mapped onto a configurable number of OS-level threads, usually one per processor core.
Software Transactional Memory
The recently introduced Software Transactional MemorySoftware transactional memory
In computer science, software transactional memory 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. A transaction in this context is a piece of code that...
(STM) extension to the Glasgow Haskell Compiler
Glasgow Haskell Compiler
The Glorious Glasgow Haskell Compilation System, more commonly known as the Glasgow Haskell Compiler or GHC, is an open source native code compiler for the functional programming language Haskell. The lead developers are Simon Peyton Jones and Simon Marlow...
reuses the process forking primitives of Concurrent Haskell. STM however:
- eschews
MVar
s in favour ofTVar
s. - introduces the
retry
andorElse
primitives, allowing alternative atomic actions to be composed together.
STM monad
The STM monad is an implementation of Software Transactional MemorySoftware transactional memory
In computer science, software transactional memory 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. A transaction in this context is a piece of code that...
in Haskell. It is implemented in the GHC compiler, and allows for mutable variables to be modified in transactions
Database transaction
A transaction comprises a unit of work performed within a database management system against a database, and treated in a coherent and reliable way independent of other transactions...
.
An example of a transaction might be in a banking application. One function that would be needed would be a transfer function, which takes money from one account, and puts it into another account. In the IO monad, this might look like this:
This might work some of the time, but 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 the line 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
Race condition
A race condition or race hazard is a flaw in an electronic system or process whereby the output or result of the process is unexpectedly and critically dependent on the sequence or timing of other events...
. This would leave the state of the banking application in an inconsistent state.
A traditional solution to such a problem is locking. For instance, one could place locks around modifications to an account to ensure that credits and debits occur atomically. In Haskell, locking is accomplished with MVars:
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:
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.
To avoid this, we can use the STM monad, which allows us 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 straight forward way:
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 LogicBusiness logic
Business logic, or domain logic, is a non-technical term generally used to describe the functional algorithms that handle information exchange between a database and a user interface.- Scope of business logic :Business logic:...
" 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:
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 and 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:
Which should print out "Bill's balance: 8000, Jill's balance: 6000". Here the atomically function has been used to run STM actions in the IO monad.