Low level atomic operations are defined in all cases for a specific set of places. These places are listed in Places for low-level atomic operations:
Application of macros that are defined by define-atomic-modify-macro is also restricted to the places in Places for low-level atomic operations above, because they implicitly use low level atomic operations.
You can test whether a place is suitable for use with these operations by the predicate low-level-atomic-place-p.
The macros with-modification-check-macro and with-modification-change provide a way for a body of code to execute and check whether there was any "modification" during this execution, where modification is execution of some other piece of code. This is useful in situations when reading some data out of some data structure is more common than modification, and reading the data involves getting some values that need to be consistent. It makes it possible to ensure consistency of the values without a lock.
The checking code should be wrapped by the macro with-modification-check-macro, and the modifying code should be wrapped by the macro with-modification-change. They are associated by the fact that their modification-place argument is the same.
modification-place is a place as defined in Common Lisp (it does not need to be one of the places for atomic locking) which can receive a fixnum. It must be initialized to a fixnum. It must not be modified by any code except with-modification-change.
with-modification-check-macro defines a lexical macro (by
macrolet) with the name macro-name which takes no arguments, and is used to check whether there was any change since the entering the body.
Note that these macros do not guard against errors that may occur because of changes to the data structures that are accessed, and do not create any locking between users of these macro. In particular, the modifying code will typically need to lock something too, and the checking code must do only operations that cannot fail because of modification in another thread.
Provided that all modification to the
b slots of a
my-cache object are done by the modifier code above, the return values of
b in the reading code are guaranteed to have been set by the same
setf invocation in the modifier code.
A set of synchronization functions is provided which ensure order of memory between operations in different threads. These are ensure-loads-after-loads, ensure-memory-after-store, ensure-stores-after-memory and ensure-stores-after-stores.
The effect of each of these functions is to ensure that all the operations of the first type (the word following the
ensure-) that are in the program after the call to the function are executed after all the operations of the second type (last word in the function name) that are in the program before the call to the function.
Before or after "in the program" means the order that a programmer interpreting (correctly) the program would expect the operations to be executed. On a modern CPU this is not necessarily the same as the actual execution order. On a single CPU the end result is guaranteed to be the same, but on a computer with multiple CPU cores it is not.
An operation of type load is an operation that reads data from an object into a local variable. Typical load operations are
svref, structure accessors,
slot-value and getting the value of a symbol. A store operation is an operation that modifies data in an object. A memory operation is either a load or a store.
You need these functions when you need to synchronize between threads and you do not want to use the system supplied synchronization objects (Locks, mailboxes, Condition variables, Counting semaphores, Synchronization barriers). In most cases you should try first to use a synchronization object. Using the synchronization functions described in this section is useful if you can identify a serious bottleneck in your code that can be optimized using them.
Suppose you have two code fragments, which may end up executed in parallel, and both of which access a global structure
*gs*. The first fragment is a setter, and you can be sure that it is not executed in parallel to itself (normally because it actually runs while holding a lock):
The second fragment is the reader. You want to guarantee that it gets a value that was stored after the counter reached some value (the counter value always increases). You may think that this will suffice:
LispWorks User Guide and Reference Manual - 20 Sep 2017