Atomic operations are useful for writing safe concurrent/multithreaded code with shared mutable state without having to resort to (expensive) locks or mutexes. For small data types such as integers or pointers, atomics allow us to replace locks like
{
QMutexLocker locker(&mutex);
x += 1;
}
with atomics like
atomicX.fetchAndAddAcquire(1);
Or with the C++ standard library, we can replace
{ std::lock_guard<std::mutex> guard(mutex); x += 1; }
with
atomicX.fetch_add(1, std::memory_order_acq_rel)
.
This has potential performance benefits since CPUs provide dedicated instructions for atomic operations. Also, there are different "memory orderings" with different guarantees about when which value is visible to a different CPU core. Atomics allow us to select the appropriate degree of control here, with more relaxed orderings leading to potentially better performance than using a stricter ordering or a mutex/lock. Memory orders are defined in the C++ standard, for example see the summary on cppreference.com.
Specifically on x86 architectures, you will likely not see a difference between ordinary volatile variables and atomics with relaxed or acquire/release orderings. The CPU architecture already provides strong guarantees for all memory accesses. However, accurate use of memory orderings is quite relevant on ARM architectures. Using too relaxed memory orders (or no atomics at all) could corrupt data.
Specific answers to your questions:
-
When should one utilize atomic operations?
When all of the following hold:
- you have multiple threads that share mutable state
- your modifications to this state only affect single words (integers, pointers, …)
- you want to avoid locks/mutexes
Counter-indications:
- the data is only used by a single thread → use ordinary variables
- the shared data remains constant → use ordinary variables
- changes to the shared data affect more than one word at a time → use locks/mutexes
- you do not want to learn about memory orderings → use atomics with sequentially consistent ordering or locks/mutexes
-
Is it supposed to improve performance?
Primarily, it is intended to improve correctness.
But different memory orders allow us to select the most relaxed (and therefore fastest) memory order that still meets our needs. Of course we could always impose a sequentially consistent order (memory_order_seq_cst
) but that is generally slowest and will involve CPU-level locks.
-
Are there other considerations besides performance?
Correctness.
-
When would one look at someones code, and chastize them for having not used atomic operations?
Not at all.
- Chastizing people is usually not very didactic – it builds resentment.
- If someone shares mutable state across threads and doesn't protect accesses via mutexes, locks, or atomics, there are potential race conditions. It could make sense to raise this issue.
- If someone uses mutexes or locks to protect single-word data changes, switching to atomics could lead to simplifications and performance improvements.