f :: Port Int -> STM ()
f p = do { item <- readPort p
; g item}
If g goes wrong (throws an exception), the author of f might
reasonably want to ensure that the item is not read from
the port p and then discarded. And indeed, if f is called in
an atomic context, such as atomic (f p), the effects of
readPort are discarded, so that the item is not read. But
suppose f is called in a context that catches the exception
before leaving the STM world:
bad :: Port Int -> Port Int -> STM ()
bad p1 p2 = catch (f p1) (\exn -> f p2)
municate safely. An MVar is a mutable location like a TVar,
except that it may be either empty, or full with a value. The
take MVar function leaves a full MVar empty, but blocks
on an empty MVar. A putMVar on an empty MVar leaves it
full, but blocks on a full MVar. So MVars are, in effect, a one-place channel.
It is easy to implement MVars on top of TVars. An MVar
holding a value of type a can be represented by a TVar holding
a value of type Maybe a; this is a type that is either an empty
value (“Nothing”), or actually holds an a (e.g., “Just 42”).
type MVar a = TVar (Maybe a)
newEmptyMVar :: STM (MVar a)
newEmptyMVar = newTVar Nothing
The takeMVar operation reads the contents of the TVar
In our original paper we proposed that the effects of (f p1) and retries until it sees a value other than Nothing:
would be retained and be visible to the call (f p2). Furthermore, if the latter succeeds without itself throwing an exception or retrying, the effects of (f p1) would be permanently
committed.
Ultimately we felt that this treatment of effects that precede an exception seemed inconsistent. Consider the author
of f; in an effort to ensure that the item is indeed not read if
g throws an exception, he might try this:
takeMVar :: MVar a -> STM a
takeMVar mv
= do { v <- readTVar mv
; case v of
Nothing -> retry
Just val -> do { writeTVar mv Nothing
; return val}}
f :: Port Int -> STM ()
f p = do { item <- readPort p
; catch (g item)
(recover exn item)}
where
recover exn item
= do { unReadPort p item
; throw exn}
The corresponding putMVar operation retries until it sees
Nothing, at which point it updates the underlying TVar:
putMVar :: MVar a -> a -> STM ()
putMVar mv newval
= do { v <- readTVar mv
; case v of
Nothing -> writeTVar mv
(Just newval)
Just val -> retry}
But that relies on the existence of unReadPort to manually
replicate the roll-back supported by the underlying STM.
The conclusion is clear: the effects of the first argument of
catch should be reverted if the computation raises an ex- Notice how operations that return a Boolean success / fail-ception. Again, this works out nicely in the context of STM- ure result can be built directly from these blocking designs.
Haskell because the catch operation used here has an STM For instance:
type, which indicates to the programmer that the code is
transactional.
4. aPPLications anD eXamPLes
In this section we provide some examples of how composable memory transactions can be used to build higher-level concurrency abstractions. We focus on operations
that involve potentially blocking communication between
threads. Previous work has shown, many times over, how
standard shared-memory data structures can be developed
from sequential code using transactional memory operations. 8, 11
4. 1. mVars
Prior to our STM work, Concurrent Haskell provided MVars
as its primitive mechanism for allowing threads to com-
tryPutMVar :: MVar a -> a -> STM Bool
tryPutMVar mv val
= do {putMVar mv val ; return True}
‘orElse‘ return False
4. 2. multicast channels
MVars effectively provide communication channels
with a single buffered item. In this section we show how
to program buffered, multi-item, multicast channels,
in which items written to the channel (writeMChan in
the interface below) are buffered internally and received
once by each read-port created from the channel. The
full interface is: