软件事务内存 - 可组合性示例

时间:2011-04-01 20:33:59

标签: haskell clojure transactional-memory

软件事务内存的一个主要优点是可组合性和模块性。可以组合不同的片段以产生更大的组分。在基于锁的程序中,通常情况并非如此。

我正在寻找一个用实际代码说明这一点的简单示例。我更喜欢Clojure中的一个例子,但Haskell也很好。如果示例还展示了一些无法轻松编写的基于锁的代码,则会获得奖励。

4 个答案:

答案 0 :(得分:20)

在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);
  }
}

所以,存款是可以的,退出是可以的,但转移不合适:如果A开始转移到B,当B开始转移到A时,我们可能会遇到死锁情况。

现在在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

由于没有明确的锁定,withdrawdeposit自然地在transfer中构成。语义仍然确保如果撤销失败,整个传输失败。它还确保撤销和存款将以原子方式完成,因为类型系统确保您不能在atomically之外调用转移。

atomically :: STM a -> IO a

这个例子来自于: http://cseweb.ucsd.edu/classes/wi11/cse230/static/lec-stm-2x2.pdf 根据本文改编,您可能需要阅读: http://research.microsoft.com/pubs/74063/beautiful.pdf

答案 1 :(得分:6)

将Ptival的例子翻译成Clojure:

;; (def example-account (ref {:amount 100}))

(defn- transact [account f amount]
  (dosync (alter account update-in [:amount] f amount)))

(defn debit [account amount] (transact account - amount))
(defn credit [account amount] (transact account + amount))
(defn transfer [account-1 account-2 amount]
  (dosync
    (debit account-1 amount)
    (credit account-2 amount)))

所以debitcredit可以自己调用,就像Haskell版本一样,事务嵌套,所以整个transfer操作是原子的,重试将在提交时进行碰撞,您可以添加验证器以保持一致性等。

当然,语义是永远不会陷入僵局的。

答案 2 :(得分:5)

这是一个Clojure示例:

假设您有一个银行账户向量(在现实生活中,向量可能会更长一点......):

(def accounts 
 [(ref 0) 
  (ref 10) 
  (ref 20) 
  (ref 30)])

(map deref accounts)
=> (0 10 20 30)

一个“转移”功能,可以在一个交易中安全地转移两个账户之间的金额:

(defn transfer [src-account dest-account amount]
  (dosync
    (alter dest-account + amount)
    (alter src-account - amount)))

其工作原理如下:

(transfer (accounts 1) (accounts 0) 5)

(map deref accounts)
=> (5 5 20 30)

然后,您可以轻松编写转移功能以创建更高级别的交易,例如从多个帐户转移:

(defn transfer-from-all [src-accounts dest-account amount]
  (dosync
    (doseq [src src-accounts] 
      (transfer src dest-account amount))))

(transfer-from-all 
  [(accounts 0) (accounts 1) (accounts 2)] 
  (accounts 3) 
  5)

(map deref accounts)
=> (0 0 15 45)

请注意,所有多次转账都发生在单一的合并交易中,即可以“组合”较小的交易。

使用锁定执行此操作会很快变得复杂:假设帐户需要单独锁定,那么您需要执行诸如在锁定获取顺序上建立协议以避免死锁的操作。正如Jon正确指出的那样,你可以在某些情况下通过对系统中的所有锁进行排序来实现这一点,但在大多数复杂系统中,这是不可行的。做出难以察觉的错误很容易。 STM可以帮助您免受所有这些痛苦。

答案 3 :(得分:2)

为了使trprcolin的例子更加惯用,我建议更改 transact 函数中的参数顺序,并定义 debit credit 更多紧凑。

(defn- transact [f account amount]
    ....  )

(def debit  (partial transact -))
(def credit (partial transact +))