我有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)
当我运行此代码时,代理打印的最后一个值是(经过多次尝试):
435445
,
4556294
,
1322061
,
3950017
。
但绝不是9999999
我所期待的。
而且每次收藏真的都是空的。
我做错了什么?
修改
我尽可能简单地重写了worker-loop:
(defn worker-loop
[{:keys [tasks] :as state}]
(loop []
(when-let [task (get-task tasks)]
(println "Task: " task)
(recur)))
state)
但问题仍然存在。 当创建一个且只有一个worker时,此代码的行为与预期一致。
答案 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))))
运行此代码表明,i
和t
都是懒惰的,可靠地工作,而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))))