One of the major advantages of software transactional memory that always gets mentioned is composability and modularity. Different fragments can be combined to produce larger components. In lock-based programs, this is often not the case.
I am looking for a simple example illustrating this with actual code. I'd prefer an example in Clojure, but Haskell is fine too. Bonus points if the example also exhibits some lock-based code which can't be composed easily.
An example where locks don't compose in Java:
class Account{
float balance;
synchronized void deposit(float amt){
balance += amt;
}
synchronized void withdraw(float amt){
if(balance < amt)
throw new OutOfMoneyError();
balance -= amt;
}
synchronized void transfer(Account other, float amt){
other.withdraw(amt);
this.deposit(amt);
}
}
So, deposit is okay, withdraw is okay, but transfer is not okay: if A begins a transfer to B when B begins a transfer to A, we can have a deadlock situation.
Now in Haskell STM:
withdraw :: TVar Int -> Int -> STM ()
withdraw acc n = do bal <- readTVar acc
if bal < n then retry
writeTVar acc (bal-n)
deposit :: TVar Int -> Int -> STM ()
deposit acc n = do bal <- readTVar acc
writeTVar acc (bal+n)
transfer :: TVar Int -> TVar Int -> Int -> STM ()
transfer from to n = do withdraw from n
deposit to n
Since there is no explicit lock, withdraw
and deposit
compose naturally in transfer
. The semantics still ensure that if withdraw fails, the whole transfer fails. It also ensures that the withdraw and the deposit will be done atomically, since the type system ensures that you cannot call transfer outside of an atomically
.
atomically :: STM a -> IO a
This example comes from this: http://cseweb.ucsd.edu/classes/wi11/cse230/static/lec-stm-2x2.pdf Adapted from this paper you might want to read: http://research.microsoft.com/pubs/74063/beautiful.pdf