软件事务内存的一个主要优点是可组合性和模块性。可以组合不同的片段以产生更大的组分。在基于锁的程序中,通常情况并非如此。
我正在寻找一个用实际代码说明这一点的简单示例。我更喜欢Clojure中的一个例子,但Haskell也很好。如果示例还展示了一些无法轻松编写的基于锁的代码,则会获得奖励。
答案 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
由于没有明确的锁定,withdraw
和deposit
自然地在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)))
所以debit
和credit
可以自己调用,就像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 +))