如何在Clojure中同步读写操作?

6
在一个Web应用程序中,我正试图从有限的ID池中生成唯一的线程安全ID。我面临的问题是,在读取和写入之间,另一个线程可能已经更改了数据结构;这就是为什么我必须使用compare-and-set!的原因。
(def sid-batch 10)
(def sid-pool (atom {:cnt 0
                     :sids '()}))

(defn get-sid []
  (let [{:keys [cnt sids] :as old} @sid-pool]

    ; use compare-and-set! here for atomic read & write
    (if (empty? sids)

      ; generate more sids
      (if (compare-and-set!
            sid-pool
            old
            (-> old
              (assoc :sids (range (inc cnt) (+ sid-batch cnt)))
              (assoc :cnt (+ cnt sid-batch))))

        ; return newest sid or recur till "transaction" succeeds
        cnt
        (recur))

      ; get first sid
      (if (compare-and-set! sid-pool old (update-in old [:sids] next))

        ; return first free sid or recur till "transaction" succeeds
        (first sids)
        (recur)))))

有没有更简单的方式在不需要手动进行STM操作且不滥用sid-pool中的字段作为swap!的返回值的情况下,实现读写同步?

1
我认为你的“滥用sid-pool中的字段”的想法是正确的。除了你不需要一个字段,只需在swap!的返回值上调用(comp first sids)即可。(sid-pool的第一个sid将始终是刚刚分配的id)。 - cgrand
3个回答

5
您可以通过在原子中添加一个字段来实现,方法就像您建议的那样在sid-pool中添加。我同意这有点粗糙,但为了如此简单的东西使用compare-and-swap!是很糟糕的。相反,使用原子;或者使用ref,在dosync块中可以返回您想要的任何内容,同时仍然具有事务安全性:
(defn get-sid []
  (dosync
   (let [{:keys [cnt sids]} @sid-pool]
     (if (empty? sids)
       (do 
         (alter sid-pool
                (fn [old]
                  (-> pool
                      (assoc :sids (range (inc cnt) (+ sid-batch cnt)))
                      (update-in [:cnt] + sid-batch))))
         cnt)
       (do
         (alter sid-pool update-in [:sids] next)
         (first sids))))))

@mikera 是正确的,如果你只需要生成整数 ID,那么只需使用 atom 并递增它。我写出这段话是因为如果你实际上正在做一些更复杂的事情,并且可以从批处理中获益。 - amalloy
这实际上是我的第一个想法,但我认为 sid-pool 可能会在 let 子句(其中它被取消引用)和 alter 调用之间更改。所以读取/取消引用毕竟是 dosync 事务的一部分? - Philip Kamenarsky
是的。如果您有一个仅读取特定ref的dosync,那么可能存在问题;我从未完全理解过这种情况。但是这绝对是一个孤立的、线程安全的事务。如果您担心,可以在某个地方添加额外的Thread/sleep来尝试它。 - amalloy

2
也许我对您试图做什么有些困惑,但在Clojure中创建唯一ID的规范方式只需:
(let [counter (atom 0)]
  (defn get-unique-id []
    (swap! counter inc)))

不需要任何复杂的锁定。请注意:

  • 闭包封装了let-bound原子,因此您可以确保没有其他人可以触及它。
  • swap!操作可以确保在并发情况下的原子安全性,因此get-unique-id函数可以在不同线程之间共享。

是的,我应该更具体一些;sids 可以返回到 sid-pool。我这样做是因为我需要每个会话的最短可能的唯一数字标识符。 - Philip Kamenarsky

2
(def sid-batch 10)
(def sid-pool (atom {:cnt 0
                     :sids '()}))

(defn get-sid []
  (first (:sids (swap! sid-pool
                  (fn [{:keys [cnt sids]}]
                    (if-let [sids (next sids)]
                      {:cnt cnt :sids sids}
                      {:sids (range cnt (+ sid-batch cnt))
                       :cnt (+ cnt sid-batch)}))))))

就像我在评论中说的那样,我认为你对“滥用sid-pool中的字段”有正确的想法。除了你不需要一个字段,只需要在swap的返回值上调用(comp first sids)!

我从调用范围中删除了inc,因为它导致生成器跳过10的倍数。

要将sid返回到池中:

(defn return-sid [sid]
  (swap! sid-pool (fn [{:keys [cnt [_ & ids]]}]
                    {:cnt cnt
                     :sids (list* _ sid ids)})))

不幸的是,这会使新返回的 sid 保留在池中。 - Philip Kamenarsky
这是一个约定的问题,任何时候可用的 ID 池是 (next (:sids @sid-pool)) 而不仅仅是 (:sids @sid-pool)。如果不是 ID 而是对大型对象的引用,则可能存在内存泄漏的潜在风险,但现在没有。它可以正常工作。 - cgrand
是的,我应该更具体一些;sids可以随时返回到sid池中。我这样做是因为我需要每个会话中最短的可能的唯一数字标识符 - 因此,除非我漏掉了什么明显的东西,否则s id不能留在sid池中。:/ - Philip Kamenarsky
在短时间内重复使用SIDs听起来不是一个好主意。我怀疑这会给你带来一些痛苦。你真的确定需要像这样限制你的SIDs吗?使用案例是什么?也许如果我们更了解原因,就有更好的解决方案了。这是XY问题吗?http://meta.stackexchange.com/questions/66377/what-is-the-xy-problem - sw1nn
@sw1nn:这很可能是一个 XY 问题;以下是使用案例:我想在两个用户之间建立一个自适应通信渠道(聊天)。为此,每个人都会收到一个 sid,然后他们交换 sids。当双方输入了其伙伴的正确 sid 时,通道就建立了。因此,我需要 sids 尽可能短,而不会发生冲突;我简要考虑过只使用 (mod (swap! sid inc) 10000) 或类似的 4 位数字,但这不可扩展,我做这件事的部分原因是为了在 Clojure 中获得一些真实世界的后端经验。 - Philip Kamenarsky

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接