Portable Threads
GBBopen's Portable Threads provides a uniform interface to commonly used
thread (multiprocessing) entities. Wherever possible, these entities
do something reasonable in Common Lisp implementations that do not provide
threads. However, entities that make no sense without threads signal errors in
non-threaded implementations (as noted with each entity). The feature
:threads-not-available
is added on Common Lisp implementations
without thread support, and the feature
:with-timeout-not-available
is added on implementations that do
not support with-timeout.
Portable Threads entities are provided by the :portable-threads
module in GBBopen. Stand-alone use of the Portable Threads interface is also
easy, requiring only the
portable-threads.lisp
file for the portable-interface layer and, if desired,
scheduled-periodic-functions.lisp
for the scheduled and periodic function
entities.
Threads and Processes
Common Lisp implementations that provide multiprocessing capabilities use one
of two approaches:
- Application-level threads (also called “Lisp processes”)
which are created, deleted, and scheduled internally by the Common Lisp
implementation
- Operating-system threads (or “native threads”) which are
lightweight, operating-system threads that are created, deleted, and
scheduled by the operating system
There are advantages and complexities associated with each approach, and the
Portable Threads Interface is designed to provide a uniform abstraction over
them that can be used to code applications that perform consistently and
efficiently on any supported Common Lisp implementation.
Locks
Common Lisp implementations provide differing semantics for the behavior of
mutual-exclusion locks that are acquired recursively by the same
thread: some always allow recursive use, others provide special
“recursive” lock objects in addition to non-recursive locks, and still
others allow recursive use to be specified at the time that a lock is being
acquired. To enable behavioral consistency in all Common Lisp
implementations, the :portable-threads
interface module provides
(non-recursive) locks and recursive locks and a single
acquisition form, with-lock-held, that behaves
appropriately for each lock type.
Condition Variables
POSIX-style condition variables provide an atomic means for a
thread to release a lock that it holds and go to sleep until it is
awakened by another thread. Once awakened, the lock that it was holding is
reacquired atomically before the thread is allowed to do anything else.
A condition variable must always be associated with a lock (or
recursive lock) in order to avoid a race condition created when one
thread signals a condition while another thread is preparing to wait on it.
In this situation, the second thread would be perpetually waiting for the
signal that has already been sent. In the POSIX model, there is no explicit
link between the lock used to control access to the condition variable and the
condition variable. The Portable Threads Interface makes this association
explicit by bundling the lock with the
condition-variable CLOS object instance and allowing
the condition-variable object to be used directly in
lock entities.
Hibernation
Sometimes it is desirable to put a thread to sleep (perhaps for a long time)
until some event has occurred. The Portable Threads Interface provides two
entities that make this situation easy to code:
hibernate-thread and
awaken-thread. Thread hibernation can only be
performed by the thread on itself, eliminating issues of a thread being
hibernated at an undesirable time. Note that there is the potential for a
hibernate/awaken race condition if a thread hibernates itself again soon after
being awakened (when a second awaken-thread intended
for the original hiberation is applied to the second hibernation rather than
being ignored because the target thread is not hibernating). Using a
condition-variable is preferable in this situation.
When a thread is hibernating, it remains available to respond to
run-in-thread and
symbol-value-in-thread operations as well as to be
awakened by a dynamically surrounding with-timeout.
What about Process Wait?
Thread coordination functions, such as process-wait
, are
expensive to implement with operating-system threads. Such functions stop the
executing thread until a Common Lisp predicate function returns a true
value. With application-level threads, the Lisp-based scheduler evaluates the
predicate function periodically when looking for other threads that
can be run. With operating-system threads, however, thread scheduling is
performed by the operating system and evaluating a Common Lisp
predicate function requires complex and expensive interaction between
the operating-system thread scheduling and the Common Lisp implementation.
Given this cost and complexity, many Common Lisp implementations that use
operating-system threads have elected not to provide
process-wait
-style coordination functions, and this issue
extends to the Portable Threads Interface as well.
Fortunately, most uses of process-wait
can be replaced by a
different strategy that relies on the producer of a change that would affect
the process-wait
predicate function to signal the event
rather than having the consumers of the change use predicate functions to poll
for it. Condition variables, the Portable Threads
hibernate-thread and
awaken-thread mechanism, or blocking I/O functions
cover most of the typical uses of process-wait
.
Entities
The GBBopen Project