Overview
This section explains what atomics is and how they are used.
For decades CPUs have had built-in hardware instructions for doing thread-safe math and compare operations.
These instructions are available as built-in intrinsics in any modern C/C++ compiler.
In FPL these are called Atomics and are wrapper functions around those intrinsics.
Atomics functions generate memory barriers (or fences) to ensure that memory operations are completed in order.
See Synchronization methods for a comparison with the other synchronization primitives.
Fetch-And-Add (Add)
With a Fetch-And-Add instruction, you can do an atomic addition and get back the previous value (before the add) as an atomic operation.
Pseudo code:
oldValue = *storageValue;
*storageValue = oldValue + inc;
return oldValue;
FPL has several versions for various datatypes:
Usage:
volatile uint32_t storageValue;
fpl_platform_api uint32_t fplAtomicFetchAndAddU32(volatile uint32_t *value, const uint32_t addend)
Adds a 32-bit unsigned integer to the value by the given addend atomically.
Add-And-Fetch (Increment)
With an Add-And-Fetch instruction, you can do an atomic addition by one and get back the new value (after the increment) as an atomic operation.
Pseudo code:
*StorageValue++;
NewValue = *StorageValue;
return newValue;
FPL has several versions for various datatypes:
Usage:
volatile uint32_t storageValue;
fpl_platform_api uint32_t fplAtomicAddAndFetchU32(volatile uint32_t *dest, const uint32_t addend)
Adds the addend to destination 32-bit unsigned integer atomically and returns the result after the ad...
Exchange
With an Exchange instruction, you can do an atomic exchange and get back the previous value (before the change) as an atomic operation.
Pseudo code:
oldValue = *storageValue;
*storageValue = exchangeValue;
return oldValue;
FPL has several versions for various datatypes:
Usage:
volatile uint32_t storageValue;
fpl_platform_api void fplAtomicStoreU32(volatile uint32_t *dest, const uint32_t value)
Overwrites the 32-bit unsigned value atomically.
- Note
- The exchange atomic is identical to Store, except that it returns the previous value before the change.
Store
With a Store instruction, you can do a memory write as an atomic operation.
Pseudo code:
*storageValue = newValue;
FPL has several versions for various datatypes:
Usage:
volatile uint32_t storageValue;
- Note
- The store atomic is identical to Exchange, except that it does not return any value.
Load
With a Load instruction, you can do a memory read as an atomic operation.
Pseudo code:
FPL has several versions for various datatypes:
Usage:
volatile uint32_t storageValue;
fpl_platform_api uint32_t fplAtomicLoadU32(volatile uint32_t *source)
Loads the 32-bit unsigned value atomically and returns the value.
Compare-And-Swap
With a Compare-And-Swap instruction, you can check a value against another and exchange them as an atomic operation.
The previous/current value will always be returned regardless of the result.
Pseudo code:
oldValue = *storageValue;
if (oldValue == comparend) {
*storageValue = exchangeValue;
}
return oldValue;
FPL has several versions for various datatypes:
Usage:
volatile uint32_t storageValue = 0;
volatile uint32_t storageValue = 0;
} else {
}
fpl_platform_api bool fplAtomicIsCompareAndSwapU32(volatile uint32_t *dest, const uint32_t comparand, const uint32_t exchange)
Compares a 32-bit unsigned integer with a comparand and swaps it when comparand matches the destinati...
fpl_platform_api uint32_t fplAtomicCompareAndSwapU32(volatile uint32_t *dest, const uint32_t comparand, const uint32_t exchange)
Compares a 32-bit unsigned integer with a comparand and swaps it when comparand matches the destinati...
Memory Barriers
With memory barriers you can prevent the compiler from reordering memory reads/writes.
In FPL additional CPU fences may be added to ensure that the previous memory operations are completed before the future ones.
Without fences
int theValue;
int valueIsPublished = 0;
void updateValue(int newValue) {
theValue = newValue;
valueIsPublished = 1;
}
int getValue() {
if (valueIsPublished) {
return theValue;
}
return -1;
}
- Warning
- This example may break even on a single-threaded environment!
With a read/write barrier
int theValue;
int valueIsPublished = 0;
void updateValue(int newValue) {
theValue = newValue;
valueIsPublished = 1;
}
int getValue() {
if (valueIsPublished) {
return theValue;
}
return -1;
}
fpl_platform_api void fplAtomicReadWriteFence(void)
Inserts a memory read and write fence/barrier.
Read or Write
In some special cases you don't have to issue a full memory barrier like fplAtomicReadWriteFence() - you can use a read or write barrier only:
- Use fplAtomicReadFence() to ensure prior memory reads complete before future ones (and prevent the compiler from reordering memory reads).
- Use fplAtomicWriteFence() to ensure prior memory writes complete before future ones (and prevent the compiler from reordering memory writes).
Atomics vs Barriers?
Atomic functions may include memory barriers to ensure that CPU memory read/write instructions are executed in order and to prevent the compiler from reordering memory read/writes.
This means you don't have to use any kind of memory barriers around atomic functions ;-)