$Revision: 5.0.2.8 $
The document introduction.htm provides an overview of the Allegro CL documentation with links to all major documents. The document index.htm is an index with pointers to every documented object (operators, variables, etc.) The revision number of this document is below the title. These documents may be revised from time to time between releases.
The os-threaded model will in the future be used on all platforms. Consequently, the stack-group facility in the non-os-threaded model will become unavailable. Code using it will not be portable to future versions of Allegro CL. |
1.0 Multiprocessing introduction
1.1 Data types
added to standard Common Lisp
1.2 stack-groups
(non :os-threads model)
2.0 Processes and profiling (both
models)
3.0 :os-threads
model thread-related variables and functions
3.1
Threads and processes in the :os-threads model
3.2 The
:os-threads model and foreign functions (:os-threads model)
3.3
Waiting for input from a stream (:os-threads model)
3.3.1
mp:process-wait vs mp:wait-for-input-available (:os-threads model)
4.0
Stack-group variables and functions in the non :os-threads model
4.1
Stack-group programming example (non :os-threads model)
4.2 Processes in
the non :os-threads model
4.3
Waiting for input from a stream (non :os-threads model)
4.3.1
mp:process-wait vs mp:wait-for-input-available (non :os-threads model)
5.0 Process functions and
variables (both models)
6.0 Processes and
their dynamic environments (both models)
6.1 Lisp
listeners and special variable bindings
7.0 Process locks (both models)
8.0 A simple example of multiprocessing
1.0 Multiprocessing introduction
Allegro CL has extensions to support coroutining and multiprocessing within a single executing Lisp images. The programming interface is based on similar features on MIT Lisp Machines, with which they are largely compatible.
All processes share the same Lisp address space, sometimes called the Lisp world. Each process has its own execution stack (i.e. "call" stack) and dynamic variable bindings. All processes share everything else in the Lisp world, include packages, streams, global function bindings, and global values of special variables. (The global value of a special variable is its outermost value, outside any dynamic binding.) The Allegro CL compiler, interpreter, top level, and other subsystems all lambda bind their necessary special variables, and Allegro CL code itself is reentrant. Therefore, multiple processes can correctly maintain any number of correct and independent ongoing computations simultaneously within the single Lisp world.
There are two implementations of multiprocessing in Allegro CL 5.0, the :os-threads model and the non :os-threads model. :os-threads appears on the *features* list of the implementation using the :os-threads model.
In the :os-threads model, each process within Lisp is implemented by an operating system thread and management tasks such as scheduling are done by the operating system. In the non :os-threads model, all processes are implemented internally by Lisp and one pseudo process in Lisp, called the scheduler, manages them.
Although there are many subtle differences between these two implementations, in actual practice it is very rare for actual application code to have any dependency on these differences. Most code developed on one will run without modification on the other. This has been verified during the porting of several preexisting multiprocess-intensive subsystems. |
Release 5.0 of Allegro CL on Windows uses the :os-threads model. Allegro CL on all Unix platforms uses the non :os-threads model. Earlier releases of Allegro CL on Windows did not implement multiprocessing. Earlier releases on Unix used the non :os-threads model, as they still do. It is expected over time that all versions will adopt the :os-threads model.
Both models are described in the document. Most operator names are common between the two models because the operators do highly analogous things. This makes porting code much easier. Still, there is sometimes a loss of transparency where the operators do somewhat different things. For example, mp:start-scheduler initiates multiprocessing within Lisp. In the non :os-threads model, the function initializes and starts the scheduler process. But in the :os-threads model there is no scheduler and so the function name is misleading. The function only performs the necessary initialization.We try to make the distinction clear by describing both the :os-threads and non :os-threads behavior when it differs and by (for example) preferring the neutral term initializing multiprocessing to starting the scheduler.
Sections which relate only to one or the other model are so labeled. Unfortunately, a large amount of information is repeated in sections specific to each model.
Most symbol names for multiprocessing are in the multiprocessing package, nicknamed mp. Programmers must preface symbols with the package prefix mp: or execute
(use-package :multiprocessing)
before the first reference to any multiprocessing function. Depending how Lisp is installed the multiprocessing module may not be loaded into the initial Lisp system. To guarantee it is available, execute the form
(require :process)
before calling any multiprocessing functions, or place this top-level form
(eval-when (:compile-top-level :load-top-level :execute) (require :process))
near the start of any source file which uses multiprocessing.
Multiprocessing adds user-visible data types (defstructs) are added to standard Common Lisp:
The non :os-threads model also adds a third data type, the stack-group, which implements a coroutining facility. Stack-groups are not available in the :os-thread implementation and should be considered deprecated, since in the future all implementations of Allegro CL will employ that model. This section may safely be skipped by programmers concerned only with new code.
A new stack-group is created with the mp:make-stack-group function. Each stack-group contains three component stacks which implement Lisp recursive functional evaluation. Although the programmer never needs to manipulate the component stacks of a stack-group, it is useful to understand them. The control stack maintains the function evaluation state including lexical bindings. The binding stack records the dynamic bindings of special variables. The fasl stack is used by the Lisp compiled-file loader. In addition to its component stacks, a stack-group has a name, state, initial function, and list of initial arguments. The name must be a string and is used only for documentation and when printing the stack-group object.
The interesting thing that can be done with a stack-group is to resume it. There are several ways to resume a stack-group, but before it is resumed for the first time it must be preset with an initial function and initial arguments. When the stack-group is first resumed, it applies the function to the argument list in the normal manner of Lisp evaluation. Unless the stack-group resumes some other stack-group, it runs until the initial function returns. It is worth emphasizing that the only way a stack-group switch ever happens is for the executing stack-group to complete or else actively to request some other stack-group to resume. A stack-group switch never happens automatically.
A stack-group resumes another by calling one of several functions for this purpose, for example, mp:stack-group-resume. When (and if) the resuming stack-group is itself resumed, it appears to it simply as though the call to mp:stack-group-resume has returned. The mp:stack-group-resume function requires two arguments. The first is the stack-group to resume. The second is an arbitrary Lisp object which is transmitted to the resumed stack-group. This object is returned as the value of the mp:stack-group-resume call in the resumed stack-group. The very first time a stack-group is resumed the transmitted value is ignored; instead the stack-group's initial function is applied to the initial arguments.
A stack-group object also remembers a resumer in its mp:stack-group-resumer slot. This slot will either contain nil or some other stack-group. This stack-group serves as a default stack-group to resume if, for example, the initial function of a stack-group completes. The slot name stack-group-resumer is traditional but something of a misnomer because the slot does not necessarily identify the stack-group that last resumed a stack-group.
These are the ways one stack-group can resume another:
The only way to change the stack-group-resumer slot of a stack-group, aside from an explicit setf, is to execute mp:stack-group-funcall. The mp:stack-group-resume and mp:stack-group-return functions never change the stack-group-resumer slot of either the resumed or the resuming stack-group. These identities may be useful to remember:
(stack-group-funcall sg v)
is equivalent to
(setf (stack-group-resumer sg) sys:*current-stack-group*)
(stack-group-resume sg v)
and
(stack-group-return v)
is equivalent to
(stack-group-resume (stack-group-resumer sg) v)
If a stack-group ever needs to resume its stack-group-resumer, and that value is nil, then as a special case the system will resume the stack-group in which Lisp initially starts execution.
A stack-group's state may be examined but not set by user code. It has four possible values:
A stack-group may be resumed if its state is either :awaiting-initial-call or :resumable. An error is signaled by an attempt to resume an :exhausted stack-group, or if an :active stack-group tries to resume itself. A stack-group (except the one currently running!) may be reinitialized at any time to :awaiting-initial-call using the mp:stack-group-preset function. However, doing so to a stack-group representing an active computation simply discards the computation without giving it a chance to clean up. In particular, unwind-protect cleanup bodies are not run. Note, however, that this is not very different from just discarding a :resumable stack-group, never allowing it to complete. This will also prevent execution of unwind-protect cleanup forms.
When first started, each stack-group is automatically provided bindings for several dynamic system variables, such as *evalhook* and *applyhook*, which are required if an error should ever invoke the debugger on that stack.
Stack-groups are the mechanism underlying Allegro CL's non :os-threads implementation of multiprocessing, i.e., running multiple quasi-simultaneous processes within a single Lisp world. They are a robust extension to the Common Lisp language. Nonetheless, users should consider alternatives to using multiple stack-groups as a coding technique within a single computation. First, stack-groups are not a standard feature of Common Lisp and their use is not portable. Second, they may be less efficient than other less-general mechanisms for maintaining state in a functional evaluation, such as closures. Switching stack-groups requires saving away all the special variable bindings on the current binding stack and then reestablishing all the saved bindings on the new binding stack. In the present implementation the active control stack must be in a fixed place (the hardware stack), so the current control stack must also be saved away in a vector inside the stack group and the new stack copied into place. All this overhead occurs with each stack group switch. Stack groups are therefor not appropriate for very fine-grained multiprocessing.
The time profiler in Allegro CL (documented in profiling.htm) collects data by periodically recording which functions are on the stack. The space profiler records all requests for new allocations. It is possible to set a flag in a process object that prevents sampling when that process is the current process. This permits more accurate data for time spent or space used by the process and code of interest.
When a new process is created, the value of the flag is set to its value in the current process. The predicates mp:profile-process-p (:os-threads, and non :os-threads for processes that run one stack group only) and mp:profile-stackgroup-p (non :os-threads) poll the profiling flag and can be used with setf to change its value.
Users familiar with the non :os-threads model of multiprocessing will find some functions, variables, etc. missing from the :os-threads model. Others are still meaningful in 5.0 and usually preserve their old names for backward-compatibility (e.g. symeval-in-stack-group), even where the old names are inaccurate. In the cases where a function, variable, etc. is no longer defined, the naming symbol has been removed so references will signal error. If you are porting code between :os-threads and non :os-threads versions, you may use #+:os-threads and #-:os-threads to conditionalize code.
The descriptions below provide only a brief introduction. Please follow the links to the individual description pages for details.
Name | Arguments | Notes |
sys:*current-stack-group* | [variable] | This name not descriptive: kept for compatibility. This variable always holds the thread that is currently executing. This variable should be treated as read only. |
mp:symeval-in-stack-group | symbol thread | Name not descriptive: kept for backward-compatibility. This
function returns two values. The first is the value of the special symbol in the given
thread (which may be the current thread). It only looks for actual bindings on the thread;
if the symbol has a global value but is not bound on the thread, the global value is not
returned. The second returned value describes the status of the binding. t is returned if the symbol is bound on the thread, nil if the symbol has no binding, and :unbound if there is a binding which has been subjected to makunbound. In the latter two cases, the first returned value is nil. |
mp:symeval-in-process | symbol process | Same as the badly named mp:symeval-in-stack-group function above, but with a better name. |
sys:global-symbol-value | symbol | This function returns two values. The first is the global value for the special variable named by symbol, ignoring any bindings on the current thread and the second is t if the variable has a value (in the sense of boundp). Otherwise the first value will be nil and the second will be the symbol :unbound. |
mp:profile-process-p | process | This function returns the value of the profile-process-p flag for the thread specified by process. If the value of the flag is non-nil, then the space and time profilers will collect samples when this process is executing. |
The following symbol names from the non :os-threads model are not present in the :os-threads model:
mp:make-stack-group
mp:stack-group-preset
mp:stack-group-resume
mp:stack-group-funcall
mp:stack-group-return
mp:stack-group-name
mp:stack-group-resumer
The process object implements the abstraction of independent processes running within the same Lisp world. Processes are implemented on top of OS threads; each active process is associated with a particular thread. Active process threads are managed by the Operating System. Thread objects are not directly accessible to Lisp code.
A process object is implemented as a structure. Some of its slots are meaningful to user code, but except for those explicitly noted they should be treated as read only.
Whether and when a process runs is controlled by several mechanisms. First the process' initial function and arguments must be specified, and then the process must be 'reset'. (The mp:process-preset function combines these operations.) Second, a process maintains two lists: its run-reasons and its arrest-reasons. These lists can contain Lisp objects of any type (whether or not each list is empty is the only relevant issue). For the OS to consider a process for execution, it must have at least one object on its run-reason list, and no objects on its arrest-reason list. Finally, a process that needs to wait for some arbitrary condition does so using the mp:process-wait function. This function specifies a function and arguments. When the OS considers a waiting process for running, it causes the wait-function to be applied to the wait-arguments (in the process environment). If a non-null value is returned the process runs and the call to mp:process-wait returns.
It is useful to define some terms for process states. A process is active if it has been preset, has not completed, has at least one run reason, and has no arrest reasons; otherwise, it is inactive. Active processes are further divided into two classes. An active process is waiting if it has executed a mp:process-wait that has not yet completed. An active process is runnable if it is not waiting. In addition, the process actually running at any time is called the current process.
Processes run until complete or interrupted. When a process is run, it is given an amount of processor time; when that time expires the operating system interrupts the process and looks about for other processes which can be run.
A process has two parameters to control scheduling. Its priority is an integer indicating its scheduling priority, higher numbers request more frequent processing. Its quantum indicates the minimum time (in the range 0.1 to 20.0 seconds inclusive) the process should be allowed to run before a clock tick might suspend it, assuming it does not give up execution voluntarily. This is to prevent thrashing between processes, particularly those that might have deep stacks.
In the non :os-threads model, foreign code is not interruptible and Lisp code cannot run until the foreign code returns control to Lisp, either by completing or by calling back to Lisp. In the :os-threads model, Lisp code on one process may run while foreign code runs in another process. This means that certain properties of foreign code may no longer hold. Among them this is the most significant:
Lisp pointers passed to foreign code are not guaranteed to be valid by default. In earlier releases, because Lisp code never ran while foreign code was running, a Lisp pointer passed to foreign code was guaranteed to be valid until the foreign code completed or called back to Lisp. In the :os-threads model, because Lisp code and foreign code can run in different OS processes, Lisp code may be run concurrently with foreign code. A garbage collection may move Lisp objects, rendering invalid pointers held by the foreign code. Each foreign function's definition specifies whether a lisp process calling that function retains exclusive control of the heap or releases the heap so that other lisp processes can run. Releasing the heap can improve overall application concurrency, especially if long-running or blocking foreign functions are involved, but it also introduces the danger that a lisp string passed as an argument may become invalid before the called code is through with it. Foreign function definitions by default perform non-releasing calls. A special keyword argument must be used in the def-foreign-call if a heap-releasing linkage is desired. We recommend storing all values used by both Lisp and foreign code in foreign (not garbage-collected) space or dynamically allocated on the stack, whenever those values may be used in heap-releasing calls. See foreign_functions.htm for more information.
This section deals with the situation where no process can run until input arrives from outside of Lisp, typically via a stream. The issue is how to have Lisp not waste machine cycles waiting for the arrival of input, yet have Lisp wake up in a timely manner when the input becomes available.
Each waiting process has an associated wait function. Whenever the process' thread is given processor time, it executes the wait function and if the result is still false, it immediately releases the processor back to the OS. If the OS were to cycle repeatedly through all the waiting process, the processor will be perpetually busy even though no useful work is being done. This might be reasonable on a dedicated machine -- the wasted processor cycles would not be valuable as there would be nothing else for the processor to do. But this would be bad computing citizenship on a host with other processes unrelated to Lisp, since it would consume processor cycles and paging bandwidth that might be in short supply. So Lisp tries to conserve machine resources by keeping waiting process threads completely quiescent when nothing is runnable, and allowing the OS to give threads processor time to check their wait functions only when something may have changed that could affect the result of one or more wait functions. However, Lisp needs a little help to do this.
There are only three things that can cause a wait function to return true after previously having returned nil: (1) some other running Lisp process has changed some piece of state in the Lisp world that the wait function tests; (2) an asynchronous signal arrives; or (3) input becomes available on a file descriptor (generally associated with a Lisp stream) that the wait-function tests.
Case (1) requires that wait functions be tested periodically whenever (or at least immediately after) any Lisp process actually runs. The operating system thread management does this automatically. But what should happen when absolutely no processes are runnable? We want Lisp to stop running completely until either (2) or (3) happens.
When a process finds that its wait function is not yet satisfied, it releases the CPU by performing the equivalent of a Unix select() on the set of "interesting" file descriptors. This causes the process to block The OS will not give the associated thread any CPU time until a signal arrives (2) or input becomes available on one of the interesting file descriptors (3). The process can run its wait-function again to determine whether it has actually become unblocked.
Unfortunately, the process machinery has no way to correlate wait functions with OS file descriptors, such that input becoming available on the file descriptor would (or might) cause the wait-function to return true. The system needs to be told explicitly that a file descriptor is interesting to some wait-function. The low-level input handlers (e.g. for read-char) for Allegro CL streams do this automatically, but any user implementing custom streams and/or making a low-level foreign-function interface to (for example) the underlying operating-system socket interface will need to write input functions in such a way that they inform the process machinery about input file descriptors. The following description of wait-for-input-available describes how this is done.
The purpose of mp:wait-for-input-available is to wait on one or more input streams. mp:wait-for-input-available takes a wait function just like mp:process-wait, but before suspending the calling process it carefully records the input file descriptors for each stream or file descriptor argument. While the calling process is blocked inside the call to mp:wait-for-input-available, these file descriptors will be passed to select() so that available input will immediately return from select() and awaken this thread.
If mp:process-wait is used instead of mp:wait-for-input-available, the thread may fail to notice when input becomes available and not run the wait function until some other possibly-unrelated interrupt or input causes the entire Lisp image to wake up and eventually run through all wait functions again.
The descriptions below provide only a brief introduction. Please follow the links to the individual description pages for details.
Name | Arguments | Notes |
sys:*current-stack-group* | [variable] | This variable always holds the stack-group that is currently executing. It is nil before the first mp:stack-group-resume is done. This variable should be treated as read only. |
sys:*current-stack-group-resumer* | [variable] | This variable holds the stack-group that resumed sys:*current-stack-group*, that is the stack-group that ran immediately before the current stack-group. Note that this need not be the same stack-group which is the value of the stack-group-resumer slot of the current stack-group, even though the names are similar. This variable should be treated as read only and is not usually useful in user code |
mp:make-stack-group | name &key :preset-function :preset-arguments :profile | This function creates a new stack-group object. See the full description for more information. |
mp:stack-group-preset | stack-group function &rest arguments | This function resets a stack-group and specifies its initial function and arguments. |
mp:stack-group-resume | stack-group value | This function switches to the given stack-group, transmitting value to it. |
mp:stack-group-funcall | stack-group value | This function is just like mp:stack-group-resume except that it also sets the mp:stack-group-resumer slot of the resumed stack-group to the resuming stack-group. |
mp:stack-group-return | value | This function resumes the stack-group in the stack-group-resumer slot of the current stack-group. |
mp:symeval-in-stack-group | symbol stack-group | This function returns two values. The first is the value of
the special symbol in the given stack-group (which may be the current stack-group). It
only looks for actual bindings on the stack-group; if the symbol has a global value but is
not bound on the stack-group, the global value is not returned. The second returned value describes the status of the binding. t is returned if the symbol is bound on the stack-group, nil if the symbol has no binding, and :unbound if there is a binding which has been subjected to makunbound. In the latter two cases, the first returned value is nil. |
sys:global-symbol-value | symbol | This function returns two values. The first is the global value for the special variable named by symbol, ignoring any bindings on the current stack-group and the second is t if the variable has a value (in the sense of boundp). Otherwise the first value will be nil and the second will be the symbol :unbound. |
mp:stack-group-name | stack-group | This function returns the name of the stack-group, which must be a string. setf may be used to change the name. |
mp:stack-group-state | stack-group | This function returns the state of stack-group, from the list of keywords given above. The value should be treated as read only. |
mp:stack-group-resumer | stack-group | This function returns the value of the stack-group-resumer slot of stack-group. That value must be another stack-group or nil. setf may be used to reset this value, although it is more normally set by mp:stack-group-funcall. |
mp:profile-stack-group-p | sg-or-process | This function returns the value of the profile-stack-group-p
flag for the stack group or process specified by sg-or-process. If the value of
the flag is non-nil, then the space and time profilers will collect samples when this
stack group or process is executing. If sg-or-process is a stack group, all stack-groups which are created from it will inherit the current value of this flag at the time the stack-group is created (unless a specific value is given for the profile argument to mp:make-stack-group). This value may be changed with setf. |
Given two trees of conses, the same-fringe-p predicate determines whether the atoms on the fringes of the tree are the same. Since trailing nils of lists will be ignored, this is equivalent to asking whether the trees would print the same, ignoring parenthesization. (Indeed, this observation should suggest an alternative solution which would not employ stack-groups!) The function makes two stack-groups which each generates the next atom of the tree's fringe each time it is resumed. A unique symbol is returned to flag the end of a fringe.
(defun same-fringe-p (tree1 tree2)
(let ((sg1 (mp:make-stack-group "fringe1"))
(sg2 (mp:make-stack-group "fringe2"))
(flag (gensym))
atom1 atom2)
(mp:stack-group-preset sg1 #'fringe tree1 flag)
(mp:stack-group-preset sg2 #'fringe tree2 flag)
(loop (setq atom1 (mp:stack-group-funcall sg1 nil)
atom2 (mp:stack-group-funcall sg2 nil))
(unless (eq atom1 atom2) (return
nil))
(when (eq atom1 flag) (return
t)))))
(defun fringe (tree exhausted-flag)
(labels ((fringe-internal (node)
(cond ((atom
node) (mp:stack-group-return node))
(t (fringe-internal (car node))
(and (not (null (cdr node)))
(fringe-internal (cdr node)))))))
(fringe-internal tree)
exhausted-flag))
The following Lisp session results:
USER(10): (same-fringe-p '( a (b (c 3) . d) (e) f) '((a b) c (3 (d) (e
f))))
T
USER(11): (same-fringe-p '( a (b (c 3) . d) (e) f) '((a b) c (d (3) (e f))))
NIL
This example shows the utility of stack-group coroutines for implementing generators that need a stack to remember their state. A generator with simpler state, for example, one that just cdr's down the elements of a list returning each in turn, could be much more efficiently implemented using a closure.
The process object implements the abstraction of independent processes running within the same Lisp world. Processes are implemented on top of stack-groups. Each active process is associated with a particular stack-group. The identity of that stack-group changes only if the process actively resumes some other stack-group. (A process is therefore not so much identified with a stack-group as with the intrinsic identity of some ongoing computation.) Like stack-group coroutines, processes all see the same Lisp world except for their control ("call") stack and dynamic bindings. Active processes are managed by a computation which runs on one particular system stack-group, called the scheduler. The scheduler is resumed from time to time, and it determines which runnable process should next run. This effects time-multiplexed execution.
A process object is implemented as a structure. Some of its slots are meaningful to user code, but except for those explicitly noted they should be treated as read only.
Whether and when a process runs is controlled by several mechanisms. First the process' initial function and arguments must be specified, and then the process must be 'reset'. (The mp:process-preset function combines these operations.) Resetting a process potentially causes the scheduler to notice it. Second, a process maintains two lists: its run-reasons and its arrest-reasons. These can be any Lisp objects whatsoever. For the scheduler to consider a process for execution, it must have at least one object on its run-reason list, and no objects on its arrest-reason list. Finally, a process that needs to wait for some arbitrary condition does so using the mp:process-wait function. This function specifies a function and arguments. The scheduler periodically applies the wait-function to the wait-arguments, and when a non-null value is returned the process resumes from mp:process-wait.
It is useful to define some terms for process states. A process is active if it has been preset, has not completed, has at least one run reason, and has no arrest reasons; otherwise, it is inactive. Active processes are further divided into two classes. An active process is waiting if it has executed a mp:process-wait that has not yet completed. An active process is runnable if it is not waiting. In addition, the process actually running at any time is called the current process.
The scheduler maintains two queues of active processes: a queue of runnable processes, and a queue of waiting processes. The scheduler does not keep track of inactive processes at all. For example, the process-lock mechanism defined below makes a process inactive by giving it an arrest reason. The process lock remembers the process entirely independently of the scheduler and will revoke the arrest reason when the lock is unlocked. The functions which add and revoke run and arrest reasons have the major responsibility for inserting and removing processes in the scheduler's queues.
The scheduler runs a loop that never terminates and is responsible for resuming the stack-groups of runnable processes. From time to time the scheduler's stack-group is itself resumed in one of the ways discussed below. It first applies the process-wait-function of each waiting process to its process-wait-args. If the function no longer returns nil, the process is moved to the runnable queue. It is important to know that a process' wait-function runs on the scheduler's stack-group, not the stack-group of the process itself. Therefore, the wait function cannot refer to any of the dynamic special-variable bindings of its process' stack-group. The stack group containing those bindings is not in effect when the wait-function runs. A wait-function can refer to the value of a special variable that is not bound, i.e., its global value.
Actually, mp:process-wait immediately calls the wait-function just once on the client process' stack before resuming the scheduler. There are two reasons for calling the wait-function then. The first is efficiency, since a stack-group switch can be avoided if the wait-function returns true immediately. The second is to protect the scheduler; if the function signals an error, the error will occur on the process' stack, not the scheduler's.
Nonetheless, wait-functions should be as short and simple as possible. They may run many times and will throw the scheduler into the debugger if they signal an error. The scheduler stack-group's read-eval-print loop uses
<scheduler>
as its prompt if it ever invokes the debugger, but it may be difficult to resume scheduler operation after such an error.
Once all waiting processes have been checked, the scheduler determines the most deserving process in the runnable queue and resumes its stack-group. If there are no runnable processes, Lisp releases the hardware processor and waits for an asynchronous I/O or timer event possibly to make some process runnable.
A process has two parameters to control scheduling. Its priority is an integer indicating its scheduling priority, where higher numbers request more frequent processing. Its quantum indicates the minimum time (in the range 0.1 to 20.0 seconds inclusive) the process should be allowed to run before a clock tick might suspend it, assuming it does not give up execution voluntarily. This is to prevent thrashing between stack-groups, particularly those that might have deep stacks.
The scheduler can be resumed in these ways:
It is the scheduler's job to run the wait-functions of all waiting processes from time to time in order to detect when a waiting processes becomes unblocked. When no process is runnable at all the scheduler could loop continuously through the pending wait functions until one returns true. This might be reasonable on a dedicated machine -- the wasted processor cycles would not be valuable there manifestly would be nothing else for the processor to do. But this strategy would be bad citizenship on a host with other processes unrelated to Lisp, since it would consume processor cycles and paging bandwidth that might be in short supply. So the scheduler tries to conserve machine resources keeping Lisp completely quiescent when nothing is runnable, and testing pending wait-functions only when something may have changed that would affect what one or more wait functions would return. However, it needs a little help to do this.
There are only three things that can cause a wait function to return true after previously having returned nil: (1) some other running Lisp process has changed some piece of state in the Lisp world that the wait function tests; (2) an asynchronous (Unix) signal arrives; or (3) input becomes available on a Unix file descriptor (generally associated with a Lisp stream) that the wait-function is testing.
The (1) case is automatically covered because the scheduler runs through all wait functions (in decreasing process-priority order) whenever a running process blocks or its quantum expires. But what should the scheduler do when it finds absolutely no processes are runnable? It wants to Lisp stop running completely until either (2) or (3) happens.
When the scheduler finds no runnable process, it calls the Unix select() routine (or its platform-dependent equivalent) on the set of "interesting" input file descriptors. When either a signal arrives (2) or input becomes available on one of the interesting file descriptors (3) the call to select() will complete. The scheduler will then run through the pending wait-functions to determine which processes (if any) have become unblocked.
Unfortunately, the scheduler has no automatic way to correlate wait functions with Unix file descriptors, such that input becoming available on the file descriptor would (or might) cause the wait-function to return true. The scheduler needs to be told explicitly that a file descriptor is interesting to some wait-function. The low-level input handlers (e.g. for read-char) for Allegro CL streams do this automatically, but any user implementing his own streams and/or making low-level foreign-function interface to (for example) the underlying operating-system socket interface will need to write his input functions in such a way that they inform the scheduler about input file descriptors. The following description of wait-for-input-available describes how this is done.
The purpose of mp:wait-for-input-available is to wait on one or more input streams. mp:wait-for-input-available takes a wait function just like mp:process-wait, but before suspending the calling process it carefully informs the scheduler about the input file descriptors for each stream or file descriptor argument. While the calling process is blocked inside the call to mp:wait-for-input-available, if the scheduler ever decides to suspend Lisp completely because there are no runnable processes at all, these file descriptors will be passed to select() so that available input will immediately return from select() and awaken Lisp.
If mp:process-wait is used instead of mp:wait-for-input-available, the scheduler may fail to notice when input becomes available and not run the wait function until some other possibly-unrelated interrupt or input causes it to wake up and eventually run all the pending wait functions.
The descriptions below provide only a brief introduction. Please follow the links to the individual description pages for details.
Name | Arguments | Notes |
sys:*all-processes* | [variable] | The value of this variable is a list of all processes that have ever been created and have never completed or been killed. |
sys:*current-process* | [variable] | The value of this variable is the process currently running (:os-threads) which the scheduler is currently running (non :os-threads). In non :os-threads implementations, nil is returned if the scheduler itself is running. |
mp:*default-process-quantum* | [variable] | Default quantum given to each process. |
mp:start-scheduler | nil | :os-threads: initializes multiprocessing
(the function is misnamed since there is no scheduler but used for consistency with the
non :os-threads implementation.) Non :os-threads: start the scheduler process and initialize multiprocessing. |
mp:make-process | &key name reset-action run-reasons
arrest-reasons priority quantum resume-hook suspend-hook initial-bindings
message-interrupt-function stack-allocation run-immediately |
This function returns a new process object, but does nothing about making it runnable. Follow the link to the full description for details. |
mp:process-initial-bindings | process | This slot of a process stores an alist of initial special bindings which are established in process when it is started. The value may be set with setf. |
mp:process-property-list | process | The property-list slot of a process implements a generalized property list as a convenient place to store additional information about a process. |
mp:process-resume-hook | process | It is normal for execution of a process to be interrupted many times. This is transparent to the process and usually it is not necessary for the process to know when its execution is suspended and resumed. However, if these slots are non-nil, they should be functions of no arguments which are called on the process' stack-group or thread each time the execution is suspended or resumed (but not when the process if first started or when it is killed). |
mp:process-suspend-hook | ||
mp:process-run-function | name-or-keywords function &rest args | This function does a mp:make-process, then presets the new process with function and args. The first argument is either a string, which is the name of the process, or is a list of keyword arguments accepted by mp:make-process. The new process is returned. By default, the process is killed when and if it completes. |
mp:process-run-restartable-function | name-or-keywords function &rest args | This function is just like mp:process-run-function (just above), but automatically provides a :reset-action argument of t. The process thus started will restart if it is reset or completes. |
mp:process-enable | process | Makes process active by removing all its run and arrest reasons, then giving it a single run reason of :enable. |
mp:process-disable | process | This function makes process inactive by revoking all its run and arrest reasons. The effect is immediate if a process disables itself. |
mp:process-reset | process &optional no-unwind kill | This function causes process when it next runs to throw out of its present computation, if any, then apply its initial function to its initial argument. |
mp:process-preset | process function &rest arguments | This function sets the initial function and arguments of process, then resets any computation in progress in it. This does not make process active if it was not already active. |
mp:process-flush | process | This function causes process to wait forever, unless it is reset. It is not removed from the sys:*all-processes* list. It is not unwound. Its stack-group (in the non :os-threads model) is simply discarded. |
mp:process-kill | process | This function resets the process to unwind it, then removes it from consideration by the scheduler and from the sys:*all-processes* list. |
mp:process-interrupt | process function &rest args | This function forces process to apply function to args when it next executes. When function returns, the original computation of process continues. If process is waiting when interrupted, it runs the interrupt function and then continues waiting. If process is not active, mp:process-interrupt makes it active for the interrupt function, then makes it inactive again. |
mp:process-name | process | This function returns the name of process, which must be a string. This value may be changed with setf. |
mp:process-name-to-process | name &key :abbrev | This function returns the process whose process-name is name. name must be a string. If the :abbrev keyword argument is specified non-nil, then name is matched to the beginning of each process-name to find a match. |
mp:process-stack-group | process | Non :os-threads only. Symbol does not exist
in :os-threads implementations. Returns the stack-group associated with process. |
mp:process-thread | process | :os-threads only. Symbol does not exist in
non :os-threads implementations. Returns the thread associated with process. |
mp:process-initial-form | process | This function returns a cons of the initial function of process and its argument list. |
mp:process-wait-function | process | This function returns the function used to determine when a waiting process becomes runnable. |
mp:process-wait-args | process | This function returns the list of arguments passed to the wait-function of process. |
mp:process-run-reasons | process | This function returns the list of run-reasons for process. |
mp:process-arrest-reasons | process | This function returns the list of arrest-reasons for process. |
mp:process-add-run-reason | process object | This function adds object to the list of run-reasons for process. |
mp:process-add-arrest-reason | process object | This function adds object to the list of arrest-reasons for process. |
mp:process-revoke-run-reason | process object | This function removes object from the list of run reasons for process. |
mp:process-revoke-arrest-reason | process object | This function removes object from the list of arrest reasons for process. |
mp:process-runnable-p | process | These functions return t if, respectively, process is runnable or active. A process is active if it has been reset and not yet completed, and has at least one run reason and no arrest reasons. It is runnable if it is active and not waiting. |
mp:process-active-p | ||
mp:process-priority | process | This function returns the priority of process. It defaults to 0 and may be set to any fixnum with setf. |
mp:process-quantum | process | This function returns the quantum for process. The quantum may be specified when the process is created; it defaults to the value of *default-process-quantum* and may be set to any real value between 0.1 and 20 with setf. |
mp:process-whostate | process | This function returns the current who-line string of process. |
mp:without-scheduling | &body body | This macro inhibits the OS (in :os-threads) or the scheduler (non :os-threads) from suspending a process involuntarily (asynchronously) during the execution of body. This always works in non :os-threads versions since the scheduler is a Lisp process. However, in :os-threads versions, the OS will run another process if the current process blocks, waits, or executes a mp:process-allow-schedule. |
excl:without-interrupts | &body body | This macro executes body protecting against any handling of asynchronous interrupts. Execution of body is guaranteed to complete without any other process running, or any asynchronous interrupt being dispatched, unless the process does something to block or otherwise explicitly yield to the scheduler (e.g. with mp:process-allow-schedule). It is an error to call a heap-releasing foreign function within the scope of excl:without-interrupts. |
sys:*disallow-scheduling* | [variable] | This special variable is bound to t whenever multiprocessing
scheduling is disabled. For example, the system binds this variable to t during the
execution of the forms within a mp:without-scheduling
form. This variable should be treated as read-only and should never be set or bound by user code. |
mp:process-sleep | seconds &optional whostate | mp:process-sleep suspends the current process for
at least the number of seconds specified. That number may be any non-negative, non-complex
number. While the process sleeps, other processes are allowed to run. The whostate
(default "Sleep") is a string which temporarily replaces the process' whostate
during the sleep. When multiprocessing is initialized, Common Lisp function sleep is changed to be equivalent to mp:process-sleep. Instead of causing the entire Lisp world to suspend execution for the indicated time, only the executing process is suspended. This is usually the desired action. However, the original definition of sleep remains available as the function named excl:lisp-sleep; but its behavior in sleeping the given time is OS platform dependent and may not be reliable under multiprocessing. |
excl:lisp-sleep | seconds | |
mp:process-wait | whostate function &rest arguments | This function suspends the current process (the value of sys:*current-process*) until
applying function to arguments yields true. The whostate argument must be a string which
temporarily replaces the process' whostate for the duration of the wait. This function
returns nil. See the discussion under the headings Waiting for input to a file and mp:process-wait vs mp:wait-for-input-available. |
mp:process-wait-with-timeout | whostate seconds function &rest args | This function is similar to mp:process-wait, but with a timeout. The units of time are seconds. The value of seconds may be any real number. Negative values are treated the same as 0. The wait will timeout if function does not return true before the timeout period expires. |
mp:wait-for-input-available | streams &key :wait-function :whostate :timeout | This lower-level function extends the capabilities of mp:process-wait and mp:process-wait-with-timeout to allow a process to wait for input from multiple streams and to wait for input from a file. |
mp:with-timeout | (seconds . timeout-forms) &body body | This macro evaluates the body as a progn body. If the evaluation of body does not complete within the specified interval, execution throws out of the body and the timeout-forms are evaluated as a progn body, returning the result of the last form. The timeout-forms are not evaluated if the body completes within seconds. |
mp:process-allow-schedule | &optional other-process | This function resumes multiprocessing, allowing other processes to run. All other processes of equal or higher priority will have a chance to run before the executing process is next run. If the optional argument is provided, it should be another process. |
The multiprocessing system described in this chapter is a nonstandard but upward-compatible extension to Common Lisp. Although multiprocessing can be used within a single application towards performing a single, integrated task, it is an important design feature that multiprocessing allows multiple unrelated tasks to reside in the same Lisp image. A user might run a window system, several independent Lisp listeners, a Lisp editor, and an expert system application (with network connections to processes in other hosts) all in the same Lisp world.
For independent tasks to coexist in a single world, the names of functions and special variables in separate systems cannot interfere with one another. This is the problem the package system is intended to solve, and with a little care it is quite possible to keep names separate. However, there are a number of public common-lisp package special variables which are implicitly used by all Lisp applications. Each process may want its own values assigned to these special variables. This section discusses how conflicting demands on these variables can be supported.
For example, according to the Common Lisp standard, the default value of the special variable *print-base* is 10. A stand-alone application may assume that if it never changes *print-base* its value will be 10. Or, if a stand-alone application always wanted to print in octal, it might set *print-base* to 8. The effect on its own calculation is well defined by the Common Lisp standard, but changing the global value of *print-base* may have an unfortunate effect on other unrelated applications running in the same Lisp world. If one application changes *print-base* what about others that assume it will always have the default value, or want to exercise independent control?
A solution to this problem is to make a process that uses public variables maintain its own bindings for those variables. When a process is first started, a stack-group or thread is created on which it will run. Whenever the process' computation binds special variables, those bindings are recorded on its stack-group or thread. Binding hides the value of special variables with (possibly) different values. Inside the scope of a binding, code will only see the bound value, not the original value, of a variable. Special variable bindings are local to the process that creates them and are never seen by other processes.
Note that there is only one global "binding" for a variable. While it always exists and is shared by all processes, if the variable has been bound in the process to another value, code inside that binding in that process will only see the bound value, not the global value. This is important to understand when there are multiple processes. Sometimes it is useful for processes to communicate through the value in a global binding. Other times processes must be protected from seeing the value other processes have bound a variable to.
Assume you have several processes and none of them binds the special variable *print-base*. If one process sets the variable, the global binding is the one affected and all of the processes will see the new value. However, any process that binds the variable as a special will not see changes to the global binding while its own binding is in effect, and conversely, any setq it does will not affect the value seen by the others.
The multiprocessing system provides a mechanism to allow processes to bind special variables at process startup time. The process-initial-bindings slot of a process is examined when the process is first started. If not nil, it should be an alist of symbols and forms to evaluate for value. The symbols are bound in the process as special variables with the given initial values, effectively wrapping those bindings around the entire execution of that process. If a particular variable appears more than once on the alist, entries after the first are ignored.
By default, mp:make-process and mp:process-run-function create processes with a null process-initial-bindings list. Such action is not appropriate for processes which may do arbitrary computations - for instance, a Lisp listener which accepts computations from a user - or for applications which need to be isolated from others. In such cases, the multiprocessing system provides a list of special variables with appropriate default values. The variable excl:*cl-default-special-bindings* is bound to that list.
In the :os-threads implementation, sys:*current-process* is always added to each process' special binding list; when a process is running that process is the value of sys:*current-process*. In the non :os-threads model the scheduler maintains this as a global variable.
The standard CL stream variables are bound to the value of *terminal-io* but that variable itself is not given a dynamic binding. This can cause problems because sometimes *terminal-io* may be set to a stream that will signal an error when used (see debugging.htm). The variable excl:*initial-terminal-io* holds the original *terminal-io* stream when Lisp starts up. It may be useful for processes that aren't connected to a usable *terminal-io* but wish to produce some output, for example for debugging.
Note that the value forms are evaluated in the dynamic environment of the new process, not the process that created it, and this new dynamic environment has no special bindings in effect. Those value forms that themselves depend upon special variable values, e.g. cltl1:*break-on-warnings*, will therefore see the global values of those variables. The intention of this mechanism is that the new process should not inherit variable values from the process that started it without something explicit being done to pass the value. Some other ways to achieve the same end are shown below.
If, for example, you want your new process to share the readtable of the invoking process, putting an entry
(*readtable* . *readtable*)
on the alist would not work. The value form would be evaluated on the new process, and the global value of *readtable* would result. Instead, you should do something like this (note that this is a code fragment - you must add forms where there are suspension points):
(process-run-function
`(:name ...
:initial-bindings
((*readtable* . ',*readtable*)
,@excl:*cl-default-special-bindings*))
...)
Since the :initial-binding list is treated as an alist, the first entry shadows all succeeding ones. The effect of the above will be to set the alist cons for *readtable* to something like:
(*readtable* . (quote #<readtable @ #x504a1>))
where the readtable object is the readtable of the invoking process, and the quote is stripped off by the evaluation when the new process begins.
Using mp:process-run-function and related functions it is possible to run any kind of Lisp computation as a separate quasi-parallel process. All processes share the same Lisp world, so the only thing that differentiates one from process from another (besides the state of its computation, of course) is the set of special-variable dynamic bindings of its binding stack. For example, in an environment with multiple windows it is possible to run multiple top-level Lisp listeners or other interactive command loops, each process in its own window; *terminal-io* would be bound in each process to a stream object connected to its particular input-output window.
There are two ways to create special bindings in a new process. The simple way is just to place the special variable on the lambda list of the process' initial function, and pass the value as an argument (note that this is a code fragment - the suspension points indicate where additional forms are required):
(defun my-command-processor (*terminal-io* *page-width*)
(declare (special *page-width*))
...)
(let ((my-window (create-a-window ...)))
(process-run-function "Bizarre Command Interpreter"
#'my-command-processor my-window
(page-size my-window)))
However, this requires the process' initial function to provide for each variable that will be bound. The following more general idiom permits establishment of arbitrary bindings for the new process:
(defun establish-bindings (symbol-list value-list function args)
(progv symbol-list value-list (apply function args)))
(let ((my-win (create-a-window ...)))
(process-run-function "Bizarre Command Interpreter"
#'establish-bindings
'(*user-name* *phone-number*)
(list "Fred" "555-1234")
#'my-command-processor
(list my-win (window-size my-win))))
Here the establish-bindings function is wrapped around the application of my-command-interpreter function to its arguments; while the function is executing it will see bindings of the special variables user-name and phone-number.
The problem of starting a real Lisp listener is actually somewhat more complex than the above discussion and sample code fragments suggest. For one thing, the Lisp system defined by Common Lisp depends on a large number of special variables, such as *readtable*, *package*, and *print-level*. Since commands executed by one Lisp listener might side effect some variables and thereby inappropriately affect other processes, each process should maintain separate bindings for these variables. Allegro CL provides a wrapping function, tpl:start-interactive-top-level, which automatically binds to reasonable default initial values all the special variables in the Lisp system. Within a call to tpl:start-interactive-top-level, a read-eval-print loop can be started with tpl:top-level-read-eval-print-loop.
Thus, the Allegro CL idiom for running a standard Lisp listener communicating with *terminal-io* (this might be done in a excl:*restart-app-function*) looks something like this:
(tpl:start-interactive-top-level *terminal-io*
#'tpl:top-level-read-eval-print-loop
nil)
And the idiom for running a standard Lisp listener inside a window looks something like this:
(process-run-function "My Lisp Listener"
#'tpl:start-interactive-top-level
my-window-stream
#'tpl:top-level-read-eval-print-loop
nil)
Entry to tpl:top-level-read-eval-print-loop establishes additional bindings for certain variables used by the top level loop. These bindings are established inside any bindings established by tpl:start-interactive-top-level. The variables and initial binding values are taken from the alist bound to the variable tpl:*default-lisp-listener-bindings*.
tpl:top-level-read-eval-print-loop also provides a hook to customize individual Lisp listeners. It calls the function which is the value of tpl:*top-level-read-eval-print-loop-wrapper*, with two arguments, the internal function to invoke the read-eval-print loop, and the argument it requires.
In this rather trivial example, we define the wrapper so that the command character (the value of tpl:*command-char*, initially #\:) is #\$. Of course, in a real example, something more complex would be done, but the form would be similar to this example.
(defun my-lisp-listener-wrapper (function args)
(let ((tpl:*command-char* #\$))
(apply function args)))
;; After we evaluate the following forms, the command character will be $
;; (rather than :) in any new lisp listener process.
(setq tpl:*top-level-read-eval-print-loop-wrapper*
'my-lisp-listener-wrapper)
What about setting up personal customizations? When Allegro CL is first started it searches for .clinit.cl files as described in startup.htm. The typical purpose of a .clinit.cl file is to load other files and to define personalized functions and top-level commands, but it is possible for a .clinit.cl file to setq special variables. It is important to understand what it means to do so.
A .clinit.cl file is loaded into a Lisp before multiprocessing is started and even before the initial Lisp listener is created. If a .clinit.cl file sets any special variables, the value affected will (in general) be the global value of the special. If the purpose is to customize Lisp for some particular application this is probably the wrong way to do it. Someday an unrelated system may be loaded into the same Lisp world which may depend on reasonable default global variable values. Furthermore, the various default binding mechanisms described above will generally keep the global value even from being seen.
For example, if it is necessary to set *print-escape* to nil for some application, it is better for the application to set up its own binding of the variable in the application code itself, or if that is impossible, to have the code that starts the process wrap a binding of the variable around execution of the application with the mechanisms illustrated above. The worst way is to setq *print-escape* in a .clinit.cl file. Processes that assume the documented Common Lisp standard simply might not work properly if *print-escape* is nil.
A dynamic binding - that is, a 'location' where a value is stored - cannot be shared between processes. Each process may store the same (i.e. eql) value in its binding, but if one process does a setf of *print-base* it cannot affect the value of the binding of *print-base* seen by another process. If that value is a Lisp object that can be modified, side effects to that object will obviously be seen by both processes. Of the variables listed above, those that typically hold obviously side-effectable objects are *package*, *read-table* and the several stream variables. Numbers (such as the value of *print-base*) and the boolean values t and nil (such as might be the value of *print-escape*) are not objects that can be side affected.
Unlike the standard common-lisp package special variables, it is quite reasonable to place in your .clinit.cl file personal customizations for top-level Lisp listener variables documented in top_level.htm, such as tpl:*prompt*, that are not bound per process. No standard Common Lisp code should depend on these.
Since the multiprocessing system tries hard to insulate variable bindings between processes, the macro tpl:setq-default is provided to make it easier for a user to change the default value of some standard variable when that is what is really desired. It is intended primarily for inclusion in .clinit.cl files.
tpl:setq-default is convenient for simple customization and suffices for simple environments. However, because it sets the global value of a symbol which is seen by all processes, it may not be appropriate in Lisp environments where the user may not have control over the needs of some processes. In such circumstances it may be preferable not to change a global symbol value in a .clinit.cl file with tpl:setq-default. Instead, users may just push a new cons onto the tpl:*default-lisp-listener-bindings* alist. Such action will have much the same effect with regard to any code run inside a Lisp listener but will not affect non-listener processes.
A process-lock is a defstruct which provides a mechanism for interlocking process execution. A process-lock is either free or it is seized by exactly one process. When a process is seized, a value is stored in the lock. Usually this is the process which seized the lock, but can be any Lisp object. Any process which tries to seize the lock before it is released will block. This includes the process which has seized the lock; the mp:with-process-lock macro protects against such recursion.
Both mp:process-lock and the mp:without-scheduling macro protect a segment of code from interleaving execution with other processes. Neither has significant execution overhead, although mp:without-scheduling is somewhat more efficient. However, the mechanisms have different ranges of applicability. A process-lock blocks only those other processes which request the same lock; mp:without-scheduling unconditionally blocks all other processes, even those completely unrelated to the operation being protected. This might include high-priority processes that need to field interrupts with low latency. Therefore, the mp:without-scheduling macro should not be used around a code body that might require significant time to execute.
The descriptions below provide only a brief introduction. Please follow the links to the individual description pages for details.
Name | Arguments | Notes |
mp:make-process-lock | &key name | This function creates a new lock object. The value of the :name keyword argument should be a string which is used for documentation and in the whostate of processes waiting for the lock. |
mp:process-lock | lock &optional lock-value whostate timeout | This function seizes lock with the value lock-value. |
mp:process-unlock | lock &optional lock-value | This function unlocks lock. |
mp:process-lock-locker | lock | This function returns the value stored in lock, which is usually the process holding the lock, and returns nil if lock is not locked. |
mp:with-process-lock | (lock &key norecursive) &body body | This macro executes the body with lock seized. |
The example below can be loaded into Lisp either compiled or interpreted. It shows simple use of mp:process-run-function and mp:with-process-lock. Three parallel processes compute the number of trailing zeroes in factorial N for different ranges of N.
(in-package :user)
(require :process)
(defun factorial (n) (if (< n 2) n (* n (factorial (1- n)))))
;; This lock is used to prevent output interleaving.
(defvar moby-output-lock (mp:make-process-lock))
;; Print to the stream the number of trailing
;; zeros in (factorial n) from n=from up to n=to.
;; This is a *very* inefficient way to do this computation,
;; but the point is to make it run slow enough to see.
(defun process-test (stream from to)
(do ((n from (1+ n)))
((>= n to))
(do ((x (factorial n) (/ x 10))
(zeros -1 (1+ zeros)))
((not (integerp x))
(mp:with-process-lock (moby-output-lock)
(format stream
"factorial(~d) has ~d trailing zeros~%"
n
zeros))))))
;; This starts three processes in parallel.
;; The original Lisp listener returns immediately,
;; and will accept types forms while the other processes run.
(defun moby-process-test ()
(mp:process-run-function "Test 1" #'process-test t 400 440)
(mp:process-run-function "Test 2" #'process-test t 440 470)
(mp:process-run-function "Test 3" #'process-test t 470 400)
t)
;; Make sure factorial itself is compiled
;; because large factorials exceed the interpreter's stack.
(unless (compiled-function-p #'factorial) (compile 'factorial))
(format t "Type (moby-process-test) to test multi-processing.~%")
Copyright (C) 1998-1999, Franz Inc., Berkeley, CA. All Rights Reserved.