具有数据队列的多线程环境的Eventloop设计

时间:2013-09-09 00:21:21

标签: multithreading common-lisp sbcl

问题的描述我目前正在尝试找到一个lispy /工作解决方案:

作业队列提供一组相等(通过其代码)的线程,其中包含他们应该处理的任务。如果队列为空,则线程应等到新条目生成,但我还想提供干净的关闭。因此,即使在等待队列时,母线程也必须设置一些变量/来调用线程并告诉它们关闭。他们不直接遵守的唯一原因应该是线程当前正在评估任务,因此忙碌/无法在任务完成之前进行干净关闭。

我目前有两个解决方案我不相信:

(defparameter *kill-yourself* nil)

(defparameter *mutex* (sb-thread:make-mutex))

(defparameter *notify* (sb-thread:make-waitqueue))

#|the queue is thread safe|#
(defparameter *job-queue* (make-instance 'queue))


(defun fill-queue (with-data)
   (fill-stuff-in-queue)
   (sb-thread:with-mutex (*mutex*)
     (sb-thread:condition-notify *notify*)))


#|solution A|#
(with-mutex (*mutex*)
  (do ((curr-job nil))
      (*kill-yourself* nil)
    (if (is-empty *job-queue*)
    (sb-thread:condition-wait *notify* *mutex*)
    (progn
      (setf curr-job (dequeue *job-queue*))
      (do-stuff-with-job)))))


#|solution B|#
(defun helper-kill-yourself-p ()
  (sb-thread:with-mutex (*mutex*)
     *kill-yourself*))

(do ((job (dequeue-* *job-queue* :timeout 0) 
      (dequeue-* *job-queue* :timeout 0)))
        ((if (helper-kill-yourself-p)
         t
                 (sb-thread:with-mutex (*mutex*)
                     (sb-thread:condition-wait *notify* *mutex*)
                     (if (helper-kill-yourself-p)
                          t
                          nil)))
         (progn
           nil))
     (do-stuff-with-job))

两个do循环都可以用来启动线程。但是如果存在多个线程(因为互斥锁会阻止任何并行操作发生),A就不会真正起作用,并且B解决方案看起来很脏,因为有可能存在提取作业为零的边框。此外,我并不确定停止条件,因为它太长并且似乎很复杂。

实现一个(do)循环的正确方法是什么,该循环对队列提供的数据起作用,只要它应该并且只要没有新数据就能够睡眠,只要它不应该关闭?最后但并非最不重要的是,必须能够在无限量的多个并行线程中使用此(do)循环。

2 个答案:

答案 0 :(得分:3)

解决方案A

是的,你对解决方案A是正确的,互斥不会让工作成为现实 并行执行。

解决方案B

我认为do循环不适合这项工作。特别是在你的 代码有可能从队列和线程中提取作业 将退出而不执行它。这种情况是可能的,因为你出列了 在应该终止检查之前。另外,因为您在job的变量中定义了do 阻止您忽略从dequeue返回的多个值,这也很糟糕 因为你无法有效地检查队列是否为空。同样在场景中 在哪里检查线程是否应该停在do end-test-form中 获取*mutex*两次,检查线程是否应该停止并出列(或者 你可以发明一个奇怪的结束测试形式,它将完成循环体的工作。)

所以,说完了,你必须将所有代码放在do的主体内 将vars和end-test留空。这就是为什么我认为loop在这方面更好 情况下。

如果你必须使用do循环,你可以轻松地将loop身体包裹在其中,例如 (do nil (nil nil) *loop-body*)

我的解决方案

(require :sb-concurrency)
(use-package :sb-concurrency)
(use-package :sb-thread)

(defparameter *kill-yourself* nil)
(defparameter *mutex* (make-mutex))
(defparameter *notify* (make-waitqueue))
#|the queue is thread safe|#
(defparameter *job-queue* (make-queue :name "job-queue"))
(defparameter *timeout* 10)
(defparameter *output-lock* (make-mutex))

(defun output (line)
  (with-mutex (*output-lock*)
    (write-line line)))

(defun fill-queue (with-data)
  (enqueue with-data *job-queue*)
  (with-mutex (*mutex*)
    (condition-notify *notify*)))

(defun process-job (thread-name job)
  (funcall job thread-name))

(defun run-worker (name)
  (make-thread
    (lambda ()
      (output (format nil "starting thread ~a" name))
      (loop (with-mutex (*mutex*)
              (condition-wait *notify* *mutex* :timeout *timeout*)
              (when *kill-yourself*
                (output (format nil "~a thread quitting" name))
                (return-from-thread nil)))
            ;; release *mutex* before starting the job,
            ;; otherwise it won't allow other threads wait for new jobs

            ;; you don't want to make 2 separate calls (queue-empty-p, dequeue)
            ;; since inbetween queue can become empty
            (multiple-value-bind (job has-job) (dequeue *job-queue*)
              (if has-job
                (process-job name job)))))
    :name name))

(defun stop-work ()
  (with-mutex (*mutex*)
    (setf *kill-yourself* t)
    (condition-broadcast *notify*)))

(defun add-job (job)
  ;; no need to put enqueue in critical section
  (enqueue job *job-queue*)
  (with-mutex (*mutex*)
    (condition-notify *notify*)))

(defun make-job (n)
  (lambda (thread-name)
    (loop for i upto 1000 collecting i)
    (output (format nil "~a thread executes ~a job" thread-name n))))

(defun try-me ()
  (run-worker "worker1")
  (run-worker "worker2")
  (loop for i upto 1000 do
        (add-job (make-job i)))
  (loop for i upto 2000 collecting i)
  (stop-work))

在REPL中调用try-me应该会给你类似下面的输出

starting thread worker1
worker1 thread executes 0 job
worker1 thread executes 1 job
worker1 thread executes 2 job
worker1 thread executes 3 job
starting thread worker2
worker2 thread executes 4 job
worker1 thread executes 5 job
worker2 thread executes 6 job
worker1 thread executes 7 job
worker1 thread executes 8 job
...
worker2 thread executes 33 job
worker1 thread executes 34 job
worker2 thread executes 35 job
worker1 thread executes 36 job
worker1 thread executes 37 job
worker2 thread executes 38 job
0
worker1 thread executes 39 job
worker2 thread quitting
worker1 thread quitting

P.S。我无法找到旧SBCL的文档,所以我留下了翻译 到您的旧版API。希望它会有所帮助。

编辑类解决方案

在对(已删除)答案的评论中,我们发现您需要一个用于事件循环的类。我想出以下

(defclass event-loop ()
  ((lock
     :initform (make-mutex))
   (queue
     :initform (make-waitqueue))
   (jobs
     :initform (make-queue))
   (stopped
     :initform nil)
   (timeout
     :initarg :wait-timeout
     :initform 0)
   (process-job
     :initarg :process-job
     :initform #'identity)
   (worker-count
     :initarg :worker-count
     :initform (error "Must supply worker count"))))

(defmethod initialize-instance :after ((eloop event-loop) &key)
  (with-slots (worker-count timeout lock queue jobs process-job stopped) eloop
    (dotimes (i worker-count)
      (make-thread
        (lambda ()
          (loop (with-mutex (lock)
                  (condition-wait queue lock :timeout timeout)
                  (when stopped
                    (return-from-thread nil)))
                ;; release *mutex* before starting the job,
                ;; otherwise it won't allow other threads wait for new jobs

                ;; you don't want to make 2 separate calls (queue-empty-p, dequeue)
                ;; since inbetween queue can become empty
                (multiple-value-bind (job has-job) (dequeue jobs)
                  (if has-job
                    (funcall process-job job)))))))))

(defun push-job (job event-loop )
  (with-slots (lock queue jobs) event-loop
    (enqueue job jobs)
    (with-mutex (lock)
      (condition-notify queue))))

(defun stop-loop (event-loop)
  (with-slots (lock queue stopped) event-loop
    (with-mutex (lock)
      (setf stopped t)
      (condition-broadcast queue))))

您可以像这样使用

> (defparameter *el* (make-instance 'event-loop :worker-count 10 :process-job #'funcall))
> (defparameter *oq* (make-queue))
> (dotimes (i 100)
    (push-job (let ((n i)) (lambda ()
                             (sleep 1)
                             (enqueue (format nil "~a job done" n) *oq*))) *el*))

它使用sb-thread:queue作为输出以避免奇怪的结果。虽然这很有效,但您可以在REPL中检查*oq*

> *oq*
#S(QUEUE
:HEAD (SB-CONCURRENCY::.DUMMY. "7 job done" "1 job done" "9 job done"
       "6 job done" "2 job done" "11 job done" "10 job done" "16 job done"
       "12 job done" "4 job done" "3 job done" "17 job done" "5 job done"
       "0 job done" "8 job done" "14 job done" "25 job done" "15 job done"
       "21 job done" "28 job done" "13 job done" "23 job done" "22 job done"
       "19 job done" "27 job done" "18 job done")
:TAIL ("18 job done")
:NAME NIL)

答案 1 :(得分:0)

我使用了库chanl,它提供了一个消息队列机制。当我想要线程关闭时,我只是将关键字:stop发送到队列。当然,在队列中:stop之前的所有事情都已完成之前,这并不会停止。如果要提前停止,可以创建在数据队列之前检查的另一个队列(控制队列)。