230 likes | 454 Views
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?
E N D
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? • Pure functional language needs determinism. fx = e f 3 ... f 3 = 7 • What is Monad? • an abstract data type: IO a, e.g. IO Int • a container of impure, suspended actions/computations = 7? • How to use Monad? 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 getChar :: IO Char putChar :: Char -> IO () main :: IO () main = do c <- getChar putCharc
Creating Haskell Threads • forkIO :: IO () -> IO ThreadId effect-ful computation identification of a Haskell thread must be used in an IO monad • 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. • forkOS :: IO () -> IO ThreadId • support certain kinds of foreign calls to external code. Ex: fibEuler.hs
Mutable Variable • Haskell threads communicate through Mvars (mutable variables). • MVar writes and reads occur atomically • A MVar may be empty or it may contain a value • write 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 a • newEmptyMVar :: IO (MVar a) • newMVar :: a −> IO (MVar a) • takeMVar :: MVar a −> IO a • putMVar :: MVar a −> a −> IO () • readMVar :: MVar a −> IO a • tryTakeMVar :: MVar a −> IO (Maybe a) • tryPutMVar :: MVar a −> a −> IO Bool • isEmptyMVar :: MVar a −> IO Bool • …
Example: Make A Rendezvous main :: IO() main = do aMVar <- newEmptyMVar bMVar <- newEmptyMVar doneMVar <- newEmptyMVar forkIO (threadA aMVar bMVar doneMVar) forkIO (threadB aMVar bMVar) takeMVar doneMVar ... 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.”
Message Passing Channels • unbounded FIFO channel • data Chan a • newChan :: IO (Chan a) • writeChan :: Chan a -> a -> IO () • readChan :: Chan a -> IO a • unGetChan :: Chan a -> a -> IO () • isEmptyChan :: Chan a -> IO Bool • dupChan :: 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
bal :: TVar Int 8 7 Thread 1 1 atomically (do 2v <- readTVar bal 3writeTVar bal (v+1) 4 ) Thread 2 1 atomically (do 2v <- readTVar bal 3writeTVar bal (v-3) 4 ) • 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 • Thread 1 commits • Shared bal variable is updated • Transaction log is discarded 7 8 7 4 bal transaction log of Thread 1 transaction log of Thread 2
bal :: TVar Int 5 8 Thread 1 1 atomically (do 2v <- readTVar bal 3writeTVar bal (v+1) 4 ) Thread 2 1 atomically (do 2v <- readTVar bal 3writeTVar bal (v-3) 4 ) 5 transaction log of Thread 2 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 :: TVarInt −> Int −> STM () withdraw acc n = do { bal <− readTVar acc; if bal < n then retry; writeTVar acc (bal-n) } Ex: account.hs • 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 atomically (do { withdraw a1 3 ‘orElse‘ withdraw a2 3; deposit b 3 } )
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 ArrayBlockingQueueIOe = ArrayBlockingQueueIO{ iempty :: QSem, ifull :: QSem, ilock :: MVar (), ihead :: IORefInt, itail :: IORefInt, iused :: IORefInt, ilen :: Int, ia :: IOArrayInte } data ArrayBlockingQueueSTM e = ArrayBlockingQueueSTM { shead :: TVar Int, stail :: TVar Int, sused :: TVar Int, slen :: Int, sa :: Array Int (TVar e) }
function: take takeIO :: ArrayBlockingQueueIOe -> IO e takeIOabq = do b <- waitQSem (iemptyabq) e <- withMVar (ilockabq) (\dummy -> readHeadElementIOabq True) return e takeSTM :: ArrayBlockingQueueSTMe -> IO e takeSTMabq = do me <- atomically ( readHeadElementSTMabq 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 :: ArrayBlockingQueueSTMe -> IO (Maybe e) peekSTMabq = atomically (readHeadElementSTMabq False False)
helper function: readHeadElement readHeadElementIO :: ArrayBlockingQueueIOe -> Bool -> IO e readHeadElementIOabq remove = do h <- readIORef (iheadabq) e <- readArray (iaabq) h if remove then do let len = ilenabq newh = h `mod` len u <- readIORef (iusedabq) writeIORef (iheadabq) newh writeIORef (iusedabq) (u-1) signalQSem (ifullabq) else return () return e readHeadElementSTM :: ArrayBlockingQueueSTMe -> Bool -> Bool -> STM (Maybe e) readHeadElementSTMabq remove block = do u <- readTVar (susedabq) if u == 0 then if block then retry else return Nothing else do h <- readTVar (sheadabq) let tv = saabq ! h e <- readTVartv if remove then do let len = slenabq let newh = h `mod` len writeTVar (sheadabq) $! newh writeTVar (susedabq) $! (u-1) else return () return (Just e)
A More Complex Function: pollTimeout • lock-based mechanism has no support for composing two concurrency abstractions pollTimeoutSTM :: ArrayBlockingQueueSTMe -> TimeDiff -> IO (Maybe e) pollTimeoutSTMabq timeout = do c <- startTimerIO timeout atomically ((do readTChanc return Nothing) `orElse` (do me <- readHeadElementSTMabq 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 • ...