Программная транзакционная память - пример компоновки

Одно из основных преимуществ программной транзакционной памяти, о котором всегда упоминают, - это компонуемость и модульность. Различные фрагменты можно комбинировать для получения более крупных компонентов. В программах, основанных на блокировках, это часто не так.

Я ищу простой пример, иллюстрирующий это на реальном коде. Я бы предпочел пример на Clojure, но Haskell тоже подойдет. Бонусные баллы, если в примере также присутствует некоторый код на основе блокировки, который не может быть легко составлен.


person dbyrne    schedule 01.04.2011    source источник


Ответы (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

person Ptival    schedule 01.04.2011
comment
@luqui Мне никогда не приходилось сталкиваться с переполнением денег: \ Интересно, почему ... Если бы люди могли присылать мне деньги, чтобы я мог отлаживать этот путь моего кода. - person Ptival; 02.04.2011
comment
В вашем примере передачи Java есть более серьезная проблема: вы не блокируете other. Обе проблемы можно решить, заключив два вызова внутри передачи в synchronized(other) { ... }. - person Anm; 02.04.2011
comment
Это кажется правильным. Но если вы это сделаете, вы все равно окажетесь в ситуации взаимоблокировки, поскольку a.transfer (b, 1) требует блокировки на a, затем на b, а b.transfer (a, 1) требует тех же блокировок в противоположном порядке, am Я неправ? - person Ptival; 02.04.2011
comment
Та последняя статья, которую вы упомянули, просто замечательна, прочтите ее некоторое время назад. - person Waldheinz; 07.04.2011
comment
Проблема в java-коде - это порядок двойной блокировки, который по своей сути отличается (т. Е. Всегда сначала текущий объект). Он исчезает, когда вы удаляете synchronized на transfer, понижая необходимость блокировки до последовательных одиночных блокировок. Тем не менее, хороший пример и отличная ссылка (+1). - person didierc; 22.08.2013

Перевод примера Птивала на 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 операция является атомарной, повторные попытки будут происходить при коллизиях фиксации, вы можете добавить валидаторы для согласованности и т. Д.

И, конечно же, семантика такова, что они никогда не зайдут в тупик.

person trptcolin    schedule 01.04.2011
comment
Но вы не проверяете, что мы не снимаем больше, чем есть на счету, так что все немного по-другому. Но это Clojure, так что это будет интересно для dbyrne. Также хорошее повторное использование aav. - person Ptival; 02.04.2011
comment
Это честно. Для этого вы должны использовать валидаторы: - person trptcolin; 02.04.2011
comment
Упс. Для этого вы должны использовать упомянутые мной валидаторы, например (def account (ref {:amount 100} :validator (fn [act] (>= (:amount act) 0)))) Я бы разделил функцию валидатора на именованную функцию в реальном коде. Это обеспечивает гарантию согласованности, о которой я упоминал. - person trptcolin; 02.04.2011

Вот пример 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 избавляет вас от всей этой боли.

person mikera    schedule 06.04.2011
comment
@Jon - конечно, сортировка работает в простых случаях для упорядочивания блокировок. Но это не так просто в корпоративном приложении с сотнями классов, каждый со своей собственной стратегией блокировки и необходимостью координировать различные типы транзакций во всем этом. Это когда замки не складываются, начинает сильно болеть. - person mikera; 07.10.2011
comment
Я полностью согласен. Я просто подумал, что было бы полезно указать, что решение, по крайней мере, теоретически простое. - person J D; 11.10.2011
comment
Как будет выглядеть протокол блокировки в этом примере, даже если не учитывать случай большого приложения? - person GS - Apologise to Monica; 12.10.2011
comment
@Ganesh - вы можете дать каждой учетной записи идентификатор учетной записи и получить отдельные блокировки для каждой учетной записи, которую вы хотите изменить, в порядке идентификатора учетной записи. Если вы сделаете это таким образом, вы сможете избежать тупиковых ситуаций. - person mikera; 13.10.2011
comment
Предполагает ли это, что повторно входящие блокировки, чтобы мы могли сначала получить их вне передачи, а затем позволить передаче повторно захватить их? Без этого, я думаю, нам понадобится альтернативная версия передачи без блокировки. - person GS - Apologise to Monica; 13.10.2011

А чтобы сделать пример trprcolin еще более идиоматичным, я бы предложил изменить порядок параметров в функции transact и сделать определения дебет и кредит более компактными.

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

(def debit  (partial transact -))
(def credit (partial transact +))
person aav    schedule 01.04.2011
comment
На самом деле в Clojure это не более идиоматично. Для идиоматического Clojure учетная запись должна идти первой. - person kotarak; 02.04.2011