The idea is to provide a retry operation to indicate that
the current atomic action is not yet ready to run to completion. Here is the code for getR:
getR :: Resource -> Int -> STM ()
getR r i = do { v <- readTVar r
; if (v < i) then retry
else writeTVar r (v - i)}
It reads the value v of the resource and, if v >= i, decreases
it by i. If v < i, there is insufficient resource in the variable,
in which case it calls retry. Conceptually, retry aborts
the transaction with no effect, and restarts it at the beginning. However, there is no point in actually re-executing the
transaction until at least one of the TVars read during the attempted transaction has been written by another thread. Happily, the transaction log (which is needed anyway) already
records exactly which TVars were read. The implementation, therefore, blocks the thread until at least one of these
is updated. Notice that retry’s type (STM a) allows it to be
used wherever an STM action may occur.
Unlike the validation check, which is automatic and implicit, retry is called explicitly by the programmer. It does
not indicate anything bad or unexpected; rather, it shows up
when some kind of blocking would take place in other approaches to concurrency.
Notice that there is no need for the putR operation to remember to signal any condition variables. Simply by writing
to the TVars involved, the producer will wake up the consumer. A whole class of lost-wake-up bugs is thereby eliminated.
From an efficiency point of view, it makes sense to call
retry as early as possible, and to refrain from reading unrelated locations until after the test succeeds. Nevertheless,
the programming interface is delightfully simple, and easy
to reason about.
3. 3. sequential composition
By using atomic, the programmer identifies atomic transactions, in the classic sense that the entire set of operations
that it contains appears to take place indivisibly. This is the
key to sequential composition for concurrency abstractions.
For example, to grab three units of one resource and seven of
another, a thread can say
atomic (do {getR r1 3; getR r2 7})
The standard do {. . ; . .} notation combines the STM actions
from the two getR calls and the underlying transactional
memory commits their updates as a single atomic I/O action.
The retry function is central to making transactions
composable when they may block. The transaction above
will block if either r1 or r2 has insufficient resource: there
is no need for the caller to know how getR is implemented,
or what condition guarantees its success. Nor is there any
risk of deadlock by awaiting r2 while holding r1.
This ability to compose STM actions is why we did not
define getR as an I/O action, wrapped in a call to atomic.
By leaving it as an STM action, we allow the programmer to
compose it with other STM actions before finally sealing it
into a transaction with atomic. In a lock-based setting, one
would worry about crucial locks being released between the
two calls, and about deadlock if another thread grabbed the
resources in the opposite order, but there are no such concerns here.
The STM type on an atomic action provides a strong guarantee: the only way the action can be executed is for it to be
passed to atomic. Any STM action can be robustly composed
with other STM actions: the resulting sequence will still execute atomically.
3. 4. composing alternatives
We have discussed composing transactions in sequence, so
that both are executed. STM-Haskell also allows us to compose transactions as alternatives, so that only one is executed.
For example, to get either 3 units from r1 or 7 units from r2:
atomic (getR r1 3 ‘orElse‘ getR r2 7)
The orElse function is provided by the STM module
(Figure 1); here, it is written infix, by enclosing it in back-quotes, but it is a perfectly ordinary function of two arguments. The transaction s1 ‘orElse‘ s2 first runs s1; if s1
calls retry, then s1 is abandoned with no effect, and s2
is run. If s2 also calls retry then the entire call retries—
but it waits on the variables read by either of the two nested
transactions (i.e., on the union of two variable sets). Again,
the programmer needs know nothing about the enabling
conditions of s1 and s2.
Using orElse provides an elegant way for library implementers to defer to their caller the question of whether or
not to block. For instance, it is straightforward to convert the
blocking version of getR into one which returns a Boolean
success or failure result:
nonBlockGetR :: Resource -> Int
-> STM Bool
nonBlockGetR r i =
do {getR r i ; return True}
‘orElse‘ return False
If getR completes normally, nonBlockGetR will return
True; on the other hand, if getR blocks (i.e., retries), the
orElse will try its second alternative, which succeeds immediately, returning False. Notice that this idiom depends
on the left-biased nature of orElse. The same kind of construction can be also used to build a blocking operation from
one that returns a Boolean result: simply invoke retry on
receiving a False result:
blockGetR :: Resource -> Int -> STM ()
blockGetR r i =
do {s <- nonBlockGetR r i;
if s then return () else retry}