总是被提及的软件事务内存的主要优点之一是可组合性和模块化。可以组合不同的片段以产生更大的组件。在基于锁的程序中,通常情况并非如此。
我正在寻找一个用实际代码说明这一点的简单示例。我更喜欢 Clojure 中的一个例子,但 Haskell 也很好。如果该示例还展示了一些不易编写的基于锁的代码,则可以加分。
总是被提及的软件事务内存的主要优点之一是可组合性和模块化。可以组合不同的片段以产生更大的组件。在基于锁的程序中,通常情况并非如此。
我正在寻找一个用实际代码说明这一点的简单示例。我更喜欢 Clojure 中的一个例子,但 Haskell 也很好。如果该示例还展示了一些不易编写的基于锁的代码,则可以加分。
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
将 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
操作是原子的,重试将在提交冲突时发生,您可以添加验证器以实现一致性等。
当然,语义是这样的,它们永远不会死锁。
这是一个 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)
然后,您可以轻松地编写 transfer 函数来创建更高级别的交易,例如从多个账户进行转账:
(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 将您从所有这些痛苦中解救出来。
为了使 trrpcolin 的示例更加惯用,我建议更改交易函数中的参数顺序,并使借方和贷方的定义更加紧凑。
(defn- transact [f account amount]
.... )
(def debit (partial transact -))
(def credit (partial transact +))