事务中的deref可能会触发重试 - ref状态历史的作用是什么?

时间:2014-02-23 09:14:09

标签: clojure stm

“Clojure编程”(Emerick,O'Reilly)指出:

  

(...)如果一个新值如果从当前事务开始以来由另一个事务提交,则无法提供从事务开始时起的ref的新值。有帮助的是,STM注意到了这个问题并维护了事务中涉及的ref状态的有界历史,其中历史的大小随每次重试而增加。这增加了 - 在某些时候 - 事务不再需要重试的机会,因为,当ref被更新时,所需的值仍然存在于历史中。

接下来,他们提供一些代码示例来说明问题。

首先,说明只有在完成所有作家交易之后(即a = 500),阅读交易才会成功:

(def a (ref 0))
(future (dotimes [_ 500] (dosync (Thread/sleep 20) (alter a inc))))
@(future (dosync (Thread/sleep 1000) @a))
; 500
(ref-history-count a)
; 10

其次,为了说明设置:min-history:max-history可以帮助读取器事务重试(此时a已成功读取 - 值33):

(def a (ref 0 :min-history 50 :max-history :100))
(future (dotimes [_ 500] (dosync (Thread/sleep 20) (alter a inc))))
@(future (dosync (Thread/sleep 1000) @a))
; 33

我确实理解为什么阅读器事务中的deref会导致它重试(当某些编写器事务将更改提交给ref时)。我不明白的是这一部分:“这增加了 - 在某些时候 - 交易不再需要重试的机会,因为,当ref得到最新更新时,所需的值仍然存在于历史中”。

什么是“期望值”?在上面的例子中,ref历史如何随时间变化?有人能指出我的解释或一些时间表显示参考历史是如何工作的吗?

1 个答案:

答案 0 :(得分:13)

Clojure的STM并不关心现在。到观察时,现在已经移动了。 Clojure的STM只关心捕获状态的一致快照。

这个示例不是很明显,因为我们知道单个读取始终是一致的快照。但是,如果您只在一个dosync上使用ref,那么您可能根本不应该使用ref,而是使用atom

所以,想象一下,我们正在阅读ab并试图返回它们的总和。当我们返回总和时,我们并不关心ab是当前的 - 试图跟上现在是徒劳的。我们所关注的是ab来自一段时间。

如果在dosync块中,我们在{2}读取a,然后b,但b已更新,我们有ab来自不一致的时间点。我们必须再试一次 - 重新开始并尝试从近处读取a然后b

除非......假设我们对b的每次更改保留了b的历史记录。和以前一样,假设我们阅读a然后b,但在我们完成之前就会对b进行更新。由于我们保存了b的历史记录,因此我们可以在b更改之前及时返回并找到一致的ab。然后,通过近距离的一致ab,我们可以返回一致的总和。我们不必使用近现在的新值重试(并且可能再次失败)。


通过将输入dosync时拍摄的快照与退出时的快照进行比较来维持一致性。在此模型下,对其间相关数据的任何更改都需要重试。默认情况是乐观的情况。当发生故障时,它会在适用的ref上标记,以便下次更改时保留历史记录。现在,只要在退出时拍摄的快照与退出时的快照进行比较或保留单个过去的历史记录,就会保持一致性。因此,现在ref期间对dosync的单个更改不会导致失败。两个变化仍然会因为历史将耗尽。如果确实发生了另一次故障,则再次标记该故障,并且现在保持长度为2的历史记录。

通过这个例子,假装我们正在尝试协调多个引用。默认的初始历史记录长度为0,最大值为10.

(defn stm-experiment 
  [min-hist max-hist] 
  (let [a (ref 0 :min-history min-hist :max-history max-hist)] 
    (future (dotimes [_ 500] (dosync (Thread/sleep 20) (alter a inc)))) 
    (dosync (Thread/sleep 1000) @a)))

所以默认是

(stm-experiment 0 10)
;=> 500 (probably)

a的更新每20毫秒发生一次,读取发生在1000毫秒之后。因此,在每次尝试读取之前,会发生50次a更新。 min-history和max-history的默认调整是乐观地0次更新将发生在a,最多10次将发生。也就是说,我们从a开始没有历史记录,每次发生故障时,我们将a的历史记录增加一个,但最多只增加10个。由于发生了50次更新,因此永远不会足够。

比较
(stm-experiment 50 100)
;=> 0 (quite possibly, multicore)

历史记录为50,a的所有50个更改都保留在历史记录中,因此我们在输入时捕获的a状态仍然位于历史队列的最后退出。

尝试

(stm-experiment 48 100)
;=> 100 (or thereabouts, multicore)

如果初始历史记录长度为48,那么对a的50次更改将导致历史记录耗尽并导致读取错误。但是,这个读取错误会将历史记录延长到49.这仍然不够,所以发生了另一个读取错误并且历史记录被延长到50.现在aa一致可以在历史记录中找到dosync的{​​{1}},并且在a更新50 x 2 = 100次之后两次尝试成功。

最后,

(stm-experiment 48 48)
;=> 500

在历史记录长度上限为48,我们永远无法找到50个更新发生之前我们开始的a的值。