Back to TILs

C++ atomic operations

Date: 2024-02-27Last modified: 2024-11-02

Table of contents

C++ atomic operations are essential for writing safe and efficient concurrent code by ensuring that operations on shared variables are performed atomically and with well-defined memory ordering semantics. They are a powerful tool for writing high-performance multithreaded applications while avoiding data races and synchronization issues.

Atomic operations provide a lock-free mechanism for synchronization, avoiding the overhead and potential issues of locks. They are more lightweight and efficient when used correctly for simple operations.

However, atomic operations have limitations and are suitable for specific scenarios. For more complex synchronization, such as protecting critical sections or implementing more intricate synchronization patterns, you may still need to use mutexes or other synchronization primitives.

Atomic Operations use hardware

Atomic operations use hardware-supported atomic instructions (e.g., compare-and-swap, load-link/store-conditional) to ensure that certain operations on shared variables are performed indivisibly.

These operations are typically implemented using CPU instructions that guarantee atomicity without requiring explicit locking mechanisms.

Atomic operations are designed to be lock-free or wait-free depending on the specific implementation and underlying hardware support.

Lock use software

Locks (e.g., mutexes, spinlocks) use software-based synchronization techniques to control access to shared resources.

Locks require acquiring and releasing a lock object explicitly using locking mechanisms such as mutex locks (std::mutex) or spinlocks (std::atomic_flag).

When a thread acquires a lock, it gains exclusive access to the critical section of code, preventing other threads from entering that section until the lock is released.

Memory order

Categorization of atomic operations


  std::atomic<int> x{ 2 };
  fmt::print( "Initial value: {}\n", x.load() );

  x.store( 5 );
  fmt::print( "New value: {}\n", x.load() );

  fmt::print( "fetch_add: was {} now {}\n", x.fetch_add( 3 ), x.load() );

  // Perform a release-store (ensures all previous writes are visible before this store)
  x.store( 10, std::memory_order_release );

  // Perform an acquire-load (ensures this read sees the value written in the release store)
  int value = x.load( std::memory_order_acquire );

  value = 11;

  bool ret_val = x.compare_exchange_weak(
      value,                       //
      13,                          //
      std::memory_order_release,   // for success scenario
      std::memory_order_relaxed ); // for failure scenario

Possible output

Initial value: 2
New value: 5
fetch_add: was 5 now 5

References