C++: lock-free counting with std::atomic

Increment a shared counter from four threads without a mutex.

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

int main() {
  std::atomic<int> counter{0};
  std::vector<std::thread> threads;

  for (int t = 0; t < 4; ++t) {
    threads.emplace_back([&counter] {
      for (int i = 0; i < 50000; ++i) {
        counter.fetch_add(1, std::memory_order_relaxed);
      }
    });
  }
  for (std::thread& th : threads) {
    th.join();
  }
  std::cout << "counter=" << counter.load() << "\n";
}
Output
counter=200000

Explanation

An atomic operation completes as one indivisible step, so no other thread can observe it half done. std::atomic<int> provides atomic reads, writes, and read-modify-write operations such as fetch_add. Unlike a plain int, concurrent access to an atomic is not a data race.

The memory order argument controls how the operation is ordered relative to other memory accesses. memory_order_relaxed guarantees only atomicity, which is enough for a counter whose value is read after all threads have been joined. join also makes the threads' writes visible to the main thread.

Explore the dry run
Guided walkthroughReady
1#include <atomic>
2#include <iostream>
3#include <thread>
4#include <vector>
5
6int main() {
7 std::atomic<int> counter{0};
8 std::vector<std::thread> threads;
9
10 for (int t = 0; t < 4; ++t) {
11 threads.emplace_back([&counter] {
12 for (int i = 0; i < 50000; ++i) {
13 counter.fetch_add(1, std::memory_order_relaxed);
14 }
15 });
16 }
17 for (std::thread& th : threads) {
18 th.join();
19 }
20 std::cout << "counter=" << counter.load() << "\n";
21}

Follow the example

Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.

View the complete dry-run table
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput so far
An integer whose operations are indivisible.counter = 0
—
Four threads are started, each with the same lambda.counter = 0
—
Each increment is atomic, so none are lost.counter grows by 1 per call
—
4 threads × 50000 increments.counter = 200000
counter=200000

Cost and common mistakes

O(n) total increments, with contention between threads.

  • Atomics make single operations safe, not whole sequences. 'Read, decide, then write' still needs a lock or compare-exchange.
  • A contended atomic is a bottleneck: all cores fight over one cache line. Count locally and combine at the end for better speed.
  • Prefer the default (sequentially consistent) memory order unless you can prove a weaker one is correct.

Try it yourself

What would counter++ on a plain int do here?

Reveal answer and reasoning

It is a data race, which is undefined behaviour in C++: increments would usually be lost and the total would be less than 200000.

Further reading