软件交易内存 - 合并性示例

发布于 2024-10-29 09:27:47 字数 168 浏览 6 评论 0原文

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

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

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.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

你的背包 2024-11-05 09:27:47

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

因此,存款可以,取款可以,但转账不行:如果当 B 开始向 A 转账时 A 也开始向 B 转账,我们可能会出现死锁情况。

现在在 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 :: STM a -> IO a

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

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

你又不是我 2024-11-05 09:27:47

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 版本一样,交易嵌套,所以整个 传输操作是原子的,在提交冲突时会发生重试,您可以添加验证器以确保一致性等。

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

A translation of Ptival's example to 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)))

So debit and credit are fine to call on their own, and like the Haskell version, the transactions nest, so the whole transfer operation is atomic, retries will happen on commit collisions, you could add validators for consistency, etc.

And of course, semantics are such that they will never deadlock.

等你爱我 2024-11-05 09:27:47

这是一个 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)

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

使用锁来做到这一点很快就会变得复杂:假设需要单独锁定帐户,那么您需要做一些事情,例如建立关于锁获取顺序的协议,以避免死锁。正如乔恩正确指出的那样,在某些情况下,您可以通过对系统中的所有锁进行排序来做到这一点,但在大多数复杂的系统中这是不可行的。很容易犯难以察觉的错误。 STM 可以帮助您摆脱所有这些痛苦。

Here's a Clojure example:

Suppose you have a vector of bank accounts (in real life the vector may be somewhat longer....):

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

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

And a "transfer" function that safely transfers an amount between two accounts in a single transaction:

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

Which works as follows:

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

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

You can then easily compose the transfer function to create a higher level transaction, for example transferring from multiple accounts:

(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)

Note that all of the multiple transfers happened in a single, combined transaction, i.e. it was possible to "compose" the smaller transactions.

To do this with locks would get complicated very quickly: assuming the accounts needed to be individually locked then you'd need to do something like establishing a protocol on lock acquisition order in order to avoid deadlocks. As Jon rightly points out you can do this in some cases by sorting all the locks in the system, but in most complex systems this isn't feasible. It's very easy to make a hard-to-detect mistake. STM saves you from all this pain.

灼疼热情 2024-11-05 09:27:47

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

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

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

And to make trprcolin's example even more idiomatic i would suggest changing parameter order in transact function, and make definitions of debit and credit more compact.

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

(def debit  (partial transact -))
(def credit (partial transact +))
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文