2013-05-27
So far we've only discussed independent tasks which didn't interact with each other
Remember forkIO's type signature:
forkIO :: IO () -> IO ThreadIdKnowing the ThreadId does not even provide a way to wait on thread completion!
(but you can throwTo an exception to a ThreadId)
IORef: mutable lock-free variables supporting atomic updates
MVar: synchronized mutable Maybe-like container
TVar: software transactional memory (STM) variable
IORef primitiveIORefs are similiar to pointers:
data IORef a
instance Eq (IORef a) -- "pointer equality"
newIORef :: a -> IO (IORef a)
readIORef :: IORef a -> IO a
writeIORef :: IORef a -> a -> IO ()
modifyIORef :: IORef a -> (a -> a) -> IO ()
modifyIORef' :: IORef a -> (a -> a) -> IO ()All operations above are lock-free, i.e. no deadlocks possible.
IORef primitiveImportant for use in multi-threaded programs
atomicModifyIORef' :: IORef a -> (a -> (a, b)) -> IO b
atomicWriteIORef :: IORef a -> a -> IO ()The operations above guarantee consistent write-ordering as seen from other threads (→ details):
Safe to use IORefs if IORefs updates have no inter-dependencies, otherwise use TVars.
MVar communication primitiveSomewhat like a mutable & synchronized Maybe container:
data MVar a
newEmptyMVar :: IO (MVar a)
newMVar :: a -> IO (MVar a)
takeMVar :: MVar a -> IO a
putMVar :: MVar a -> a -> IO ()takeMVar takes the MVar's item (but blocks if MVar empty)putMVar puts an item into the MVar (but blocks if MVar full)
MVar communication primitive (cont.)Instead of using takeMVar and putMVar directly, there's are useful wrappers which combine takeMVar/putMVar transactions in an atomic (or rather exception-safe) way, e.g.:
readMVar :: MVar a -> IO a
swapMVar :: MVar a -> a -> IO a
withMVar :: MVar a -> (a -> IO b) -> IO b
modifyMVar :: MVar a -> (a -> IO (a, b)) -> IO bHowever, care must be taken to avoid issuing putMVars (w/o prior takeMVars) while using the wrappers above.
→ Don't mix takeMVar/putMVar with the wrappers listed above.
MVar communication primitive (cont.)MVars are a versatile primitive that can be used in different ways:
takeMVar and putMVar as receive and send, andMVar (), with takeMVar and putMVar as wait and signal.As opposed to IORefs, MVars are not subject to reordering.
MVars can be used for emulating scoped locks (c.f. binary semaphores):
type Lock = MVar ()
newLock :: IO Lock
newLock = newMVar ()
withLock :: Lock -> IO a -> IO a
withLock x = withMVar x . constSimilar to context-manager syntax in Python:
logMsg msg = withLock loggingLock $ do
print =<< getPOSIXTime
putStr " | "
putStrLn msgWith MVars we can now wait on thread completion:
main = do
done <- newEmptyMVar
forkIO $ do
-- ...do stuff...
putMVar done ()
-- ...do other stuff...
() <- takeMVar done -- blocks until threads completes
return ()() we could also communicate back a result value.Due to being low-level, explicit locking allows for many potential programming errors:
Error handling/rollback in exception handlers requires more attention w.r.t. locks.
Programming with explicit locks is hard
Motivating example in pseudo-code:
class Account{
double balance;
synchronized void deposit(double amount) {
// do some other stuff that may throw exception
balance += amount;
}
synchronized void withdraw(double amount) {
if (balance < amount)
throw new OutOfMoneyError();
balance -= amount;
}
}synchronized enforces that at all times, at most one invocation of withdraw() or deposit() in progress per Account instance.
Let's add a xfer() method:
class Account{
/* */
synchronized void deposit(double amount) { /* */ }
synchronized void withdraw(double amount) { /* */ }
synchronized void xfer(Account other, double amount) {
other.withdraw(amount);
this.deposit(amount);
}
}How many potential issues can you spot?
The previous example would be much easier to get right in SQL thanks to transactions.
Idea for Haskell: provide atomic blocks with all-or-nothing semantics and TVars
atomically :: STM a -> IO a
data TVar a
newTVar :: a -> STM (TVar a)
readTVar :: TVar a -> STM aHaskell's STM provides atomicity and isolation.
STM has been available for Haskell since GHC 6.4.1 (i.e. since 2005)!
Simple example:
withdraw :: TVar Double -> Double -> STM ()
withdraw acc amt = do
bal <- readTVar acc
unless (bal >= amt) $
throwSTM OutOfMoneyError
writeTVar acc (bal-n)
deposit :: TVar Double -> Double -> STM ()
deposit acc amt = modifyTVar acc (+amt)And now we can safely construct a proper xfer implementation:
xfer :: TVar Double -> Double -> TVar Double -> STM ()
xfer srcAcc amt dstAcc = withdraw srcAcc amt >>
deposit dstAcc amt
main :: IO ()
main = do
acc1 <- newTVarIO 900.00 -- 'newTVarIO' similiar to
acc2 <- newTVarIO 123.45 -- 'atomically . newTVar'
atomic $ do -- same as 'xfer acc1 500.00 acc2'
withdraw acc1 500.00
deposit acc2 500.00
-- acc1 = 400.00 and acc2 = 623.45
-- the invariant acc1+acc2 == 1023.45 holds
Left _ <- try $ atomic $ xfer acc1 500.00 acc2 -- will fail
-- the invariant acc1+acc2 == 1023.45 holdsThere are other noteworthy STM primitives:
retry :: STM a
orElse :: STM a -> STM a -> STM a
always :: STM Bool -> STM ()Warning: retry can be used to construct deadlocks!
This was just a brief overview of STM, but there's plenty of literature!
IORefs are most lightweight tool for atomic updatesMVars provide synchronization at the cost of more overhead than IORefsTVars provide fast transactions with costly rollbacks.…in addition to reading list from last time: