Access to all Common Lisp objects is thread-safe in the sense that it does not cause an error because of threading issues.
Immutable (or read-only) objects such as numbers, characters, functions, pathnames and restarts can be freely shared between threads, but special precautions must be taken when all of the following conditions are true:
vector-push-extendinto a "multithreaded" hash-table or vector (see Single-thread context arrays and hash-tables).
In this situation, it is your responsibility to ensure that all of the stores that occurred when creating the new object are visible to the other threads, as described by Making an object's contents accessible to other threads.
This section outlines for which types of mutable Common Lisp object access is atomic. That is, each value read from the object will correspond to the state at some point in time. Note however, that if several values are read, there is no guarantee about how these values will relate to each other if they are being modified by another thread (see Issues with order of memory accesses).
When one of these mutable atomic objects is modified, readers see either the old or new value (not something else), and it is guaranteed that the Lisp image is not corrupted by the modification even if multiple threads read or write the object simultaneously.
The Common Lisp functions that access hash tables are atomic with respect to each other. See also modify-hash for atomic reading and writing an entry and with-hash-table-locked. See also Modifying a hash table with multiprocessing for thread-safe ways to ensure a table entry.
Operations on editor buffers (including points) are atomic and thread-safe as long as their arguments are valid. This includes modification to the text. However, buffers and points may become invalid because of execution on another thread. The macros
editor:with-point-locked should be used around editor operations on buffers and points that may be affected by other processes. Note that this is applicable also to operations that do not actually modify the text, because they can behave inconsistently if the buffer they are looking at changes during the operation. See the
LispWorks Editor User Guide
for details of these macros.
Access to lists (including alists and plists) is not atomic. Lists are made of multiple cons objects, so although access to the individual conses is atomic, the same does not hold for the list as a whole.
Macros that expand to multiple accesses are in general not atomic. In particular, modifying macros like
incf are not atomic (but see the atomic versions of some of them in Low level atomic operations).
Making several calls to Common Lisp functions that access hash tables will not be atomic overall. However LispWorks provides thread-safe ways to ensure a hash table entry - see Modifying a hash table with multiprocessing. See also modify-hash for atomic reading and writing an entry and with-hash-table-locked.
Operations on CAPI objects are not atomic in general. The same is true for anything in the LispWorks IDE. These operations need to be invoked from the thread that owns the object, for example by
An object's contents become accessible to other threads when it is stored into a cell that may be accessed by those threads (a "globally accessible cell"). In most cases, you should either use a synchronization mechanism (typically a lock) to control access to the cell because it is the most reliable approach or use a mailbox to make the object accessible to other threads (the mailbox acts as the globally accessible cell).
If you do not use a synchronization mechanism or mailbox, then all stores into the object must be forced to be visible to other threads ("ensured") before the object is stored in the globally accessible cell. This also applies to any stores that LispWorks did during construction of the object. Normally, LispWorks does not do that for every store, because it would slow the program too much. Note that numbers, except
fixnum and short-float (in 32-bit LispWorks) or
fixnum and single-float (in 64-bit LispWorks), are also objects that are constructed using stores that need to be ensured.
Storing objects into globally accessible cells would typically be done by
setf or a related macro such as
incf, but can also be done by Common Lisp functions such as
replace (when the target is globally accessible). If stores into the objects that these functions store have not been ensured and may be read by another thread without synchronization, then one of the mechanisms in Ways to guarantee the visibility of stores must be used. Note that when a sequence itself, rather than the elements, is modified, for example by
nunion, then all access to the sequence needs to controlled by a synchronization mechanisms, which will also guarantee the visibility of stores.
hash-table) to serialize all access to the globally accessible cell. Any use of a synchronization mechanism that may affect the behavior of another thread will implicitly ensure all preceding stores on the current thread.
(setf macro-function)into a symbol, or by one of
vector-push-extendinto a "multithreaded" hash-table or vector (see Single-thread context arrays and hash-tables).
characteror short-float in 32-bit LispWorks;
characteror single-float in 64-bit LispWorks) in the globally accessible cell. There are no stores that need to be ensured during the creation of these objects. However, if the store is done using an operator that allocates (see Special care for macros and accessors that may themselves allocate) then you will still need to ensure.
setfor a related macro (for example
incf), and the place argument is wrapped by the macro globally-accessible.
Stores into objects must be "ensured" once by one of the above mechanisms, before the object becomes globally accessible. Stores that occur after this are not guaranteed to be visible to other threads until another ensuring operation.
In some circumstances you can make the program more efficient by explicitly ensuring stores using globally-accessible (7) or ensure-stores-after-memory (8) before or when the object is first made visible. See Ensuring stores are visible to other threads for more details.
A synchronizing operation (1), atomic operation (4) or a call to ensure-stores-after-stores (8) ensures all stores into objects that were created by the current thread. The other situations ensure the stores that they perform and anything pointed to by those stores, but are not guaranteed to ensure other stores (because they may be able to skip ensuring in some circumstances).
A situation that always requires special care is storing into a globally accessible cell using macros and accessors that may themselves allocate. If the store is not done using (1), (4) or (5) in Ways to guarantee the visibility of stores, then you need to use globally-accessible to ensure the stores in new objects that may be allocated are visible, even if the object that is stored does not need it (because it is an immediate, an interned symbol or was ensured earlier). These macros and accessors include:
mask-field (when not a fixnum),
ldb (when not a fixnum).
Any user defined accessor (that is an operator with a
setf expander defined by
defsetf) that allocates during the setting operation. Note that allocation during the macro expansion is not an issue.
See Destructive macros and accessors that allocate internally for more details.
A store to a cell from one thread is said to be "visible" from another thread when a load from that cell from the other thread obtains the value that was stored. Within a single thread, all stores are visible immediately to loads from the same thread, but that is not always the case in a multithreaded situation. Store operations that occur in one thread are not necessarily visible from other threads until something ensures that they are. In other words, another thread loading from the cell where the store was done may still obtain the cell's previous value, even if "logically" it seems that the load happened after the store. For a new object, the previous value may be anything that was in memory, including an invalid value that may cause crashes.
(setq *a-global-symbol* (cons 1 2))
It looks like the form that thread B executes will always return either 1 (if it happens after thread A has set
*a-global-symbol*) or -1 (otherwise), because in the case that maybe-cons is a cons it must already have 1 in the car. However, that is not necessarily true because the store of the cons into
*a-global-symbol* may be visible to thread B before the store of 1 into the cons (which happens inside the call to
cons) is visible. This applies to explicit stores in the program as well, for example if thread A executes:
(setq *a-global-symbol* (rplaca (list nil) 1))
Note: the second load in thread B (inside
car) is dependent on the first load (reading the value cell from
*a-global-symbol*). Such dependent loads are guaranteed to occur in the program order in all current LispWorks releases. In situations when the two loads are independent, but you still need them to occur in the program order, you will need to use ensure-loads-after-loads.
In most circumstances, all access to globally accessible cells should be controlled by a lock, which eliminates all of these problems because releasing a lock implicitly ensures that all stores in that thread are visible to all other threads, so by the time another thread gets ownership of the lock, all the stores are already visible. Sending an object via a mailbox, using
vector-push-extend or synchronizing using any of the other synchronization mechanisms (barrier, condition-variable, semaphore or the lock of a
hash-table) also ensures the stores are already visible (for a full list, see Making an object's contents accessible to other threads). If you make an object globally accessible without any of these mechanisms, then you need to ensure explicitly that the stores are all visible. Note that if the store occurs inside a lock but the object is not read inside the same lock, then you still need to ensure the stores, because the reading may happen before unlocking has ensured the stores are visible. Note also that, for some macros and accessors (listed below), you need to ensure the stores even if the value that you store does not need ensuring.
If you need to explicitly ensure that all stores are visible, then the best approach is to use globally-accessible, which takes a single argument, place, which can be any generalized reference form as described in section
5.1.1 Overview of Places and Generalized Reference
of the Common Lisp HyperSpec. In most cases,
) is the same as place. However, when globally-accessible is used inside
setf or a related macro such as
incf then it also ensures all stores are visible to other threads before modifying place. For example, if we change the code that thread A in An example to consider the issues executes to:
The other approach is to use ensure-stores-after-stores just before storing into the globally accessible cell, but after any allocation or other operations that modify the object being stored in the cell. In the example in An example to consider the issues, thread A would do:
ensure-stores-after-stores cannot be used when the form that stores the object is a destructive macro such as
push (for a full list, see Destructive macros and accessors that allocate internally below), because
push itself allocates a cons using stores that need to be ensured. globally-accessible takes care of that, by ensure the stores after any allocation that the macro may do (except when it is a macro with a non-standard setf expansion that allocates or stores in the setter). Thus globally-accessible is preferable when you can use it. You need to use ensure-stores-after-stores when the store is encapsulated in some other operation. For example, if you use
fill to store a new object in a globally accessible sequence (some-vector below) then you will need to do something like this:
ensure-stores-after-stores always ensures all the stores that have happened in the current thread. globally-accessible is not guaranteed to ensure all preceding stores accept into the value that it stores, because it may be able to skip ensuring in some circumstances.
pushnew, push-end, push-end-new,
incf (when not a fixnum) and
decf (when not a fixnum) may generate new objects internally, so if they are used to destructively modify a globally accessible cell without synchronization then you will need to use globally-accessible.
(pushnew some-object (sys:globally-accessible *a-global-symbol*))
Note that globally-accessible is needed with
pushnew, push-end, and push-end-new even if there are no stores into the object being pushed that need ensuring. For
decf, if you can guarantee that the new value is fixnum, then you do not need globally-accessible.
ldb take a place argument that may generate new objects when modified, so if place is globally accessible and it is modified without synchronization then you will need to wrap globally-accessible around such modifications of place.
getf and cdr-assoc, globally-accessible is needed even if there are no stores into the new object and key that need ensuring because new conses might be added to the place. For
ldb, if you are absolutely sure that the new value is fixnum then you do not need globally-accessible.
setf expanders defined by
defsetf cannot be used on globally accessible cells without synchronization (by a lock or other synchronization mechanism) if they do any of the following:
See section 188.8.131.52 Setf Expansions of the Common Lisp HyperSpec for the definition of "value forms" and "storing form".
ensure-stores-after-stores can be used to ensure stores for objects that are modified after becoming globally accessible. However, if you need to ensure that the new values are seen by other threads that may be already accessing the modified objects then you need to use some synchronization mechanism anyway. Thus in most cases you should use a lock, which will deal with the synchronization.
ensure-stores-after-stores does slow the program a little on architectures that need it (Currently ARM, ARM64 and PowerPC), so you can consider the following optimizations:
As noted in Ensuring stores are visible to other threads, loads from an object that was obtained from a globally accessible cell are currently guaranteed to occur after the load the cell itself, because all the architectures that LispWorks runs on guarantee that. In principle, sometime in the future there may be a new architecture that does not provide that guarantee. You can guard against this by using globally-accessible when reading from the globally accessible cell as well. Currently that just macroexpands into its argument, so does not affect the performance, but for an architecture that requires anything it will do the right thing.
When multiple threads access the same memory location, the order of those accesses is not generally guaranteed. You should therefore not attempt to implement "lockless algorithms" which depend on the order of memory accesses unless you have a good understanding of multiprocessing issues at the CPU level (see Ensuring order of memory between operations in different threads).
However, all of the Low level atomic operations and locking operations (see Locks) do ensure that all memory accesses that happen before them have finished and that all memory accesses that happen after them start after them. Therefore, normally there is nothing special to consider when using those operations. The modification check macros described in Aids for implementing modification checks also take care of this.
make-hash-table argument single-thread tells
make-hash-table that the table is going to be used only in single thread context, and therefore does not need to be thread-safe. Such a table allows faster access.
make-array argument single-thread creates an array that is single threaded. Currently, the main effect of single-thread is on the speed of
vector-push-extend on non-simple vectors. These operations are much faster on "single threaded" vectors, typically more than twice as fast as "multithreaded" vectors.
You can also make an array be "single-threaded" with set-array-single-thread-p.
LispWorks User Guide and Reference Manual - 20 Sep 2017