C++ Basics: Count Independent Events with a Relaxed Atomic
Use atomic read-modify-write operations for a counter whose value is read after workers join.
Prepare a scratch directory
Tested on Linux x86_64 with GCC 14.2.0. Install a C++20-capable GCC compiler and its standard-library development files, with g++ on PATH. You also need Bash, a text editor, and mktemp, rm, and rmdir. No third-party library is required.
Keep the same terminal open. Stop if setup fails before saving the source:
start_directory="$PWD"
work_directory="$(mktemp -d)" || exit 1
cd -- "$work_directory" || exit 1Run the example
Save this as atomic-relaxed-counter-demo.cpp:
#include <atomic>
#include <iostream>
#include <thread>
#include <vector>
int main()
{
std::atomic<int> completed{0};
std::vector<std::jthread> workers;
workers.reserve(4);
for (int worker_index = 0; worker_index < 4; ++worker_index) {
workers.emplace_back([&] {
for (int event_index = 0; event_index < 1000; ++event_index) {
completed.fetch_add(1, std::memory_order_relaxed);
}
});
}
for (auto& worker : workers) {
worker.join();
}
std::cout << "Completed: "
<< completed.load(std::memory_order_relaxed) << '\n';
}Compile and run it. The second command runs only after compilation succeeds:
g++ -std=c++20 -Wall -Wextra -Werror -pedantic -pthread atomic-relaxed-counter-demo.cpp -o atomic-relaxed-counter-demo &&
./atomic-relaxed-counter-demoObserved output:
Completed: 4000Read the result
Each fetch_add is one indivisible update, so concurrent increments are not lost. The final load occurs after every worker has joined, and the total is always 4000. Relaxed ordering is enough for this isolated counter because it does not publish or protect any other data.
Apply it carefully
An ordinary shared integer increment would race. Separating an atomic load and store would avoid a data race but could still lose increments. Relaxed ordering does not make unrelated memory accesses safe or turn the counter into a readiness signal. Choose a synchronization protocol when the counter announces that another object can be read. Atomic does not necessarily mean lock-free.
The C++ working draft documents the relevant rules. This example uses C++20 facilities; the current draft also contains newer additions.
Clean up
Remove only the source and executable created here, then remove the empty practice directory:
rm -- atomic-relaxed-counter-demo.cpp atomic-relaxed-counter-demo
cd -- "$start_directory" || exit 1
rmdir -- "$work_directory"
unset start_directory work_directoryIf the directory is not empty, inspect its remaining contents before removing anything else.
Acknowledgements
This post was created with assistance from AI.