Multi-Process Dolist
LispWorks Version 6 introduced several routines to make it easier to make Lisp programs take advantage of multiple processors, or multiple cores, on recent computers. This macro provides a dolist function which can be used to perform time consuming parts of a Lisp application in parallel. In addition, it provides a progress indicator to give feedback about the progress of the calculation:

Example
To use the macro simply replace a dolist form with mp-dolist:
(defun demo1 (n)
(mp-dolist (x *numbers* :title "Largest factor" :processes n)
(setf (car x) (divisor (car x)))))
This example modifies the list *numbers* to replace each number with its smallest factor. The parameter n specifies how many processes we want to use. We test it with a list of large numbers to factorize:
(defparameter
*numbers*
'((1000252000747) (1000264007623) (1000250007881) (1000250008229) (1000280016119)
(1000304021879) (1000292019107) (1000304022743) (1000304022143) (1000310024009)))
and this simple-minded routine to find the smallest prime factor of a number:
(defun divisor (n)
(let ((test 2))
(loop
(when (> (* test test) n) (return nil))
(when (zerop (rem n test)) (return test))
(incf test))))
When called with:
(time (demo 1))
the routine runs in a single process and gives the result:
Timing the evaluation of (DEMO1 1) Elapsed time = 4.943
Running it with:
(time (demo1 2))
runs it in two processes and gives the following result:
Timing the evaluation of (DEMO1 2) Elapsed time = 3.355
The definition
First we define the progress bar class:
(defclass progress-bar (capi:interface) ((bar :initarg :bar :accessor bar)))
The routine make-bar displays the progress bar, and provides a :destroy-callback function to cancel all the running processes if you click the Cancel button:
(defun make-bar (end semaphore &optional (title "Progress"))
(let ((bar (make-instance 'capi:progress-bar
:x 11 :y 40 :external-min-width 200
:external-min-height 12 :start 0 :end end))
(cancel (make-instance 'capi:push-button :text "Cancel"
:callback-type :interface :callback #'capi:destroy)))
(capi:display
(make-instance
'progress-bar
:bar bar :title title :owner (capi:convert-to-screen nil)
:destroy-callback
#'(lambda (interface)
(declare (ignore interface))
(let ((w (mp:semaphore-wait-count semaphore)))
(when (and w (plusp w))
(mp:semaphore-release semaphore :count w))))
:layout (make-instance 'capi:row-layout :adjust :center
:description (list bar cancel))))))
The routine update-bar is called to move the progress-bar slug:
(defun update-bar (progress value) (setf (capi:range-slug-start (bar progress)) value))
Finally here's the definition of mp-dolist:
(defmacro mp-dolist ((x list &key title (processes 1)) &body body)
"Executes the body with x equal to each item in list,
using 4 processes and with a progress bar."
(let ((l (gensym)) (n (gensym)) (p (gensym)))
`(let* ((,l ,list)
(semaphore (mp:make-semaphore))
(max (length ,l))
(completed (list -1))
(jump (ceiling max (* *progress-bar-length* 4)))
(bar (make-bar max semaphore ,title))
processes)
(unwind-protect
(progn
(dotimes (,p ,processes)
(push
(mp:process-run-function
"Dolist-mp" nil
#'(lambda (completed semaphore)
(loop
(let ((,n (sys:atomic-incf (car completed))))
(when (>= ,n max) (return))
(let ((,x (nth ,n ,l)))
(when (zerop (rem ,n jump)) (update-bar bar ,n))
,@body)))
(mp:semaphore-release semaphore))
completed semaphore)
processes))
(mp:semaphore-acquire semaphore :count (1+ ,processes)))
(map nil #'mp:process-kill processes)
(capi:destroy bar)))))
Using mp-dolist
As with the standard dolist macro, the body of a call to the mp-dolist macro:
(mp-dolist (x lst :title title :processes n) body)
is executed once with x bound to each element in the list lst. However, because the body is executed by different processes, we have to take care when accessing a shared resource.
The simplest approach is to lock accesses to a shared resource. For example, here's a version of the demo that pushes the results onto a list result, rather than modifying the original list of numbers, using a lock to prevent two processes from trying to update the list simultaneously:
(defun demo2 (n)
(let ((lock (mp:make-lock))
result)
(mp-dolist (x *numbers* :title "Largest factor" :processes n)
(let ((d (divisor (car x))))
(mp:with-lock (lock) (push d result))))
(nreverse result)))
Alternatively, we could use atomic instructions, but note that the atomic macros like atomic-incf don't work with lexical variables, so we have to make result a pointer to a list:
(defun demo3 (n)
(let ((result (list nil)))
(mp-dolist (x *numbers* :title "Largest factor" :processes n)
(sys:atomic-push (divisor (car x)) (car result)))
(nreverse (car result))))blog comments powered by Disqus
