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 ThreadId
Knowing 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
primitiveIORef
s 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 IORef
s if IORef
s updates have no inter-dependencies, otherwise use TVar
s.
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 b
However, care must be taken to avoid issuing putMVar
s (w/o prior takeMVar
s) while using the wrappers above.
→ Don't mix takeMVar
/putMVar
with the wrappers listed above.
MVar
communication primitive (cont.)MVar
s 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 IORef
s, MVar
s are not subject to reordering.
MVar
s 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 . const
Similar to context-manager syntax in Python:
logMsg msg = withLock loggingLock $ do
print =<< getPOSIXTime
putStr " | "
putStrLn msg
With MVar
s 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 a
Haskell'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 holds
There 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!
IORef
s are most lightweight tool for atomic updatesMVar
s provide synchronization at the cost of more overhead than IORef
sTVar
s provide fast transactions with costly rollbacks.…in addition to reading list from last time: