Clojure

Refs 和事务

虽然 Vars 通过线程隔离来确保可变存储位置的安全使用,但事务性引用 (Refs) 通过 软件事务内存 (STM) 系统来确保可变存储位置的安全共享使用。Refs 在其生命周期内绑定到单个存储位置,并且仅允许在事务内对该位置进行修改。

如果您曾经使用过数据库事务,那么 Clojure 事务应该很容易理解 - 它们确保对 Refs 的所有操作都是原子性的、一致的和隔离的。原子性意味着在事务中对 Refs 做出的所有更改都必须全部执行,否则全部不执行。一致性意味着在允许事务提交之前,可以对每个新值使用验证器函数进行检查。隔离性意味着任何事务在运行时都不会看到任何其他事务的影响。STM 的另一个常见特征是,如果事务在运行时发生冲突,它会自动重试。

实现 STM 的方法有很多(锁定/悲观、无锁/乐观和混合),它仍然是一个研究问题。Clojure STM 使用 多版本并发控制,结合自适应历史队列实现 快照隔离,并提供了一个独特的 commute 操作。

在实践中,这意味着

  1. 所有对 Refs 的读取都将看到从事务开始点(其“读取点”)开始的“Ref 世界”的一致快照。事务看到它所做的任何更改。这称为事务内值

  2. 在事务期间对 Refs 做出的所有更改(通过 ref-setaltercommute)将看起来都发生在“Ref 世界”时间线上的一个点(其“写入点”)。

  3. 任何其他事务都不会对此事务已ref-set / altered / ensured 的任何 Refs 进行更改。

  4. 其他事务可能已对任何已通过此事务commute 的 Refs 进行了更改。这应该是可以的,因为commute 应用的函数应该是可交换的。

  5. 读取器和交换器永远不会阻塞写入器、交换器或其他读取器。

  6. 写入器永远不会阻塞交换器或读取器。

  7. 应避免在事务中进行 I/O 和其他具有副作用的活动,因为事务重试。可以使用 io! 宏来阻止在事务中使用不纯函数。

  8. 如果对正在更改的 Ref 的值的有效性的约束取决于未被更改的 Ref 的同时值,则可以通过调用 ensure 来保护第二个 Ref 免受修改。以这种方式“确保”的 Refs 将受到保护(项目 #3),但不会改变世界(项目 #2)。

  9. Clojure MVCC STM 被设计为与持久集合一起使用,强烈建议您将 Clojure 集合用作 Refs 的值。由于在 STM 事务中完成的所有工作都是推测性的,因此必须保证进行复制和修改的成本很低。持久集合可以免费复制(只需使用原始集合,它不能被更改),并且“修改”可以有效地共享结构。无论如何

  10. 放入 Refs 中的值必须是不可变的,或者被认为是不可变的!! 否则,Clojure 就无法帮助您。

示例

在此示例中,创建了一个指向向量的向量引用,每个向量包含(最初是顺序的)唯一数字。然后启动一组线程,这些线程反复选择两个随机向量中的两个随机位置并交换它们,在一个事务中。除了使用事务之外,没有采取任何特别措施来防止不可避免的冲突。

(defn run [nvecs nitems nthreads niters]
  (let [vec-refs (vec (map (comp ref vec)
                           (partition nitems (range (* nvecs nitems)))))
        swap #(let [v1 (rand-int nvecs)
                    v2 (rand-int nvecs)
                    i1 (rand-int nitems)
                    i2 (rand-int nitems)]
                (dosync
                 (let [temp (nth @(vec-refs v1) i1)]
                   (alter (vec-refs v1) assoc i1 (nth @(vec-refs v2) i2))
                   (alter (vec-refs v2) assoc i2 temp))))
        report #(do
                 (prn (map deref vec-refs))
                 (println "Distinct:"
                          (count (distinct (apply concat (map deref vec-refs))))))]
    (report)
    (dorun (apply pcalls (repeat nthreads #(dotimes [_ niters] (swap)))))
    (report)))

运行时,我们看到在洗牌过程中没有值丢失或重复

(run 100 10 10 100000)

([0 1 2 3 4 5 6 7 8 9] [10 11 12 13 14 15 16 17 18 19] ...
 [990 991 992 993 994 995 996 997 998 999])
Distinct: 1000

([382 318 466 963 619 22 21 273 45 596] [808 639 804 471 394 904 952 75 289 778] ...
 [484 216 622 139 651 592 379 228 242 355])
Distinct: 1000

创建 Ref:ref

检查 Ref:deref (另请参见 @ 读取器 宏)

事务宏:dosync io!

仅在事务中允许:ensure ref-set alter commute

Ref 验证器:set-validator! get-validator