Skip to content
Theory · Multithreading: An Overview · lesson 8 of 12

std::atomic

At the restaurant's entrance stands a mechanical visitor counter: press the button, and the number goes up by one. Two waiters may press it almost at the same time, but the counter still counts both presses. One press is one whole action, and it cannot be cut in half.

A mutex protects any section of code, but for a single number it is overkill. Before every change a thread locks the mutex, after it the thread unlocks it, and the other threads wait. For simple variables, the <atomic> header has the std::atomic template, which is that counter with a button:

C++
#include <atomic>
#include <iostream>
#include <thread>

int main() {
    std::atomic<int> hits{0};
    std::thread worker([&hits]() { hits += 5; });
    hits += 2;
    worker.join();
    std::cout << hits << '\n';  // 7
}

Both threads change hits at the same time, and the program has no mutex. But no data race occurs: the total is always 7.

Atomic operations

std::atomic<int> holds an ordinary int but performs every action on it as a whole. This covers these actions:

  • ++ and --;
  • += and -=;
  • reading the value;
  • assignment.

These are atomic operations. Another thread sees such an operation either not yet started or already finished, but never half done. That is why simultaneous changes are not lost: the three steps "read, add, write" merge into one press of the button.

Check it: whatever order you press "Step A" and "Step B" in, the total always ends two above the start.

LiveTwo threads, one atomic ++countercounter = 5
shared variable
5
counter
thread A
  1. the whole ++counterlock xadd [counter], eax
thread B
  1. the whole ++counterlock xadd [counter], eax
std::atomic<int>: ++counter is one indivisible instruction (clang, x86-64)

counter = 5, but now it is a std::atomic<int>: ++counter is one indivisible step. Try to lose an increment.

You choose the order of the steps

The counter from the lesson on data races becomes correct if you replace int counter = 0; with std::atomic<int> counter{0};. No mutex is needed then, and the program always prints 200000.

std::atomic works for other simple types too, such as bool. A variable of type std::atomic<…> is called atomic. A stop flag is usually declared as std::atomic<bool> stop{false};. One thread assigns stop = true, and another checks stop in a loop and leaves it once the flag is set.

Atomic variable or mutex

This is where the counter analogy ends. The button protects one number and one press, not a chain of actions.

An atomic variable protects one value and one operation on it. A mutex protects a group of actions:

  • changing two variables consistently;
  • checking a value and then changing it.

Two separate atomic variables do not help here: between two operations, another thread can see an intermediate state.

For a single counter or flag, an atomic variable is usually cheaper than a mutex. For everything else, where there is more than one action, you choose a mutex.

Common mistake

An atomic variable is incremented in two steps:

C++
counter = counter + 1;  // ✗

There is no data race here: both the read of counter and the write are atomic. But they are two separate operations, and another thread can change counter between them. The other thread's increment is then lost, and the total can end up below 200000. Write it as one operation: ++counter or counter += 1.

Key points

  • std::atomic<int> performs ++, +=, reads and assignments as a whole, so simultaneous changes are not lost.
  • For a single counter or flag, an atomic variable is usually cheaper than a mutex.
  • A group of actions on several values is protected by a mutex, not by several atomic variables.
  • counter = counter + 1 is two atomic operations, not one: an increment can be lost.

Subscribe to take the course

The course is free. Sign in with Telegram and subscribe to the channel to open every lesson.

Sign in another way