常に言及されるソフトウェアトランザクショナルメモリの主な利点の1つは、構成可能性とモジュール性です。さまざまなフラグメントを組み合わせて、より大きなコンポーネントを作成できます。ロックベースのプログラムでは、これは当てはまらないことがよくあります。
これを実際のコードで説明する簡単な例を探しています。Clojureの例をお勧めしますが、Haskellも問題ありません。例が簡単に作成できないロックベースのコードも示している場合のボーナスポイント。
常に言及されるソフトウェアトランザクショナルメモリの主な利点の1つは、構成可能性とモジュール性です。さまざまなフラグメントを組み合わせて、より大きなコンポーネントを作成できます。ロックベースのプログラムでは、これは当てはまらないことがよくあります。
これを実際のコードで説明する簡単な例を探しています。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);
}
}
したがって、入金はOK、引き出しはOKですが、転送はOKではありません.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
明示的なロックがないため、で自然に構成withdraw
できます。セマンティクスは、撤回が失敗した場合でも転送全体が失敗することを保証します。また、型システムにより transfer を の外部で呼び出すことができないため、引き出しと入金がアトミックに行われることが保証されます。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)
そして、1 回のトランザクションで 2 つのアカウント間で金額を安全に転送する「転送」機能:
(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 は、このすべての苦痛からあなたを救います。
そして、trprcolin の例をさらに慣用的にするために、トランザクション関数のパラメーターの順序を変更し、借方と貸方の定義をよりコンパクトにすることをお勧めします。
(defn- transact [f account amount]
.... )
(def debit (partial transact -))
(def credit (partial transact +))