ABA с программным обеспечением Clojure Transactional Memory
Мне было интересно, есть ли у Clojure встроенное решение проблемы ABA. Я создавал пример, который показывает эту проблему, но каким-то образом Clojure обнаруживает изменения. Это потому, что транзакции Clojure сравнивают ссылки, а не значения?
Мой пример:
(def x (ref 42))
(def io (atom false))
(def tries (atom 0))
(def t1 (Thread. (fn [] (dosync (commute x - 42)))))
(def t2 (Thread. (fn [] (dosync
(Thread/sleep 100)
(commute x + 42)))))
(def t3 (Thread.
(fn []
(dosync
(do
(Thread/sleep 1000)
(swap! tries inc)
(if (= 42 @x)
(reset! io true)))))))
(.start t3)
(.start t1)
(.start t2)
(.join t1)
@x
(.join t2)
@x
(.join t3)
@tries
(if (= true @io) (println "The answer is " @x))
Число попыток всегда равно 2, поэтому транзакция t3 должна была заметить изменения ref для t1 и t2. Кто-то знает причину такого поведения?
2 ответа
Прежде чем ответить на поставленный вопрос, позвольте мне сказать, что, безусловно, лучший источник информации о STM Clojure, помимо самого исходного кода, о котором мне известно, это статья Марка Фолькмана о программной транзакционной памяти (ссылка указывает на страницу журнала изменений, перейдите по ссылке на последнюю версию оттуда). Это невероятно всеобъемлющее. (Не беспокойтесь о отметке времени 2009 года, STM не сильно изменится.) Если вы хотите точно продумать, как все работает в таких сценариях, как этот, я настоятельно рекомендую прочитать его.
Что касается сценария под рукой:
Для чтения ссылки в транзакции STM обещает вернуть значение, которое было зафиксировано до попытки текущей транзакции. (Если, конечно, текущая попытка транзакции сама не установила значение в транзакции рассматриваемого Ref.) Это значение может быть или не быть самым последним значением, записанным в Ref, однако, если это не так, чтение должно быть доволен историей Ref. Если в истории ссылки нет такого значения, то для ссылки записывается ошибка, и транзакция повторяется. Впоследствии длина цепочки истории ссылки может быть увеличена из-за ошибки до максимальной длины истории ссылки (по умолчанию 10), хотя учтите, что это произойдет только при наличии возможности (еще одна запись в ссылку) и будет только помочь транзакциям, начатым "достаточно поздно" (чтобы их временные метки были позже, чем у меток некоторого значения, записанных в истории).
В случае под рукой, к тому времени t3
приступает к чтению ссылки, t1
а также t2
завершит свои записи в x
без проблем и x
больше не сможет удовлетворить запрос на чтение, требующий значения от t3
Первая попытка (Это потому, что цепочка истории ссылки по умолчанию начинается с длины 0, что означает, что исторические значения не сохраняются.) t3
должен записать ошибку для x
и повторите попытку.
(Если вы повторно запустите три транзакции для одного и того же Ref и вспомогательного атома - скажем, снова вставив все, кроме верхних трех строк в ваш REPL - вы увидите tries
прыгать, чтобы 4
во второй раз, а затем 5
на третьем указывает на то, что на данный момент доступна историческая ценность.)
По проблеме АБА:
Проблема ABA не относится к STM, потому что в надлежащем сценарии ABA буква "B" записывается в ячейку памяти (1) другим потоком и (2) после первого чтения "A" "главным" поток (тот, который должен страдать от проблемы ABA), а затем аналогично второй "A" записывается (1) другим потоком и (2) после записи "B", и оба "As" наблюдаются основной поток, но "B" - нет, но, как объяснено выше, в транзакции STM вы не можете наблюдать значение, записанное в Ref другим потоком после начала попытки транзакции, поэтому, если вы наблюдаете первое "A", Вы не сможете наблюдать "В" или второе "А".
Это не означает, что с STM не может возникнуть проблем, связанных с параллелизмом - довольно легко столкнуться с перекосом записи (описано в статье в Википедии об изоляции моментальных снимков - это то, что ensure
функция предназначена для исправления, но это зависит от кода пользователя, чтобы вызывать ее там, где это необходимо), commute
могут быть использованы не по назначению и c.
Вы правы, что это ожидаемое поведение (хотя я бы ожидал tries
быть 1). Помимо множества книг Clojure, в которых обсуждается программная транзакционная память (STM), вы можете также ознакомиться с
- https://clojure.org/reference/refs
- https://clojuredocs.org/clojure.core/dosync
- https://clojuredocs.org/clojure.core/ensure
Кроме того, обычно лучше всего использовать alter
вместо commute
, что легко ошибиться и обычно является случаем "преждевременной оптимизации".