Clojure等待条件不旋转

时间:2014-10-01 18:08:04

标签: multithreading clojure

我正在为一个线程实现一个包含消息的队列机制。该队列使用LinkedBlockingQueue中的java.util.concurrent构建。我想要实现的是以下内容。

Thread with mailbox:
defn work:
    * do some stuff
    * Get the head of the queue (a message):
        - if it is "hello":
             <do some stuff>
             <recur work fn>
        - if it is "bye":
             <do some stuff>
        - if it is none of the above, add the message to the back of queue
          and restart from "Get the head of the queue"
    * <reaching this point implies terminating the thread>

我尝试实现的第一个想法是使用围绕* Get the head of the queue的循环,使用条件检查消息并将其添加到:else分支中的队列中,如果它不匹配任何条款。这样做的缺点是,在使用recur时,在cond子句的任何主体中调用recur将始终重复循环(例如,在hello中} case)表示重复该函数(即work)。所以这不是一个选择。另一个缺点是,如果此类消息需要很长时间才能到达,线程将无限期地旋转并占用资源。

我的下一个想法(但尚未实施)正在使用未来。该计划如下。

* Get all the matches I have to match (i.e., "hello" and "bye")
* Start a future and pass it the list of messages:
    * While the queue does not contain any of the messages
      recur
    * when found, return the first element that matches.
* Wait for the future to deliver.
* if it is "hello":
    <do some stuff>
    <recur work fn>
  if it is "bye":
    <do some stuff>

