Программная транзакционная память - пример компоновки
Одним из основных преимуществ программной транзакционной памяти, которая всегда упоминается, является возможность компоновки и модульность. Различные фрагменты могут быть объединены для получения более крупных компонентов. В программах, основанных на блокировке, это часто не так.
Я ищу простой пример, иллюстрирующий это с помощью реального кода. Я бы предпочел пример в Clojure, но с Хаскеллом тоже все в порядке. Бонусные баллы, если в примере также показан некоторый код на основе блокировки, который не может быть легко составлен.
4 ответа
Пример, когда блокировки не создаются в 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)
Затем вы можете легко составить функцию переноса для создания транзакции более высокого уровня, например, переноса с нескольких учетных записей:
(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)
Обратите внимание, что все множественные переносы происходили в одной комбинированной транзакции, то есть было возможно "составить" меньшие транзакции.
Сделать это с блокировками будет очень быстро: если учетные записи должны быть индивидуально заблокированы, вам нужно будет сделать что-то вроде установления протокола порядка блокировки, чтобы избежать взаимных блокировок. Как справедливо отмечает Джон, в некоторых случаях вы можете сделать это, отсортировав все блокировки в системе, но в большинстве сложных систем это невозможно. Сделать ошибку, которую трудно обнаружить, очень легко. СТМ спасает вас от всей этой боли.
И чтобы сделать пример trprcolin еще более идиоматичным, я бы предложил изменить порядок параметров в транзакционной функции и сделать определения дебетов и кредитов более компактными.
(defn- transact [f account amount]
.... )
(def debit (partial transact -))
(def credit (partial transact +))