explicit concurrent programming in haskell
DESCRIPTION
Explicit Concurrent Programming in Haskell. QIAN XI COS597C 10/28/2010. Outline. Recap of IO Monad Thread Primitives Synchronization with Locks Message Passing Channels Software Transactional Memory Transactional Memory with Data Invariants. IO Monad in Haskell. Why Monad? - PowerPoint PPT PresentationTRANSCRIPT
QIAN XICOS597C 10/28/2010
Explicit Concurrent Programming in Haskell
Outline
Recap of IO Monad
Thread Primitives
Synchronization with Locks
Message Passing Channels
Software Transactional Memory
Transactional Memory with Data Invariants
IO Monad in Haskell
Why Monad? Pure functional language needs determinism.
getChar :: IO CharputChar :: Char -> IO ()
main :: IO ()main = do c <- getChar putChar c
f x = e
f 3...
f 3
What is Monad? an abstract data type: IO a, e.g. IO Int a container of impure, suspended actions/computations
How to use Monad?
= 7
= 7?
do encloses a sequence of computations:an action,a pattern bounded to the result of an action using <-a set of local definitions introduced using let
Creating Haskell Threads
forkIO :: IO () -> IO ThreadId
effect-fulcomputati
on
identification of a Haskell threadmust be used in an IO monad
forkOS :: IO () -> IO ThreadId support certain kinds of foreign calls to external code.
Concurrency is “lightweight”: both thread creation and context switching overheads are extremely low.
The parent thread will not automatically wait for the child threads to terminate.
Ex: fibEuler.hs
Mutable Variable
Haskell threads communicate through Mvars (mutable variables).
MVar writes and reads occur atomicallyA MVar may be empty or it may contain a
valuewrite to occupied MVar, read from empty
MVar: will be blocked will be rewoken when it’s empty/ a value is written and
try again wake up scheme: FIFO
MVar Operations
data MVar anewEmptyMVar :: IO (MVar a)newMVar :: a −> IO (MVar a) takeMVar :: MVar a −> IO a putMVar :: MVar a −> a −> IO ()readMVar :: MVar a −> IO atryTakeMVar :: MVar a −> IO (Maybe a)tryPutMVar :: MVar a −> a −> IO BoolisEmptyMVar :: MVar a −> IO Bool…
Example: Make A Rendezvous
module Main where import Control.Concurrent import Control.Concurrent.MVar threadA :: MVar String -> MVar String -> MVar Int -> IO() threadA valueToSendMVar valueReceiveMVar doneMVar = do putMVar valueToSendMVar "Are you going trick or treating tonight?” v <- takeMVar valueReceiveMVar putMVar doneMVar 1 threadB :: MVar String -> MVar String ->IO() threadB valueToReceiveMVar valueToSendMVar = do z <- takeMVar valueToReceiveMVar putMVar valueToSendMVar “Yes. Let’s meet at 8pm.”
main :: IO() main = do aMVar <- newEmptyMVar bMVar <- newEmptyMVar doneMVar <- newEmptyMVar forkIO (threadA aMVar bMVar doneMVar) forkIO (threadB aMVar bMVar) takeMVar doneMVar ...
Message Passing Channels
unbounded FIFO channeldata Chan anewChan :: IO (Chan a)writeChan :: Chan a -> a -> IO ()readChan :: Chan a -> IO aunGetChan :: Chan a -> a -> IO ()isEmptyChan :: Chan a -> IO BooldupChan :: Chan a -> IO (Chan a)...
Ex: chat.hs
Haskell STM
Programming with MVar can lead to deadlock one thread is waiting for a value to appear in an MVar no other thread will ever write a value to that MVar
An alternative way to synchronize: software transactional memory (STM) A special type of shared variable: TVar TVars are used only inside atomic blocks. The code inside an atomic block is executed as if it
were an atomic instruction. Functionally, no other thread is running in
parallel/interleaved. In reality, a log is used to roll back execution if
conflicts.
TVar Operations
data STM a −− A monad supporting atomic memory transactions
atomically :: STM a −> IO a −− Perform a series of STM actions atomically
data TVar a −− Shared memory locations that support atomic memory operations
newTVar :: a −> STM (TVar a) −− Create a new TVar with an initial value
readTVar :: TVar a −> STM a −− Return the current value stored in a TVar
writeTVar :: TVar a −> a −> STM () −− Write the supplied value into a TVar
7
bal :: TVar Int
Thread 11 atomically (do2 v <- readTVar bal3 writeTVar bal (v+1)4 )
What Value Read
Value Write
bal
What Value Read
Value Write
Thread 21 atomically (do2 v <- readTVar bal3 writeTVar bal (v-3)4 )
7
transaction log of Thread 2
7
transaction log of Thread 1
8 4
8
• Thread 1 commits• Shared bal variable is updated• Transaction log is discarded bal
• Attempt to commit Thread 2 fails, because value in memory is not consistent with the value in the log• Transaction re-runs from the beginning
bal :: TVar Int
8
Thread 11 atomically (do2 v <- readTVar bal3 writeTVar bal (v+1)4 )
Thread 21 atomically (do2 v <- readTVar bal3 writeTVar bal (v-3)4 )
What Value Read
Value Write
bal 8
transaction log of Thread 2
5
5
Ex: simpleSTM.hs
When To Use retry and orElse?
retry :: STM a abort the current
transaction re-execute it from the
beginning using a fresh log
withdraw :: TVar Int −> Int −> STM () withdraw acc n = do { bal <− readTVar acc; if bal < n then retry; writeTVar acc (bal-n) }
atomically (do { withdraw a1 3 ‘orElse‘ withdraw a2 3; deposit b 3 } )
orElse :: STM a -> STM a -> STM a compose two transactions if one transaction aborts
then the other transaction is executed
if it also aborts then the whole transaction is re-executed
Ex: account.hs
Case Study: ArrayBlockingQueue (Discolo et al. FLOPS 06)
from JSR-166, a java implementation of a fixed length queue
select 3 representative interfaces: take: Removes an element from the head of the queue,
blocking if the queue is empty peek: Removes an element from the head of the queue
if one is immediately available, otherwise return Nothing
pullTimeout: Retrives and removes the head of this queue, waiting up to the specified wait time if necessary for an element to become available
Data Structure
data ArrayBlockingQueueIO e = ArrayBlockingQueueIO{
iempty :: QSem, ifull :: QSem, ilock :: MVar (), ihead :: IORef Int, itail :: IORef Int, iused :: IORef Int, ilen :: Int, ia :: IOArray Int e }
data ArrayBlockingQueueSTM e = ArrayBlockingQueueSTM {
shead :: TVar Int, stail :: TVar Int, sused :: TVar Int, slen :: Int, sa :: Array Int (TVar e)}
function: take
takeIO :: ArrayBlockingQueueIO e -> IO e
takeIO abq = do b <- waitQSem (iempty abq) e <- withMVar (ilock abq) (\dummy -> readHeadElementIO abq True) return e
takeSTM :: ArrayBlockingQueueSTM e -> IO e takeSTM abq = do me <- atomically ( readHeadElementSTM abq True True) case me of Just e -> return e
function: peek
peekIO :: ArrayBlockingQueueIO e -> IO (Maybe e)
peekIO abq = do b <- tryWaitQSem (iempty abq) if b then do me <- withMVar (ilock abq) (\dummy -> do u <- readIORef (iused abq) if u == 0 then return Nothing else do e <- readHeadElementIO abq False return (Just e)) signalQSem (iempty abq) return me else return Nothing
peekSTM :: ArrayBlockingQueueSTM e -> IO (Maybe e) peekSTM abq = atomically (readHeadElementSTM abq False False)
helper function: readHeadElement
readHeadElementIO :: ArrayBlockingQueueIO e -> Bool -> IO e
readHeadElementIO abq remove= do h <- readIORef (ihead abq) e <- readArray (ia abq) h if remove then do let len = ilen abq newh = h `mod` len u <- readIORef (iused abq) writeIORef (ihead abq) newh writeIORef (iused abq) (u-1) signalQSem (ifull abq) else return () return e
readHeadElementSTM :: ArrayBlockingQueueSTM e -> Bool -> Bool -> STM (Maybe e) readHeadElementSTM abq remove block = do u <- readTVar (sused abq) if u == 0 then if block then retry else return Nothing else do h <- readTVar (shead abq) let tv = sa abq ! h e <- readTVar tv if remove then do let len = slen abq let newh = h `mod` len writeTVar (shead abq) $! newh writeTVar (sused abq) $! (u-1) else return () return (Just e)
A More Complex Function: pollTimeout
lock-based mechanism has no support for composing two concurrency abstractions
pollTimeoutSTM :: ArrayBlockingQueueSTM e -> TimeDiff -> IO (Maybe e) pollTimeoutSTM abq timeout = do c <- startTimerIO timeout atomically ((do readTChan c return Nothing) `orElse` (do me <- readHeadElementSTM abq True True return me) )
Performance Measurements(Discolo et al. FLOPS 06)
The test creates an
ArrayBlockingQueue of type integer
creates an equal number of reader and writer threads that simply loops for the specific number of iterations performing taking or put operations on the queue
completes when all threads have terminated
For each processor configuration (1-8 processors) varies only the number of
reader/writer threads
STM with Data Invariants
STM can also deal with consistency of the program
check E where E is an invariant that should be preserved by every atomic update
check :: Bool -> STM a check True = return () check False = retry
account.hs with invariants
References
“A Tutorial on Parallel and Concurrent Programming in Haskell”, Jones et al., AFP summer school notes, 2008
“Lock Free Data Structures using STM in Haskell”, Discolo et al., FLOPS 2006
http://haskell.org/haskellwiki/Haskell_for_multicores
...