<<

A Concurrency Monad (2) Running a Concurrent Computation (1) Running a computation: Introduce a monad representing “interleavable type Output = [Char] computations”. At this stage, this amounts to little MGS 2012: FUN Lecture 5 type ThreadQueue = [] Concurrency more than a convenient way to construct threads by sequential composition. type State = (Output, ThreadQueue) Henrik Nilsson How can Threads be constructed sequentially? runCM :: CM a -> Output University of Nottingham, UK The only way is to parameterize thread prefixes runCM m = runHlp ("", []) (thread m) on the rest of the Thread. This leads directly to where continuations. runHlp s t = case dispatch s t of Left (s', t) -> runHlp s' t Right o -> o

MGS 2012: FUN Lecture 5 – p.1/40 MGS 2012: FUN Lecture 5 – p.4/40 MGS 2012: FUN Lecture 5 – p.7/40

This Lecture A Concurrency Monad (3) Running a Concurrent Computation (2)

newtype CM a = CM ((a -> Thread) -> Thread) • A concurrency monad (adapted from Dispatch on the operation of the currently Claessen (1999)) fromCM :: CM a -> ((a -> Thread) -> Thread) running Thread. Then call the scheduler. • Traditional, -based concurrent fromCM (CM x) = x dispatch :: State -> Thread programming in Haskell -> Either (State, Thread) Output • Review of issues with lock-based concurrent thread :: CM a -> Thread dispatch (o, rq) (Print t) = programming thread m = fromCM m (const End) schedule (o ++ [c], rq ++ [t]) dispatch (o, rq) (Fork t1 t2) = • Software (STM monad) instance Monad CM where schedule (o, rq ++ [t1, t2]) • Why pure and STM is return x = CM (\k -> k x) dispatch (o, rq) End = a great fit m >>= f = CM $ \k -> schedule (o, rq) fromCM m (\x -> fromCM (f x) k)

MGS 2012: FUN Lecture 5 – p.2/40 MGS 2012: FUN Lecture 5 – p.5/40 MGS 2012: FUN Lecture 5 – p.8/40

A Concurrency Monad (1) A Concurrency Monad (4) Running a Concurrent Computation (3)

Demonstration that the notion of concurrent Atomic operations: Selects next Thread to run, if any. computation can be captured by a monad, and cPrint :: Char -> CM () schedule :: State -> Either (State, Thread) interesting example of a monad. cPrint c = CM (\k -> Print c (k ())) Output A Thread represents a process: a stream of schedule (o, []) = Right o primitive atomic operations: cFork :: CM a -> CM () schedule (o, t:ts) = Left ((o, ts), t) data Thread = Print Char Thread cFork m = CM (\k -> Fork (thread m) (k ())) | Fork Thread Thread | End cEnd :: CM a cEnd = CM (\_ -> End) Note that a Thread represents the entire rest of a computation.

MGS 2012: FUN Lecture 5 – p.3/40 MGS 2012: FUN Lecture 5 – p.6/40 MGS 2012: FUN Lecture 5 – p.9/40 Example: Concurrent Processes Any Use? Example: Basic Synchronization (1) Traditional lock-based synchronization: MVars p1 :: CM () p2 :: CM () p3 :: CM () • A number of libraries and embedded used as semaphores. p1 = do p2 = do p3 = do langauges use similar ideas, e.g. module Main where cPrint 'a' cPrint '1' cFork p1 - Fudgets cPrint 'b' cPrint '2' cPrint 'A' - Yampa import Control.Concurrent ...... cFork p2 - FRP in general cPrint 'j' cPrint '0' cPrint 'B' • Studying semantics of concurrent programs. countFromTo :: Int -> Int -> IO () countFromTo m n main = print (runCM p3) • Aid for testing, debugging, and reasoning | m > n = return () about concurrent programs. Result: aAbc1Bd2e3f4g5h6i7j890 | otherwise = do Note: As it stands, the output is only made putStrLn (show m) available after all threads have terminated.) countFromTo (m+1) n

MGS 2012: FUN Lecture 5 – p.10/40 MGS 2012: FUN Lecture 5 – p.13/40 MGS 2012: FUN Lecture 5 – p.16/40

Incremental Output Concurrent Programming in Haskell Example: Basic Synchronization (2) main = do Incremental output: Primitives for concurrent programming provided start <- newEmptyMVar runCM :: CM a -> Output as operations of the IO monad (or “sin bin” :-). done <- newEmptyMVar runCM m = dispatch [] (thread m) They are in the module Control.Concurrent. forkIO $ do Excerpts: dispatch :: ThreadQueue -> Thread -> Output takeMVar start dispatch rq (Print c t) = c : schedule (rq ++ [t]) forkIO :: IO () -> IO ThreadId countFromTo 1 10 dispatch rq (Fork t1 t2) = schedule (rq ++ [t1, t2]) killThread :: ThreadId -> IO () putMVar done () dispatch rq End = schedule rq threadDelay :: Int -> IO () putStrLn "Go!" newMVar :: a -> IO (MVar a) putMVar start () schedule :: ThreadQueue -> Output newEmptyMVar :: IO (MVar a) takeMVar done schedule [] = [] putMVar :: MVar a -> a -> IO () (countFromTo 11 20) schedule (t:ts) = dispatch ts t takeMVar :: MVar a -> IO a putStrLn "Done!"

