17

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

我正在寻找一个用实际代码说明这一点的简单示例。我更喜欢 Clojure 中的一个例子,但 Haskell 也很好。如果该示例还展示了一些不易编写的基于锁的代码,则可以加分。

4

4 回答 4

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在. 语义仍然确保如果撤回失败,则整个传输失败。它还确保取款和存款将自动完成,因为类型系统确保您不能在.deposittransferatomically

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

于 2011-04-01T21:02:52.823 回答
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操作是原子的,重试将在提交冲突时发生,您可以添加验证器以实现一致性等。

当然,语义是这样的,它们永远不会死锁。

于 2011-04-01T22:15:55.387 回答
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)

然后,您可以轻松地编写 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 将您从所有这些痛苦中解救出来。

于 2011-04-06T21:01:17.120 回答
2

为了使 trrpcolin 的示例更加惯用,我建议更改交易函数中的参数顺序,并使借方贷方的定义更加紧凑。

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

(def debit  (partial transact -))
(def credit (partial transact +))
于 2011-04-01T22:37:18.913 回答