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.