MGS 2012: FUN Lecture 5 – p.11/40 MGS 2012: FUN Lecture 5 – p.14/40 MGS 2012: FUN Lecture 5 – p.17/40

Example: Concurrent processes 2 MVars Example: Unbounded Buffer (1) module Main where p1 :: CM () p2 :: CM () p3 :: CM () • The fundamental synchronisation mechanism p1 = do p2 = do p3 = do is the MVar (“em-var”). import Control.Monad (when) cPrint 'a' cPrint '1' cFork p1 • An MVar is a “one-item box” that may be import Control.Concurrent cPrint 'b' undefined cPrint 'A' empty or full...... cFork p2 newtype Buffer a = • takeMVar putMVar cPrint 'j' cPrint '0' cPrint 'B' Reading ( ) and writing ( ) Buffer (MVar (Either [a] (Int, MVar a))) are atomic operations: main = print (runCM p3) - Writing to an empty MVar makes it full. newBuffer :: IO (Buffer a) - Writing to a full MVar blocks. newBuffer = do Result: aAbc1Bd*** Exception: Prelude.undefined - Reading from an empty MVar blocks. b <- newMVar (Left []) - Reading from a full MVar makes it empty. return (Buffer b)

MGS 2012: FUN Lecture 5 – p.12/40 MGS 2012: FUN Lecture 5 – p.15/40 MGS 2012: FUN Lecture 5 – p.18/40 Example: Unbounded Buffer (2) Example: Unbounded Buffer (5) Compositionality? (2) readBuffer :: Buffer a -> IO a readBuffer (Buffer b) = do The buffer can now be used as a channel of What about this? bc <- takeMVar b communication between a set of “writers” and a case bc of mutex <- newMVar () set of “readers”. E.g. Left (x : xs) -> do ... putMVar b (Left xs) main = do takeMVar mutex return x b <- newBuffer x1 <- readBuffer b Left [] -> do forkIO (writer b) x2 <- readBuffer b w <- newEmptyMVar forkIO (writer b) putMVar b (Right (1,w)) putMVar mutex () forkIO (reader b) takeMVar w Right (n,w) -> do forkIO (reader b) putMVar b (Right (n + 1, w)) ... takeMVar w

MGS 2012: FUN Lecture 5 – p.19/40 MGS 2012: FUN Lecture 5 – p.22/40 MGS 2012: FUN Lecture 5 – p.25/40

Example: Unbounded Buffer (3) Example: Unbounded Buffer (6) Compositionality? (3)

Suppose we would like to read from one of two Why isn’t Buffer simply defined as reader :: Buffer Int -> IO () reader n b = rLoop buffers. newtype Buffer a = Buffer [a] where That is, composing alternatives. ? rLoop = do Hmmm. How do we even begin? Hint: What would happen if e.g. an attempt is x <- readBuffer b made to read from an empty buffer? when (x > 0) $ do • No way to attempt reading a buffer without putStrLn (n ++ ": " ++ show x) risking . rLoop • We have to change or enrich the buffer implementation. E.g. add a tryReadBuffer operation, and then repeatedly poll the two buffers in a tight loop. Not so good!

MGS 2012: FUN Lecture 5 – p.20/40 MGS 2012: FUN Lecture 5 – p.23/40 MGS 2012: FUN Lecture 5 – p.26/40

Example: Unbounded Buffer (4) Compositionality? (1) Locks Are Pessimistic writeBuffer :: Buffer a -> a -> IO () writeBuffer (Buffer b) x = do Suppose we would like to read two consecutive • In practice, it is often the case that conflicts bc <- takeMVar b elements from a buffer b? that would lead to actual harm are rare. case bc of That is, sequential composition. • Lock-based synchronisation thus tends to Left xs -> limit concurrency unnecessarily, potentially putMVar b (Left (xs ++ [x])) Would the following work? harming performance in particular on parallel Right (n,w) -> do x1 <- readBuffer b hardware (such as multi-core processors). putMVar w x x2 <- readBuffer b if n > 1 then putMVar b (Right (n - 1, w)) else putMVar b (Left [])

MGS 2012: FUN Lecture 5 – p.21/40 MGS 2012: FUN Lecture 5 – p.24/40 MGS 2012: FUN Lecture 5 – p.27/40 Software Transactional Memory (1) STM and Pure Declarative Languages Example: Buffer Revisited (2)

