Functions to run multiple tasks (such as mutations and fitness tests) on multiple threads.
This module makes use of Bordeaux Threads. See the documentation here: https://trac.common-lisp.net/bordeaux-threads/wiki/ApiDocumentation
nil.
software
instance, but could be anything). The task can be used to
customize how to process the object after fitness testing
(basically a completion routine) and to customize how it
spins off child Jobs.
A task is an operation to be performed by the multi-threaded
task-runner. A task can be customized by the client to generate
a job (child series of tasks) by implementing the task-job method,
and the code to be performed when processing a task is defined by the
process-task method.
A job is a Lisp function, which takes no arguments, and which will
produce a task each time it is called, or nil when all of its tasks are
complete. Think of a job as a lazy sequence of task.
task-runner-jobs is a stack of jobs. Worker threads will call the
first job on the stack, and process the task returned.
A task may add 1 or more jobs to the top of the stack, causing worker threads to immediately start processing those jobs since they are now higher on the stack and therefore have priority over other tasks.
When the jobs stack is empty/NIL, then all worker threads will exit.
(setf *runner* (run-task (make-instance 'single-cut-all :object *orig*)
10))
;; When (task-runner-worker-count *runner*) = 0,
;; it means all threads are finished.
|
(defmacro task-map (num-threads function sequence)
"Run FUNCTION over SEQUENCE using a `simple-job' `task-job'."
(with-gensyms (task-map task-item)
`(if (<= ,num-threads 1)
(mapcar ,function ,sequence) ; No threading.
(progn ; Multi-threaded implementation.
(defclass ,task-map (task) ()) ; Task to map over SEQUENCE.
(defclass ,task-item (task) ()) ; Task to process elements.
(defmethod task-job ((task ,task-map) runner)
(declare (ignore runner))
(let ((objs (task-object task))) ; Enclose SEQUENCE for fn.
(lambda () (when objs ; Return nil when SEQUENCE is empty.
;; Return a task-item whose task-object run
;; FUNCTION on the next element of SEQUENCE.
(make-instance ',task-item :object
(curry ,function (pop objs)))))))
(defmethod process-task ((task ,task-item) runner)
;; Evaluate the task-object for this item as created in
;; the task-job method above. Save the results.
(task-save-result runner (funcall (task-object task))))
(task-runner-results ; Return results from the results obj.
;; Create the task-map object, and run until exhausted.
(run-task-and-block
(make-instance ',task-map :object ,sequence)
,num-threads))))))
|
The above example uses the tasks API to implement a simple parallel map spread across a configurable number of workers. When more than one worker thread is requested the following objects and methods are created to implement the parallel map.
task-map task is created to hold the sequence.
task-job method is defined for this task-map. This method
returns a function which has access to the sequence in a
closure. The function will continually pop the first element
off the top of the sequence and wrap it in a task-item object to
be returned until the sequence is empty at which point the
function returns nil causing all worker threads to exit.
task-item task is created to hold tasks for every item in
the sequence.
process-task method is defined for this task-item. This
method evaluates the function stored in the task-object of this
task-item and saves the result into the task runner’s results.
Finally, with the above objects and methods defined, the task-map
macro wraps the sequence into a task-map task object and passes
this to the run-task-and-block function yielding a runner and the
contents of that runner are extracted and returned using the
task-runner-results accessor.
See the actual implementation of task-map in the sel/utility
package for a more efficient implementation which doesn’t use a
macro or require new objects and methods to be defined on the fly.