【问题标题】:Producer consumer with qualifications有资质的生产者消费者
【发布时间】:2011-02-15 03:28:15
【问题描述】:

我是 clojure 的新手,我正在尝试了解如何正确使用它的并发功能,因此感谢任何批评/建议。 所以我正在尝试用 clojure 编写一个小测试程序,其工作原理如下:

  1. 有 5 个生产者和 2 个消费者
  2. 生产者等待随机时间,然后将一个数字推送到共享队列。
  3. 消费者应在队列非空时立即从队列中取出一个号码,然后短时间休眠以模拟工作
  4. 当队列为空时,消费者应该阻塞
  5. 当队列中有超过 4 个项目时,生产者应该阻塞,以防止它变得庞大

这是我对上述每个步骤的计划:

  1. 生产者和消费者将成为并不真正关心他们的状态的代理(只是 nil 值或其他东西);我只是在某个时候使用代理发送“消费者”或“生产者”功能。那么共享队列将是(def queue (ref []))。也许这应该是一个原子?
  2. 在“生产者”代理函数中,只需将 (Thread/sleep (rand-int 1000)) 和 (dosync (alter queue conj (rand-int 100))) 推入队列。
  3. 我正在考虑让消费者代理使用 add-watcher 观察队列的变化。虽然不确定这一点..它会在任何更改时唤醒消费者,即使更改来自消费者拉出某些东西(可能使其为空)。也许在观察者函数中检查这一点就足够了。我看到的另一个问题是,如果所有消费者都很忙,那么当生产者向队列中添加新内容时会发生什么?观看的事件是在某个消费者代理上排队还是消失了?
  4. 见上文
  5. 我真的不知道该怎么做。我听说 clojure 的序列可能有用,但我找不到足够的文档来说明如何使用它,而且我的初始测试似乎不起作用(抱歉我身上没有代码了)

【问题讨论】:

    标签: clojure


    【解决方案1】:

    这是我的看法。我强调只使用 Clojure 数据结构来看看效果如何。请注意,从 Java 工具箱中获取阻塞队列并在这里使用它是非常常见和惯用的;我认为代码很容易适应。 更新:我确实将它改编为java.util.concurrent.LinkedBlockingQueue,见下文。

    clojure.lang.PersistentQueue

    致电(pro-con) 开始试运行;然后查看output 的内容,看看是否发生了什么事,看看queue-lengths 的内容是否在给定的范围内。

    更新:为了解释为什么我觉得有必要在下面使用ensure(我在 IRC 上被问到这个问题),这是为了防止写入偏差(请参阅 Snapshot isolation 上的维基百科文章用于定义)。如果我将@queue 替换为(ensure queue),则两个或多个生产者可以检查队列的长度,发现它小于4,然后在队列中放置额外的项目,并且可能带来总长度为4以上的队列,打破了约束。同样,两个执行@queue 的消费者可以接受相同的项目进行处理,然后将两个项目从队列中弹出。 ensure 可防止这些情况发生。

    (def go-on? (atom true))
    (def queue (ref clojure.lang.PersistentQueue/EMPTY))
    (def output (ref ()))
    (def queue-lengths (ref ()))
    (def *max-queue-length* 4)
    
    (defn overseer
      ([] (overseer 20000))
      ([timeout]
         (Thread/sleep timeout)
         (swap! go-on? not)))
    
    (defn queue-length-watch [_ _ _ new-queue-state]
      (dosync (alter queue-lengths conj (count new-queue-state))))
    
    (add-watch queue :queue-length-watch queue-length-watch)
    
    (defn producer [tag]
      (future
       (while @go-on?
         (if (dosync (let [l (count (ensure queue))]
                       (when (< l *max-queue-length*)
                         (alter queue conj tag)
                         true)))
           (Thread/sleep (rand-int 2000))))))
    
    (defn consumer []
      (future
       (while @go-on?
         (Thread/sleep 100)       ; don't look at the queue too often
         (when-let [item (dosync (let [item (first (ensure queue))]
                                   (alter queue pop)
                                   item))]
           (Thread/sleep (rand-int 500))         ; do stuff
           (dosync (alter output conj item)))))) ; and let us know
    
    (defn pro-con []
      (reset! go-on? true)
      (dorun (map #(%1 %2)
                  (repeat 5 producer)
                  (iterate inc 0)))
      (dorun (repeatedly 2 consumer))
      (overseer))
    

    java.util.concurrent.LinkedBlockingQueue

    使用LinkedBlockingQueue 编写的上述版本。请注意,代码的大致轮廓基本相同,但一些细节实际上稍微干净一些。我从这个版本中删除了queue-lengths,因为LBQ 为我们处理了这个约束。

    (def go-on? (atom true))
    (def *max-queue-length* 4)
    (def queue (java.util.concurrent.LinkedBlockingQueue. *max-queue-length*))
    (def output (ref ()))
    
    (defn overseer
      ([] (overseer 20000))
      ([timeout]
         (Thread/sleep timeout)
         (swap! go-on? not)))
    
    (defn producer [tag]
      (future
       (while @go-on?
         (.put queue tag)
         (Thread/sleep (rand-int 2000)))))
    
    (defn consumer []
      (future
       (while @go-on?
         ;; I'm using .poll on the next line so as not to block
         ;; indefinitely if we're done; note that this has the
         ;; side effect that nulls = nils on the queue will not
         ;; be handled; there's a number of other ways to go about
         ;; this if this is a problem, see docs on LinkedBlockingQueue
         (when-let [item (.poll queue)]
           (Thread/sleep (rand-int 500)) ; do stuff
           (dosync (alter output conj item)))))) ; and let us know
    
    (defn pro-con []
      (reset! go-on? true)
      (dorun (map #(%1 %2)
                  (repeat 5 producer)
                  (iterate inc 0)))
      (dorun (repeatedly 2 consumer))
      (overseer))
    

    【讨论】:

    • 我最初的回答没有考虑到生产者注意不要让队列溢出的要求......现在更正了。
    • 更新了一个基于java.util.concurrent.LinkedBlockingQueue 的版本——它实际上比“纯Clojure”版本更干净!并且从基于clojure.lang.PersistentQueue 的原始版本的更改完全是本地的,以处理队列。这只是表明 Clojure 的 Java 互操作性到底有多棒。 :-)
    • 注意:java 队列不能正确处理 nil 值,因此您必须用特殊值替换它们。见clj-me.cgrand.net/2010/04/02/…
    • cgrand:感谢您的提醒!我想这也意味着担心nil 并不是不使用.poll 的理由。 (并不是说其他​​一些解决方案在某些情况下不会更合适,比如坚持承诺并在最后用future-cancel 杀死它们。)
    • 出于好奇,对于 clojure 版本,队列是否需要保存在 ref 中?因为您不将队列的更新与其他任何东西协调起来,所以一个原子还不足以实现这样的目的吗?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多