|
This is a pre-print extract from the forthcoming O'Reilly book Lisp Outside the Box. Contents are subject to change as the book's production progresses. Feedback is most welcome, either in private by or in public by responding to the blog entry which announced this chapter. Table of Contents In this chapter we’re going to take a look at the control of multiple execution strands within a single Lisp image. We’ll cover their creation and management, communication between them, shared and private state, interlocks, and critical sections. The central message is of the chapter is that Lisp implementations typically support much or all of this at a high level, leaving you to get on with developing your application without having to know too much about the innards of how threads work. The terms used to describe concurrency are not consistent between Lisp implementations. Concurrency is typically implemented by multiple OS (or native) threads but that’s not universally the case. On the other hand it’s almost certain that your Lisp image is running within a single OS process. However in this chapter I’m going to reflect common usage within the Lisp community and refer to:
In some Lisps processes can run in parallel on separate cores—see the notes in the section called “Concurrent Processes” towards the end of the chapter. The ANSI standard does not hint at any of the concepts underlying MP. Nevertheless these days a Lisp implementation which did not support MP would be regarded as somewhat deficient. Certainly all the Lisps discussed in this book support it. MP is frequently mooted as a prime candidate for standardization; although that has never happened the features discussed in this chapter can be regarded as “core” and you’d expect to see most of them supported in some form or other by any MP system. These forms will be recognizably similar to each other and you won’t have much trouble adapting from one to another. Whichever Lisp you’re using, read the manual. You should also be aware that a number of portable libraries are available which cover much of this common ground. You’ll find these listed at:
The examples in this chapter make extensive use of This chapter is an adaptation for ACL of material which I contributed a few years back to the Common Lisp Cookbook:
That piece drew its examples from the LispWorks MP implementation of the time (version 4.2) and LW users might prefer to refer to that as well. (But note that it doesn’t mention some important features such as concurrent MP which we’ll come to later on.) The first question to consider is: why bother with MP? Sometimes there’s no need to bother: your application is so straightforward that you need not concern yourself with processes at all. But in many other cases it’s difficult to imagine how a sophisticated application can be written without MP. How will you get it to respond to events in real time? For example:
It’s fairly likely when you’re working with processes that—every now and again—one of them will run away from you and either fall motionless at your feet or make a CPU-intensive break for freedom. If you’ve managed to get your Lisp tangled you may be short on options for halting a runaway process, short of terminating the Lisp session altogether. So before you go any further, you should learn how to kill processes in your Lisp development environment. This is easiest if you’re working in an IDE.
TipIn either case, open the dialog before you get into trouble. ExerciseIn the ACL IDE, type (mp:process-run-function "foo" (lambda () (loop))) into a listener and use the process dialog to kill the process. If you aren’t in an IDE, let’s hope you can find (or
fire up) a listener which isn’t totally hosed and into which
you can type your implementation’s equivalent of a
TipYou might want to think about designing your application so that these situations can be dealt with cleanly by the end user. We’ll use ACL for the examples. Typically, you don’t have to do anything to obtain, load or start up your Lisp’s MP system: if you’re running the Lisp then you’re running MP with it. This isn’t always the case (e.g. LW on Unix workstations). If you’re at all uncertain, read the manual for your implementation and use whatever facility has been provided for starting MP in your image. TipIn ACL, MP is started lazily the first time any process operation is invoked. If you’re using the IDE, this will happen long before you get to the Lisp prompt. ACL exports its MP utilities from the (use-package :mp) The general idea is simple enough: pass a function to
In the following example, you create a process called
(defvar *foo* 0) (defun foo () (incf *foo*)) (process-run-function "Incrementing *foo*" 'foo) => #<PROCESS Incrementing *foo*(3) @ #x40689562> *foo* => 1 ;; By the time you've typed this, the new process has almost certainly ;; terminated. Note that its printed representation has (subtly) changed. ** => #<PROCESS Incrementing *foo* @ #x40689562>
To kill a process programmatically (typically: from another
process), call
CL-USER(1):
TipThe If you didn’t already have a handle on the process, use
(process-name-to-process name &key abbrev (error t)) If Note the difference between:
(let ((table (make-hash-table)))
(loop for i from 1 to 20 do
(process-run-function "One closure"
(lambda () (setf (gethash i table) t))))
table)
and:
(let ((table (make-hash-table)))
(loop for i from 1 to 20 do
(process-run-function "Twenty different bindings"
(lambda (j) (setf (gethash j table) t))
i))
table)
ExerciseRun these forms in the REPL; they both return empty tables. When
you evaluate In the first case, all twenty processes share the same closure
variable ExerciseGet two or three new processes running simultaneously, and convince yourself they’re all there. Every process in your Lisp system has its own private state. The following aspects of state will vary between different processes:
On the other hand, the following are globally set rather than bound in a Lisp system and so will not vary between processes:
To see how per-process variable bindings can lead you astray, consider the following examples: CL-USER(1): In both lines 1 and 2, To create a binding which will be present at the beginning of a
new process, specify an
CL-USER(10):
TipACL also provides ExerciseExplain from examination of An obvious way to test whether processes are behaving as you imagine they ought is to get them to print messages to the listener. For example, you might feel justified in trying something like:
CL-USER(12):
Where indeed is your output? The answer is that your new process
has a different
CL-USER(13):
Exercise(In the ACL Windows IDE) open the Windows console (i.e.
ExerciseThe following behaves differently to the
(process-run-function "Why not here?"
'print
*standard-output*
#.*standard-output*)
An entirely more annoying situation occurs when two or more
processes share the same CL-USER(1): Two processes (the original Lisp listener and your own
The best approach to this particular hazard is to avoid it altogether. If you’re compelled to debug a multi-processing application at the command line, consider using SLIME (Chapter 18, SLIME). In all the above examples, a process is created to run a simple function and then halt. In a typical application at least some of your processes will run an event loop of some sort. An event loop is a function which repeatedly waits for an event external to that process to occur. When an event is noticed, it is dispatched (maybe to another process) for processing and the event loop cycles back to its waiting state. The “other process” here might already exist (perhaps running an event loop of its own) or might be created specifically to perform this task (i.e. handle a single event) and then terminate. It might be tempting to construct an event loop using
(defun bogus-event-loop ()
(loop
(sleep 1) ; THIS IS WRONG
(when (something-has-happened)
(act-on-that-thing))))
(process-run-function "Bogus event loop"
'bogus-event-loop)
This is a poor choice, because you’re condemned to wait
for the Consider instead:
(defun improved-event-loop ()
(loop
(process-wait "Waiting for something to happen"
'something-has-happened)
(act-on-that-thing)))
The arguments to TipKeep the wait-function simple: don’t use it to perform
expensive calculations and be wary of using it to mess with MP. In
particular, don’t call TipIf you’re waiting for input on an external stream, the
call ExerciseRead the ACL documentation on There’s often a risk that the wait-function will never
return true, meaning that the waiting process might wait forever.
If you don’t want that to happen, consider instead using
(process-wait-with-timeout reason timeout wait-function &rest arguments) where
(defun check-wait-for-file (file timeout)
(or (probe-file file)
(process-wait-with-timeout (format nil "Waiting for \"~a\"" file)
timeout
'probe-file file)
(error "File \"~a\" still not found" file)))
TipWait-functions are run when the system attempts to switch into the waiting processes. If there are several active processes, Lisp will switch between them many times per second. How often that is depends on implementation and OS platform: if you need to know your system’s granularity, experiment! ExerciseAdapt Related to (with-timeout (seconds &rest timeout-forms) &body body) The body is run as an implicit An important mode of communication between processes is the interrupt: a message sent by one process to another, to ask that process to perform some task. This might come in handy if:
The safest approach is to use a message queue. One process “owns” the queue and other processes can “post” tasks to it; the owning process deals with them one at a time when it’s good and ready to do so. ACL supports this paradigm with the class
;; Call (event-handler) to set up a handling process which processes events
;; from its queue, always completing one before going on to the next. Use
;; handle-in-process below to add events to the queue.
(defun event-handler ()
(process-run-function "Event handler"
'handle-events))
(defun handle-events ()
(let ((queue (make-instance 'queue)))
;; Put the queue where client processes can find it
(setf (getf (process-property-list *current-process*) 'queue)
queue)
(loop ;; Wait for a request to arrive (when one does the queue
;; will stop being empty)
(process-wait "Waiting for request in queue"
(lambda () (not (queue-empty-p queue))))
;; Now we know a request has arrived, peel it off the queue
;; and run it
(let ((request (dequeue queue)))
(funcall request)))))
(defun handle-in-process (process function &rest arguments)
(let* ((queue (getf (process-property-list process) 'queue))
(no-result (cons nil nil))
(result no-result))
;; Send function to other process
(enqueue queue
(lambda ()
(setf result (apply function arguments))))
;; Wait for the value of RESULT to change, and then return it
(process-wait (format nil "Waiting for response from ~a"
(process-name process))
(lambda () (not (eq result no-result))))
result))
ExerciseWhat is interesting about the value of TipLispWorks uses objects called When you’re working on a data structure you sometimes need to be certain that whatever you’re doing to it will be complete before any other process touches that data. Such an operation is referred to as a critical section, or as being atomic. You cannot assume that
any operator is atomic unless your implementation’s
documentation has explicitly told you that this is the case. Some
implementations support atomic versions of common operators, for
example ExerciseWhat might happen if two different processes were to
Traditionally Lisp implements MP by emuluating multi-tasking. When it comes down to it, only one task is being performed at a time. Forthcoming releases of both ACL and LW will support symmetric multiprocessing: concurrent threads running in parallel on multiple cores. This will allow for considerable performance gains but will have implications for the enforcement of atomicity. Both implementations will introduce new operators for controlling concurrency as well as a number of primitives for atomic read-modify-write operations. Some other implementations already provide symmetric MP, albeit with less sophisticated controls. In all cases, you are strongly advised to read your documentation with regard to concurrency. For ACL users an upgrade note is available here:
Sometimes it’s important to control access to a resource, so that only one process is operating on it at a time. You could do this is by restricting access from your code to that resource and only allowing one specialized process to touch it, using an event handler to enforce this. However there are two potential drawbacks to this approach:
A lock is a simple Lisp object, which can be held by no more than one process at a time. A process attempting to hold a lock which is already in use will hang (i.e. be forced to wait) until the lock is freed. In the following example, the locking mechanism is reduced to its most minimal form.
;; Create a lock and remember it.
(defvar *lock* (make-process-lock))
(defun use-resource-when-free (stream)
;; Callers must wait at this point for the lock to be freed,
;; before they can proceed with the body of this form.
(with-process-lock (*lock*)
(use-resource-anyway stream)
;; When we exit this form, the lock is freed automatically
;; and other processes will be allowed to claim it.
))
(defun use-resource-anyway (stream)
(let ((name (process-name *current-process*)))
(format stream "~&Starting ~a." name)
(sleep 1)
(format stream "~&Ending ~a." name)))
(defun test (lock-p)
(let ((run-function (if lock-p
'use-resource-when-free
'use-resource-anyway)))
(dotimes (id 3)
(process-run-function (format nil "process ~a" id)
run-function *standard-output*))))
TipYour implementation may also support other process-friendly
structures, such as Recall that AllegroCache connections are not thread-safe: you should not allow two processes to “use the same connection” at the same time. We’re now in a position to consider potential solutions to this problem. ExerciseOne option is to use a lock to guard all unsafe operations.
Implement this, in part at least. You won’t want dozens of
Another option is to maintain a pool of several connections from which processes can request a connection and to which they can be guaranteed to return it:
(defun process-run-with-connection (name-or-options function &rest args)
(process-run-function name-or-options
(lambda ()
;; Per-process binding of *allegrocache* keeps
;; the connection private
(let* ((*allegrocache* (request-connection)))
(unwind-protect
(apply function args)
(return-connection *allegrocache*))))))
;; Use of a QUEUE ensures atomicity (we don't want two processes picking
;; up the same connection)
(defvar *connections* (make-instance 'queue))
(defun request-connection ()
(or (dequeue *connections*)
(open-network-database ...)))
(defun return-connection (connection)
(enqueue *connections* connection))
ExerciseAdapt this example to enforce an upper limit on the number of connections. We should end this chapter with a word of caution. MP errors can be very hard to debug. Your only symptom may be “unexplainable” data corruption or “illogical” program flow. You may find the problem hard or impossible to reproduce, even though you have incontrovertible proof of it happening once. Any of this should point you to examine your MP usage. Pay attention to anecdotal evidence. I have in mind a race condition whose outcome flipped after one of my users defragmented his hard drive thus speeding up processes which accessed the disk. Ask yourself: “clearly this is impossible, but how could this have happened?”, “can I document the order on which I depend upon things happening?”, “is anything else unexpected happening?” |