• • STM perfect match for purely declarative Software Transactional Memory (STM) is a readBuffer :: Buffer a -> STM a new promising approach to facilitate writing languages: readBuffer (Buffer b) = do correct and performant concurrent code. - reading and writing of shared mutable xs <- readTVar b case xs of • Inspired by the notion of transactions. variables explicit and relatively rare; - most computations are pure and need not [] -> retry • (x : xs') -> do Operations on shared mutable variables be logged. grouped into transactions. writeTVar b xs' • return x • Disciplined use of effects through monads a Transactions optimistically executed huge payoff: easy to ensure that only effects concurrently. that can be undone can go inside a transaction. writeBuffer :: Buffer a -> a -> STM () • Each transaction succeeds or fails in its entirety, writeBuffer (Buffer b) x = do (Imagine the havoc arbitrary I/O actions could cause if xs <- readTVar b depending on if there actually was a problem. part of transaction: How to undo? What if retried?) writeTVar b (xs ++ [x])

MGS 2012: FUN Lecture 5 – p.28/40 MGS 2012: FUN Lecture 5 – p.31/40 MGS 2012: FUN Lecture 5 – p.34/40

Software Transactional Memory (2) The STM monad Example: Buffer Revisited (3)

• Transactions thus atomic w.r.t. other The software transactional memory abstraction The main program and code for readers and transactions. provided by a monad STM. Distinct from IO! writers can remain unchanged, except that STM • Failed transactions are automatically retried Defined in Control.Concurrent.STM. operations must be carried out atomically: until they succeed. Excerpts: main = do b <- atomically newBuffer • Transaction logs, which records reading and newTVar :: a -> STM (TVar a) forkIO (writer b) writing of shared variables, maintained to writeTVar :: TVar a -> a -> STM () forkIO (writer b) enable transactions to be validated, partial readTVar :: TVar a -> STM a forkIO (reader b) transactions to be rolled back, and to determine retry :: STM a when worth trying a transaction again. forkIO (reader b) atomically :: STM a -> IO a ... • No locks! (At the application level.)

MGS 2012: FUN Lecture 5 – p.29/40 MGS 2012: FUN Lecture 5 – p.32/40 MGS 2012: FUN Lecture 5 – p.35/40

Software Transactional Memory (3) Example: Buffer Revisited (1) Example: Buffer Revisited (4)

Let us rewrite the unbounded buffer using the • Transactional memory poised to go reader :: Buffer Int -> IO () STM monad: mainstream with the arrival of hardware reader n b = rLoop module Main where support in mainstream multi-core processors; where e.g., Intel’s upcoming (2013) Haswell import Control.Monad (when) rLoop = do architecture. import Control.Concurrent x <- atomically (readBuffer b) import Control.Concurrent.STM when (x > 0) $ do putStrLn (n ++ ": " ++ show x) newtype Buffer a = Buffer (TVar [a]) rLoop newBuffer :: STM (Buffer a) Why shouldn’t atomically be part of the newBuffer = do definition of readBuffer? b <- newTVar [] return (Buffer b)

MGS 2012: FUN Lecture 5 – p.30/40 MGS 2012: FUN Lecture 5 – p.33/40 MGS 2012: FUN Lecture 5 – p.36/40 Composition (1) Reading (2)

STM operations can be robustly composed. • Peter Bright. Transactional memory going mainstream That’s the reason for making readBuffer and with Intel Haswell. February 2012. writeBuffer STM operations, and leaving it to http://arstechnica.com/business/news/ client code to decide the scope of atomic blocks. 2012/02/transactional-memory-going- mainstream-with-intel-haswell.ars Example, sequential composition: reading two consecutive elements from a buffer b: atomically $ do x1 <- readBuffer b x2 <- readBuffer b ...

MGS 2012: FUN Lecture 5 – p.37/40 MGS 2012: FUN Lecture 5 – p.40/40

Composition (2)

Example, composing alternatives: reading from one of two buffers b1 and b2: x <- atomically $ readBuffer b1 `orElse` readBuffer b2 The buffer operations thus composes nicely. No need to change the implementation of any of the operations!

MGS 2012: FUN Lecture 5 – p.38/40

Reading (1)

• Koen Claessen. A Poor Man’s Concurrency Monad. Journal of Functional Programming, 9(3), 1999. • Wouter Swierstra and Thorsten Altenkirch. Beauty in the Beast: A Functional Semantics for the Awkward Squad. In Proceedings of Haskell'07, 2007. • Tim Harris, Simon Marlow, Simon Peyton Jones, Maurice Herlihy. Composable Memory Transactions. In Proceedings of PPoPP'05, 2005 • Simon Peyton Jones. Beautiful Concurrency. Chapter from Beautiful Code, ed. Greg Wilson, O’Reilly 2007.

MGS 2012: FUN Lecture 5 – p.39/40