clojure - 事务内部的deref可能会触发重试-ref状态历史记录的作用是什么?

标签 clojure stm

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

(...) if a new value if commited by another transaction since the beginning of the current transaction, the new value of the ref as of the start of the transaction cannot be provided. Helpfully, the STM notices this problem and maintains a bounded history of the states of refs involved in a transaction, where the size of the history is incremented by each retry. This increases the chance that - at some point - the transaction won't have to retry anymore because, while the ref is concurently updated, the desired value is still present in the history.



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

首先,说明仅在完成所有写程序事务后读取事务才会成功(因此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的更改时)。我不明白的是这一部分:“这增加了在某些时候不再需要重试事务的机会,因为在引用不断更新的同时,历史记录中仍然存在所需的值”。

什么是“期望值”?在上面的示例中,引用历史记录如何随时间变化?有人可以指出时间引用的解释或示例,以显示引用历史记录的工作方式吗?

最佳答案

Clojure的STM不在乎现在。到进行观察时,礼物已经移动了。 Clojure的STM仅关心捕获状态的一致快照。

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

因此,想象一下,相反,我们正在读取ab并尝试返回它们的总和。当我们返回总和时,我们不在乎ab是最新的-试图跟上现在是徒劳的。我们要做的只是ab来自一致的时间段。

如果在dosync块中时先读取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毫秒后进行。因此,在每次尝试读取之前,都会对a进行50次更新。最小历史记录和最大历史记录的默认调优是a乐观地发生0个更新,最多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。现在,可以在a的开头找到与a一致的dosync。在两次尝试更新a期间,历史和成功发生了两次。

最后,
(stm-experiment 48 48)
;=> 500

由于历史记录的长度上限为48,因此在进行50次更新之前,我们永远找不到开头的50 x 2 = 100的值。

关于clojure - 事务内部的deref可能会触发重试-ref状态历史记录的作用是什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/21966319/

相关文章:

clojure - 在重复时停止和拆分生成的序列 - clojure

concurrency - 使用代理来完成 STM 交易中的副作用

concurrency - Clojure 中的 commute 和 alter 有什么区别?

clojure - 添加到 Clojure 持久矢量图

java - clojure - 如何(以及何时)调用点运算符?

Clojure:atom 和 ref 的惯用用法?

haskell - 对于某些 TVar 具有部分原子性的 STM

networking - 1个处理 channel ,2个同类型IO源

clojure - IllegalStateException 试图调用未绑定(bind)的 fn

jar - Leiningen:创建可执行 jar 以在没有 java -jar 的情况下运行