parallelism concurrency
play

Parallelism & Concurrency Advanced functional programming - - PowerPoint PPT Presentation

Parallelism & Concurrency Advanced functional programming - Lecture 9 Trevor L. McDonell (& Wouter Swierstra) 1 Parallelism & Concurrency Parallelism vs. Concurrency Related concepts, but not the same Both give up the


  1. Parallelism & Concurrency Advanced functional programming - Lecture 9 Trevor L. McDonell (& Wouter Swierstra) 1

  2. Parallelism & Concurrency • Parallelism vs. Concurrency • Related concepts, but not the same • Both give up the strictly sequential execution model of the Von Neumann machine • Concurrent programming: • Structuring a program into different, interacting tasks • Tasks may be executed simultaneously or interleaved • In general non-deterministic • Examples: OS kernel, GUI, web server • Parallel programming: • Improving the execution speed of an application • Simultaneous use of multiple physical processing elements • Examples: Video encoder, image processing, simulation codes 2

  3. Overview Haskell provides many different tools for concurrency and parallelism: • Basic concurrency primitives (locks) • Software transactional memory (STM) • Erlang-style message passing (Cloud Haskell) • Primitives to control evaluation strategies • Data-parallel arrays • GPU programming • … 3

  4. Overview • I highly recommend Simon Marlow’s book Parallel and Concurrent Programming in Haskell , which you can read it free online: https://simonmar.github.io/pages/pcph.html 4

  5. Concurrency 5

  6. Control.Concurrent -- creating a thread forkIO :: IO () -> IO ThreadId -- managing the current thread threadDelay :: Int -> IO () -- delay in microseconds yield :: IO () myThreadId :: IO ThreadId -- managing other threads throwTo :: Exception e => ThreadId -> e -> IO () Working with threads 6

  7. forkIO :: IO () -> IO ThreadId Forking threads • Using threads forces you to use IO • Any thread can create new threads • If the main program ends, all its threads are stopped too • You can explicitly control other threads by sending them exceptions via their ThreadId (e.g. to kill the thread) 7

  8. Haskell threads Haskell threads created with forkIO are not OS threads! • These threads are very lightweight; they are created and scheduled by the GHC runtime system • If you use the threaded version of the runtime system (pass -threaded to the compiler), multiple OS threads may be used behind the scenes • GHC’s runtime is very clever: there are many options provided for configuring it and obtaining debug information 8

  9. Sharing data between threads If we fork off a thread of type IO () , how do we observe its result? • We can create explicit references to mutable memory in Haskell using IORef s to share memory between threads • Using IORef s to share data between threads is unsafe! In the sense that it can lead to race conditions and other inconsistent states • Generally, when working with threads, you have to be careful that they don’t interfere with each other 9

  10. Data.IORef newIORef :: a -> IO (IORef a) readIORef :: IORef a -> IO a writeIORef :: IORef a -> a -> IO () modifyIORef :: IORef a -> (a -> a) -> IO () Mutable variables in Haskell • A value of type IORef a is a mutable reference (pointer) to a value of type a • Because references are mutable, all operations have results in IO 10

  11. test :: Int -> IO () test n = do x <- newIORef 0 mapM_ (forkIO . loop x) [1..n] loop :: IORef Int -> Int -> IO () loop ref m = do writeIORef ref m loop ref m Example loop x 0 n <- readIORef ref when (m /= n) $ putStrLn (show m) Question: What, if anything, will the following code produce? 11

  12. Shared state concurrency Non-determinism makes it much harder to develop correct programs • Threads communicate via a shared state • Problem: inconsistent data structures, race conditions 12

  13. Shared state concurrency We require a lock (of some kind) to control access to the shared state 13

  14. :: MVar a -> a -> IO () -- wait if already full newMVar :: a -> IO (MVar a) newEmptyMVar :: IO (MVar a) takeMVar :: MVar a -> IO a -- wait if empty putMVar Control.Concurrent.MVar Synchronised mutable variables in Haskell • More flexible than IORef , and can be used to implement concurrency primitives such as locks and semaphores • An MVar may be either empty or full : a thread will block trying to read from an empty MVar , or trying to write to a full one • The runtime system manages blocked threads with some fairness guarantee 14

  15. Example: Bank account Model a bank account and operations like withdrawal, deposit, and transfer of funds between accounts. It should not be possible to observe a state where, during a transfer, money has been withdrawn from one account without yet being deposited into the target account. 15

  16. withdraw :: Int -> MVar Int -> IO Bool withdraw amount account = modifyMVar account $ \balance -> -- acquires lock if balance >= amount then return (balance - amount, True) else return (balance, False) transfer :: Int -> MVar Int -> MVar Int -> IO Bool transfer amount from to = do withdraw amount from -- inconsistent state mustn't be observable! deposit amount to Example: Bank account 16

  17. • Locks must be acquired in a fixed (global) order, otherwise there is a potential for deadlock transfer :: Int -> MVar Int -> MVar Int -> IO Bool transfer amount from to = withMVar from $ \balance_from -> withMVar to $ \balance_to -> ... Example: Bank account We need to implement transfer differently • Question: What happens if someone simultaneously tries to transfer funds in the opposite direction? 17

  18. transfer :: Int -> MVar Int -> MVar Int -> IO Bool transfer amount from to = withMVar from $ \balance_from -> withMVar to $ \balance_to -> ... Example: Bank account We need to implement transfer differently • Question: What happens if someone simultaneously tries to transfer funds in the opposite direction? • Locks must be acquired in a fixed (global) order, otherwise there is a potential for deadlock 17

  19. transfer Concurrency using locks The good: • ..? The bad: • Taking too many or too few locks • Taking the wrong locks, or in the wrong order • Difficult error recovery • Lost wake-ups and erroneous retries The ugly: • Lock’s don’t support modular programming • We had to inline the definition of withdraw and deposit into 18

  20. readTVar :: TVar a -> a -> STM () atomically :: STM a -> IO a -- run a transaction newTVar :: a -> STM (TVar a) -- STM equivalent of IORef newTVarIO :: a -> IO (TVar a) Control.Concurrent.STM :: TVar a -> STM a writeTVar Software Transactional Memory (STM) • A concurrency abstraction which takes ideas from database systems • Threads execute transactions , whose effects can be undone if necessary • atomicity: all effects of executing a transaction become visible at once • isolation: can not see the effects of other threads 19

  21. transfer :: Int -> TVar Int -> TVar Int -> IO Bool transfer amount from to = atomically $ do -- run a transaction ok <- withdraw amount from -- no locks! when ok $ deposit amount to return ok Example: Bank account, revisted • Modular concurrency! • Be optimistic: locks are pessimistic 20

  22. retry :: STM a orElse :: STM a -> STM a -> STM a Software Transactional Memory • The retry function rolls back the effects of the current transaction and restarts the atomic operation • orElse offers an alternative to immediate execution: if the first alternative leads to a retry , attempt the second These operations allow you to assemble more complex transactions 21

  23. STM is lock free An alternative to concurrent programming using lock/mutex/synchronised methods Compositional and modular concurrent programming • Software transactional memory does not use locking: deadlocks can not occur • Robust in the presence of failure or cancellation • However, large transactions can take a huge number of retries, so STM works best if transactions are small, or unlikely to interfere The concept of STM has been around since ’95 • Can it be implemented efficiently? 22

  24. STM implementation Naive implementation: a single global lock Better implementation: • Each transaction keeps a log of all of the memory accesses during a transaction (record initial value and latest update), but does not actually perform any writes yet • At the end of the transaction, validate the log: if the initial values are the same as the current values, the memory is still consistent and the transaction is committed; otherwise, it is restarted • Validation and committing must be truly atomic 23

  25. • Haskell’s type system is particularly well suited to statically check the restrictions required by STM because side effects are controlled derp = atomically $ do derp :: IO a brexit ?? -- :: STM a retry (international) side effects -- :: IO () STM in other languages Software transactional memory is supported in many languages, including C/C++, C#, Java, Perl, Python, Scala, OCaml, SmallTalk … Transactions try to commit, but roll-back and retry later if the log is no longer consistent Question: Why might this cause problems (in other languages)? 24

Recommend


More recommend