clojure ref

时间:2016-08-20 13:55:30

标签: concurrency clojure ref stm

我有100名工作人员(代理人)共享一个包含任务集合的ref。虽然此集合包含任务,但每个工作人员从此集合中获取一个任务(在dosync块中),打印它,有时将其放回集合中(在dosync块中):

(defn have-tasks?
  [tasks]
  (not (empty? @tasks)))

(defn get-task
  [tasks]
  (dosync
    (let [task (first @tasks)]
      (alter tasks rest)
      task)))

(defn put-task
  [tasks task]
  (dosync (alter tasks conj task))
  nil)

(defn worker
  [& {:keys [tasks]}]
  (agent {:tasks tasks}))

(defn worker-loop
  [{:keys [tasks] :as state}]
  (while (have-tasks? tasks)
    (let [task (get-task tasks)]
      (println "Task: " task)
      (when (< (rand) 0.1)
        (put-task tasks task))))
  state)

(defn create-workers
  [count & options]
  (->> (range 0 count)
       (map (fn [_] (apply worker options)))
       (into [])))

(defn start-workers
  [workers]
  (doseq [worker workers] (send-off worker worker-loop)))

(def tasks (ref (range 1 10000000)))

(def workers (create-workers 100 :tasks tasks))

(start-workers workers)
(apply await workers)

当我运行此代码时,代理打印的最后一个值是(经过多次尝试): 435445455629413220613950017。 但绝不是9999999我所期待的。 而且每次收藏真的都是空的。 我做错了什么?

修改

我尽可能简单地重写了worker-loop:

(defn worker-loop
  [{:keys [tasks] :as state}]
  (loop []
    (when-let [task (get-task tasks)]
      (println "Task: " task)
      (recur)))
  state)

但问题仍然存在。 当创建一个且只有一个worker时,此代码的行为与预期一致。

3 个答案:

答案 0 :(得分:4)

这里的问题与代理无关,几乎与懒惰无关。这是原始代码的某种简化版本仍然存在问题:

(defn f [init]
  (let [state (ref init)
        task (fn []
               (loop [last-n nil]
                 (if-let [n (dosync
                              (let [n (first @state)]
                                (alter state rest)
                                n))]
                   (recur n)
                   (locking :out
                     (println "Last seen:" last-n)))))
        workers (->> (range 0 5)
                     (mapv (fn [_] (Thread. task))))]
    (doseq [w workers] (.start w))
    (doseq [w workers] (.join w))))

(defn r []
  (f (range 1 100000)))

(defn i [] (f (->> (iterate inc 1)
                   (take 100000))))

(defn t []
  (f (->> (range 1 100000)
          (take Integer/MAX_VALUE))))

运行此代码表明,it都是懒惰的,可靠地工作,而r可靠地不工作。问题实际上是range调用返回的类中的并发错误。实际上,该错误已记录在this Clojure ticket中,并且在Clojure版本1.9.0-alpha11中已修复。

如果由于某种原因无法访问故障单的错误的快速摘要:在rest调用range结果的内部,竞争条件很小的机会:“flag”表示“已计算下一个值”为set before the actual value itself,这意味着第二个线程可以将该标志视为true,即使“下一个值”仍为{{ 1}}。然后,对nil的调用将修复ref上的alter值。它由swapping the two assignment lines修复。

如果nil的结果要么在单个线程中强制实现,要么包含在另一个懒惰的seq中,那么该错误就不会出现。

答案 1 :(得分:3)

我在question上问Clojure Google Group,这有助于我找到答案。

问题是我在STM事务中使用了一个惰性序列。

当我更换此代码时:

(def tasks (ref (range 1 10000000)))

由此:

(def tasks (ref (into [] (range 1 10000000))))

它按预期工作了!

在我发生问题的生产代码中,我使用了Korma框架,它也返回了一个懒惰的元组集合,如我的例子所示。

结论:避免在STM事务中使用惰性数据结构。

答案 2 :(得分:1)

当达到范围中的最后一个数字时,工作人员仍然保留较旧的数字。其中一些将返回队列,再次处理。

为了更好地了解正在发生的事情,您可以更改worker-loop以打印每个工作人员处理的上一个任务:

(defn worker-loop
  [{:keys [tasks] :as state}]
  (loop [last-task nil]
    (if (have-tasks? tasks)
      (let [task (get-task tasks)]
        ;; (when (< (rand) 0.1)
        ;;   (put-task tasks task)
        (recur task))
      (when last-task
        (println "Last task:" last-task))))
  state)

这也显示了代码中的竞争条件,其中have-tasks?看到的任务经常是在处理任务结束时调用get-task时由其他人执行的。

竞争条件可以通过删除have-tasks?来解决,而是使用来自get-task的返回值nil作为不再有任务可用的信号(此刻)。

<强>更新

如所观察到的,这种竞争条件并不能解释问题。

通过在get-task中删除可能的竞争条件来解决问题:

(defn get-task [tasks]
  (dosync
   (first (alter tasks rest))))

但是,将get-task更改为使用显式锁似乎可以解决问题:

 (defn get-task [tasks]  
   (locking :lock
     (dosync
       (let [task (first @tasks)]
         (alter tasks rest)
         task))))