9

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

(...) 如果自当前事务开始后由另一个事务提交了新值,则无法提供截至事务开始时的新值。有用的是,STM 注意到了这个问题,并维护了事务中涉及的 refs 状态的有限历史,其中历史的大小随着每次重试而增加。这增加了 - 在某些时候 - 事务不必再重试的机会,因为虽然 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 被同时更新,但所需的值仍然存在于历史记录中”。

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

4

1 回答 1

13

Clojure 的 STM 不关心现在。在进行观察时,现在已经发生了变化。Clojure 的 STM 只关心捕获一致的状态快照。

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

因此,想象一下我们正在读取 ana和 ab并尝试返回它们的总和。我们不在乎这一点,a并且b当我们返回总和时是最新的——试图跟上现在是徒劳的。我们所关心的就是这个a,并且b来自一个一致的时期。

如果在一个dosync块中,我们读取a然后在两次读取之间进行了更新,则我们有一个b不一致的时间点。我们必须再试一次——从头再来,然后试着从最近的现在开始阅读。babab

b除非...假设我们为 的每次更改保留历史记录b。和以前一样,假设我们阅读a然后,但是在我们完成之前发生b了更新。b由于我们保存了历史b,我们可以及时回到b更改之前,找到一致的ab。然后,通过一个一致的ab最近的过去,我们可以返回一个一致的总和。我们不必使用近期的新值重试(并可能再次失败)。


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

通过这个例子,假设我们正在尝试协调多个 refs。默认初始历史长度为 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 毫秒后。a因此,在每次尝试读取之前会发生50 次更新。min-history 和 max-history 的默认调整是乐观地发生 0 次更新a,最多 10 次。也就是说,我们从没有历史记录开始a,每次发生故障时,我们都会增加一个历史记录a,但最多只能增加 10 次。由于发生了 50 次更新,这永远不够。

相比于

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

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

也试试

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

初始历史长度为 48 时,对 50 的更改a将导致历史耗尽和读取错误。但是,这个读取错误会将历史记录延长到 49。这仍然不够,因此发生另一个读取错误并将历史记录延长到 50。现在可以在历史记录和成功中找到与开头的a一致在两次尝试更新时间后发生。adosynca50 x 2 = 100

最后,

(stm-experiment 48 48)
;=> 500

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

于 2014-02-24T02:38:35.047 回答