C++: lock-free counting with std::atomic
Increment a shared counter from four threads without a mutex.
On this page
C++ threads and the mutex lesson.
- Increment a shared counter from four threads without a mutex.
- Predict the sample output and explain the state changes.
- Complete the practice task and check the boundary cases.
The concept
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.
Code example
#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";
}counter=200000
Follow the code
Open the walkthrough to inspect the example one step at a time.
Show guided walkthrough
#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";}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
| Step / line | What happens | State after step | Output 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 |
Behind the scenes
How C++ processes this code
The C++ compiler produces a native executable. Execution enters main, where objects and function calls perform the work.
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.