lparallel is a multithreading library for common lisp, it is useful because the ANSI CL standard doesnt have multithreading it is available via quicklisp:
(ql:quickload :lparallel)
to get the number of cores we have to use the serapeum
package:
(ql:quickload :serapeum)
usage:
(serapeum:count-cpus)
16
in the context of lparallel, a kernel is an abstract entity that schedules and executes tasks. the lparallel kernel API is meant to describe parallelism in a generic manner.
the implementation uses a group of worker threads. it is intended to be efficiency-wise comparable to (or faster than) similar hand-rolled solutions while also providing full condition handling and consistency checks. all higher-level constructs in lparallel are implemented on top of the kernel.
kernel-related operations are applied to the current kernel, stored in kernel. a kernel is typically created with
(setf lparallel:*kernel* (lparallel:make-kernel <N>))
where N
is the number of worker threads, which ideally would be equal to the number of cores obtained using (serapeum:count-cpus)
.
in most circumstances a kernel should exist for the lifetime of the Lisp process. multiple kernels are possible, and setting the current kernel is done in the expected manner by dynamically binding kernel (with let
, for example).
a task is a function designator together with arguments to the function. to execute a task, (1) create a channel, (2) submit the task through the channel, and (3) receive the result from the channel.
(let ((channel (lparallel:make-channel))) (lparallel:submit-task channel '+ 3 4) (lparallel:receive-result channel))
7
a channel is simply a means to communicate with the kernel
multiple tasks may be submitted on the same channel, though the results are not necessarily received in the order in which they were submitted. receive-result
receives one result per call.
some examples:
(let ((channel (lparallel:make-channel))) (lparallel:submit-task channel '+ 3 4) (lparallel:submit-task channel (lambda () (+ 5 6))) (list (lparallel:receive-result channel) (lparallel:receive-result channel)))
11 | 7 |
(let ((channel (lparallel:make-channel))) (lparallel:submit-task channel #'sleep 1) (lparallel:submit-task channel (lambda () (sleep 3) (+ 7 6))) (lparallel:submit-task channel (lambda () (+ 4 6))) (print (lparallel:receive-result channel)) (print (lparallel:receive-result channel)) (print (lparallel:receive-result channel)))
10 NIL 13
this would take full advantage of 3 cores if the kernel was created with 3+ workers (dont run it!):
(let ((channel (lparallel:make-channel))) (lparallel:submit-task channel (lambda () (loop))) (lparallel:submit-task channel (lambda () (loop))) (lparallel:submit-task channel (lambda () (loop))) (print (lparallel:receive-result channel)))
note that a kernel will not be garbage collected until end-kernel
is called.
(lparallel:end-kernel)
plet
plet might be the simplest construct that lparallel offers for asynchronous computation, consider the following example:
(lparallel:plet ((a (progn (sleep 1) 9))) (print "this runs instantly") (print a)) ; => 9
once my-slow-function
is done running, the returned value, 9, will be in a
and the form (print a)
would be executed, but until then, execution hangs on (print a)
, notice that the first print statement is executed instantly because it doesnt depend on a variable (the variable a
, in this case) that was defined using plet
, which depends on a task to finish executing
although notice that, the whole sexp does pause and doesnt execute asynchronously, consider this example:
(lparallel:plet ((a (progn (sleep 1) 9))) (format t "hi~%") (format t "~A~%" a)) (format t "~A~%" 10)
hi 9 10
the next sexp after plet
doesnt run until plet
is done.
promise
promises and futures are also useful
a promise
is a "promise" that needs to be fulfill
'ed, fullfilledp
checks whether a promise has been fulfilled, fulfill
turns a promise into a fulfilled one, and attaches the second argument as its return value, force
returns the value returned by a promise
example:
(let ((p (lparallel:promise))) (lparallel:fulfilledp p) ; => nil (lparallel:fulfill p 3) (lparallel:fulfilledp p) ; => t (lparallel:force p)) ; => 3
a future
is a promise
which is fulfilled in parallel, it takes forms to execute asynchronously, fulfilledp
is used to check whether a future is done executing the forms, and force
is used to get the result of the execution, if the promise (the future) is not yet fulfilled (hasnt done executing), the call to force
block execution until it is, example:
(let ((f (lparallel:future (sleep 0.2) (+ 3 4)))) (lparallel:fulfilledp f) ; => nil (sleep 0.4) (lparallel:fulfilledp f) ; => t (lparallel:force f)) ; => 7