Atomic Types

  • All operations on the variable will be atomic

  • C++11 defines an atomic template

    • In the <atomic> header
    • The parameter is the type of the object
    • The object must initialized
    // Atomic int, initialized to 0
    atomic<int> x = 0;
    
  • The parameter must be a type which is "trivially copyable"

    • Scalar type
    • Class where all the copy and move constructors are trivial
  • Normally only integer types and pointers are used

  • For more complex types, locks may be silently added

    • Locking a mutex takes longer
    • To avoid this, use a pointer to the type

Using an std::atomic<T> Object

  • We can assign to and from the object
x = 2;        // Atomic assignment to x
y = x;        // Atomic assignment from x. y can be non-atomic
  • These are two distinct atomic operations

    • Other threads could interleave between them
  • Operations such as ++ are atomic

    • Fetch old value
    • Increment
    • Store new value

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

std::atomic<int> counter = 0;

void task() {
  for (int i = 0; i < 100'000; ++i) {
    ++counter;
  }
}

int main() {
  std::vector<std::thread> tasks;
  
  for (int i = 0; i < 10; ++i) {
    tasks.push_back(std::thread(task));
  }

  for (auto &thr : tasks)
    thr.join();

  std::cout << counter << '\n';
}

Volatile Keyword

  • May change without explicit modification

    • Prevents some compiler optimizations
    • Typically used when accessing hardware
  • Often mistakenly used in threading

    • Some programmers expect the Java / C# behaviour
    • Has no impact on thread safety
  • At one point, Visual Studio supported this in C++

    • Removed before C++11

Double-checked Locking

  • One solution is to make the initialized object atomic atomic<Test *> ptest = nullptr;
  • Atomic types do not support the . or -> operators
  • We must copy to a non-atomic pointer before we can use it
Test *ptr = ptest;
ptr->func();