当这样做时,我几乎得到了我想要的东西:

  1. 接收"hello""bye"个阻止,直到我拥有其中任何一个。
  2. 我可以使用无限数量的条款来匹配消息
  3. 我已将循环行为提取到阻塞的future中 每次我评估cond时都有很好的副作用 我确定我有一个匹配的消息,不必担心重试。
  4. 我真正想要的一件事,但无法想象如何实现,就是在这种情况下的未来不会旋转。就目前而言,它会不断消耗掉流经队列的宝贵CPU资源,而从未接收到它正在寻找的消息之一可能是完全正常的。

    也许放弃LinkedBlockedQueue并将其换成具有方法的数据结构是有意义的,比如,getEither(List<E> oneOfThese)会阻塞,直到其中一个元素可用为止。

    我有另一种想法,这是我在Java中可能做到的一种方式,如果队列中没有任何元素在队列中,则在队列上进行上述getEither()操作,调用wait() 。当另一个线程将消息放入队列时,我可以调用notify(),以便每个线程将根据他想要的消息列表检查队列。

    示例

    以下代码运行正常。但是,它有旋转问题。这基本上是我想要实现的一个非常基本的例子。

    (def queue (ref '()))
    
    (defn contains-element [elements collection]
      (some (zipmap elements (repeat true)) collection))
    
    (defn has-element
      [col e]
      (some #(= e %) col))
    
    (defn find-first
             [f coll]
             (first (filter f coll)))
    
    ; This function is blocking, which is what I want.
    ; However, it spins and thus used a LOT of cpu,
    ; whit is *not* what I want..
    (defn get-either
      [getthese queue]
      (dosync
        (let [match   (first (filter #(has-element getthese %) @queue))
              newlist (filter #(not= match %)  @queue)]
    
          (if (not (nil? match))
            (do (ref-set queue newlist)
                match)
            (Thread/sleep 500)
            (recur)))))
    
    (defn somethread
      [iwantthese]
      (let [element (get-either iwantthese queue)
            wanted  (filter #(not= % element) iwantthese)]
        (println (str "I got " element))
        (Thread/sleep 500)
        (recur wanted)))
    
    (defn test
      []
      (.start (Thread. (fn [] (somethread '(3 4 5)))))
    
      (dosync (alter queue #(cons 1 %)))
      (println "Main: added 1")
      (Thread/sleep 1000)
    
      (dosync (alter queue #(cons 2 %)))
      (println "Main: added 2")
      (Thread/sleep 1000)
    
      (dosync (alter queue #(cons 3 %)))
      (println "Main: added 3")
      (Thread/sleep 1000)
    
      (dosync (alter queue #(cons 4 %)))
      (println "Main: added 4")
      (Thread/sleep 1000)
    
      (dosync (alter queue #(cons 5 %)))
      (println "Main: added 5")        
      )
    

    任何提示?

    (如果有人注意到,是的,这就像演员一样,目的是为了学术目的而在Clojure中实现)

2 个答案:

答案 0 :(得分:1)

你需要2个队列而不是1个队列:传入队列和#34;死信&#34;队列。

  1. A&#34;线程&#34;应该以阻塞方式从Linked队列中读取(LinkedBlockingQueue.take(),core.async/<!或使用代理)。
  2. 如果消息不符合任何条款:
    1. 将消息放入死队列结束
    2. 转到1.
  3. 如果消息与子句匹配:
    1. 运行子句工作
    2. 对于死队列中的每条消息,匹配子句,删除匹配的子句。
    3. 转到1。
  4. 请参阅下面的两个实现。

    <强>剂

    特工与演员非常相似,只有&#34;区别在于您向演员发送数据/消息,但您将功能发送给代理。可能的实现方式是:

    (defn create-actor [behaviour]
      (agent {:dead-queue [] 
              :behaviour behaviour}))
    

    dead-queue将包含与任何子句都不匹配的消息。这基本上就是你的队列结束&#34;。 behaviour应该是match-fn到fn的一些map / vector来运行。在我的特定实现中,我选择了一个映射,其中键是要匹配的元素,值是新项匹配时要运行的fn:

    (def actor (create-actor {3 println
                              4 (partial println "Got a ")
                              5 #(println "Got a " %)}))
    

    您可能需要更复杂的behaviour数据结构。唯一重要的是知道元素是否被处理,所以你知道元素是否必须转到死队列。

    向演员发送消息:

    (defn push [actor message]
      (send actor
            (fn [state new-message]
              (if-let [f (get-in state [:behaviour new-message])]
                (do
                  (f new-message)
                  state)
                (update-in state [:dead-queue] conj new-message)))
            message))
    

    因此,如果behaviour上存在匹配项,则会立即处理该消息。如果不是,则将其存储在死队列中。如果您希望behaviours不是纯函数,则可以在处理新消息后尝试匹配/处理死队列中的所有消息。在此示例实现中,这是不可能的。

    我们可以更改actor的behaviour以使死队列中的消息有机会被处理:

    (defn change-behaviour [actor behaviour]
      (send actor
            (fn [state new-behaviour]
              (let [to-process (filter new-behaviour (:dead-queue state))
                    new-dead-queue (vec (remove (set to-process) (:dead-queue state)))]
                (doseq [old-message to-process
                        :let [f (get new-behaviour old-message)]]
                  (f old-message))
                {:behaviour new-behaviour
                 :dead-queue new-dead-queue}))
            conds))
    

    使用它的一个例子:

    (push actor 4)
    (push actor 18)
    (push actor 1)
    (push actor 18)
    (push actor 5)
    (change-behaviour actor {18 (partial println "There was an")})
    

    基于core.async的相同解决方案:

    (defn create-actor [behaviour]
      (let [queue (async/chan)]
        (async/go-loop [dead-queue []
                        behaviour behaviour]
        (let [[type val] (async/<! queue)]
          (if (= type :data)
            (if-let [f (get behaviour val)]
              (do
                (f val)
                (recur dead-queue behaviour))
              (recur (conj dead-queue val) behaviour))
            (let [to-process (filter val dead-queue)
                  new-dead-queue (vec (remove (set to-process) dead-queue))]
              (doseq [old-msg to-process
                      :let [f (get val old-msg)]]
                (f old-msg))
              (recur new-dead-queue val)))))
      queue))
    
    (defn push [actor message]
      (async/go
        (async/>! actor [:data message])))
    
    (defn change-behaviour [actor behaviour]
      (async/go
        (async/>! actor [:behaviour behaviour])))
    

答案 1 :(得分:0)

您是否考虑过使用core.async?它以轻量级的方式提供您所需的功能。