Atomic Counters in C++

Our example demonstrates the use of atomic counters in C++. While C++ doesn’t have built-in goroutines, we’ll use threads to showcase concurrent operations on a shared counter.

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

int main() {
    // We'll use an atomic integer type to represent our
    // (always-positive) counter.
    std::atomic<uint64_t> ops(0);

    // We'll start 50 threads that each increment the
    // counter exactly 1000 times.
    std::vector<std::thread> threads;
    for (int i = 0; i < 50; i++) {
        threads.emplace_back([&ops]() {
            for (int c = 0; c < 1000; c++) {
                // To atomically increment the counter we use fetch_add.
                ops.fetch_add(1, std::memory_order_relaxed);
            }
        });
    }

    // Wait until all the threads are done.
    for (auto& t : threads) {
        t.join();
    }

    // Here no threads are writing to 'ops', but using
    // load() it's safe to atomically read a value even while
    // other threads are (atomically) updating it.
    std::cout << "ops: " << ops.load(std::memory_order_relaxed) << std::endl;

    return 0;
}

To compile and run this program:

$ g++ -std=c++11 -pthread atomic_counters.cpp -o atomic_counters
$ ./atomic_counters
ops: 50000

We expect to get exactly 50,000 operations. Had we used a non-atomic integer and incremented it with ops++, we’d likely get a different number, changing between runs, because the threads would interfere with each other. Moreover, we’d get undefined behavior due to data races.

In this C++ version:

  1. We use std::atomic<uint64_t> instead of Go’s atomic.Uint64.
  2. We use std::thread instead of goroutines.
  3. We use std::vector<std::thread> to manage our threads instead of a WaitGroup.
  4. We use fetch_add() for atomic increments instead of Go’s Add().
  5. We use load() to safely read the final value.

Note that C++ atomic operations allow specifying memory ordering. In this example, we use std::memory_order_relaxed for simplicity, but in real-world scenarios, you might need stronger ordering guarantees depending on your specific requirements.

Next, we’ll look at mutexes, another tool for managing state in concurrent programs.