Atomic Operations

Member Functions for Atomic Types

  • store()

    • Atomically replace the object's value with its argument
  • load()

    • Atomically return the object's value
  • operator =()

  • operator T()

    • Synonyms for store() and load()
  • exchange()

    • Atomically replace the object's value with its argument
    • Returns the previous value

Member Functions for Specializations

  • Atomic pointers support pointer arithmetic

    • Increment and decrement operators
    • fetch_add() synonym for x++
    • fetch_sub() synonym for x--
    • += and -= operators
  • Integer Specializations have these, plus

    • Atomic bitwise logical operations &, | and ^

#include <atomic>
#include <iostream>

int main() {
  std::atomic<int> x = 0;
  std::cout << "After initialization: x = " << x << '\n';

  // Atomic assignment to x
  x = 2;

  // Atomic assignment from x. y can be non-atomic
  int y = x;

  std::cout << "After assignment: x = " << x << " , y = " << y << '\n';

  x.store(3);
  std::cout << "After store: x = " << x.load() << '\n';

  std::cout << "Exchange returns " << x.exchange(y) << '\n';
  std::cout << "After exchange: x = " << x << " , y = " << y << '\n';
}

std::atomic_flag

  • std::atomic_flag is an atomic boolean type

    • Has less overhead than std::atomic<bool>
  • Only three operations

    • clear() sets flag to false
    • test_and_set() sets flag to true and returns previous value
    • operator =()
  • Must be initialized to false atomic_flag lock = ATOMIC_FLAG_INIT;

Spin Lock

  • A spin lock is essentially an infinite loop

    • It keeps "spinning" until a condition becomes true
  • An alternative to locking a mutex or using a condition variable

  • We can use std::atomic_flag to implement a basic spin lock

    • The loop condition is the value of the flag

Spin Lock with std::atomic_flag

  • Each thread calls test_and_set() in a loop

  • If this returns true

    • Some other thread has set the flag and is in the critical section
    • Iterate again
  • If it returns false

    • This thread has set the flag
    • Exit the loop and proceed into the critical section
  • After the critical section, set the flag to false

    • This allows another thread to execute in the critical section

Spin Lock


#include <atomic>
#include <iostream>
#include <thread>
#include <vector>

// Initialize flag to false
std::atomic_flag flag = ATOMIC_FLAG_INIT;

void task(int n) {
  // Loop until we can set the flag
  while (flag.test_and_set()) {
  }

  // Critical section
  using namespace std::literals;
  std::this_thread::sleep_for(50ms);
  std::cout << "I'm a task with argument " << n << '\n';
  // End of critical section

  flag.clear();
}

int main() {
  std::vector<std::thread> threads;

  for (int i = 0; i < 10; ++i) {
    threads.push_back(std::thread(task, i));
  }

  for (auto &thr : threads) {
    thr.join();
  }
}

Pros and Cons of spin lock

  • A spinning thread remains active

    • A mutex may block the thread
  • It can continue immediately when it "gets the lock"

    • With a mutex, the thread may need to be reloaded or woken up
  • Processor intensive

    • Only suitable for protecting very short critical sections
    • And / or very low contention
    • Performance can be heavily impacted if spinning threads interrupt each other
    • Usually only used in operating systems and libraries

Hybrid Mutex

  • Often used to implement std::mutex

  • Start with spin lock with a time out

    • If the thread sets the flag in time, enter the critical section
    • If the thread cannot set the flag in time, use the normal mutex implementation
  • This gives better performance than the conventional implementation