🎓 A1Learner
🔍
0 of 28 lessons completed

C++ Atomics

An atomic value is a shared value whose single operations complete without interruption. Threads can update one atomic counter safely without writing a lock.

An atomic counter without a lock

Four threads each call fetch_add 250 times on one atomic counter. No lock is written anywhere in the program.

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

void addMany(std::atomic<int>* counter) {
  for (int i = 0; i < 250; i++) {
    counter->fetch_add(1);
  }
}

int main() {
  std::atomic<int> counter = 0;
  std::thread first(addMany, &counter);
  std::thread second(addMany, &counter);
  std::thread third(addMany, &counter);
  std::thread fourth(addMany, &counter);
  first.join();
  second.join();
  third.join();
  fourth.join();
  std::cout << "Total: " << counter.load() << std::endl;
  return 0;
}

Reading an atomic after the joins

Two threads each add one 150 times. Main reads the final value with load and prints whether the atomic type is lock free on this machine.

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

void addFixed(std::atomic<int>* counter, int times) {
  for (int i = 0; i < times; i++) {
    counter->fetch_add(1);
  }
}

int main() {
  std::atomic<int> counter = 0;
  std::thread first(addFixed, &counter, 150);
  std::thread second(addFixed, &counter, 150);
  first.join();
  second.join();
  std::cout << "Value: " << counter.load() << std::endl;
  std::cout << "Lock free: " << std::boolalpha << counter.is_lock_free() << std::endl;
  return 0;
}

Two atomics kept in step

Each round adds three points and counts one turn on a second atomic. Both totals are read after every thread has joined.

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

void addRounds(std::atomic<int>* score, std::atomic<int>* turns) {
  for (int i = 0; i < 25; i++) {
    score->fetch_add(3);
    turns->fetch_add(1);
  }
}

int main() {
  std::atomic<int> score = 0;
  std::atomic<int> turns = 0;
  std::thread first(addRounds, &score, &turns);
  std::thread second(addRounds, &score, &turns);
  std::thread third(addRounds, &score, &turns);
  std::thread fourth(addRounds, &score, &turns);
  first.join();
  second.join();
  third.join();
  fourth.join();
  std::cout << "Score: " << score.load() << std::endl;
  std::cout << "Turns: " << turns.load() << std::endl;
  return 0;
}

Example

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

void sellTickets(std::atomic<int>* sold) {
  for (int i = 0; i < 30; i++) {
    sold->fetch_add(1);
  }
}

int main() {
  std::atomic<int> sold = 0;
  int stock = 90;
  std::thread first(sellTickets, &sold);
  std::thread second(sellTickets, &sold);
  std::thread third(sellTickets, &sold);
  first.join();
  second.join();
  third.join();
  int total = sold.load();
  bool soldOut = total >= stock;
  std::cout << "Sold: " << total << std::endl;
  std::cout << "Sold out: " << std::boolalpha << soldOut << std::endl;
  return 0;
}

Key points

  • std::atomic wraps a value so each single operation on it completes as one step.
  • fetch_add adds its argument to an atomic value as one step.
  • load reads the current value of an atomic.
  • The value fetch_add returns depends on which call runs first, so these programs never print it.
  • is_lock_free reports whether this machine runs the atomic type without a hidden lock.

In this example, three threads each record 30 ticket sales on one atomic counter. Main joins all three threads and then loads the final count. The count is 90, which reaches the fixed stock of 90, so the sold out verdict prints true.

Note

The joined total is guaranteed everywhere, while the lock-free answer belongs to this machine.

Try it Yourself

Edit the code, then press Run.

Running is simulated

Exercise

Why do these programs print the counter with load after the joins instead of printing each fetch_add return value?