A Concise Guide to C++ std::atomic

When it comes to thread safety in multithreaded programming, many people’s first reaction islocks (mutex)πŸ”’.

For example, with a counter, some threads are incrementing while others are decrementing.

To prevent multiple threads from operating simultaneously, we can use locks for protection.

But is using a lock for a counter a bit too costly?

Is there a more lightweight way? β€” Yes, it is<span><span>std::atomic</span></span>.

What is <span><span>std::atomic</span></span>❓

In simple terms, <span><span>std::atomic<T></span></span> is an atomic type, which guarantees that read and write operations on the variable are indivisible. This means that in a multithreaded environment, there will be no “half-read” or “half-write” situations.

We can think of it as a variable that is intrinsically thread-safe, requiring no explicit locking.

What types can it be used on❓

  • Integer types (<span><span>int</span></span>, <span><span>long</span></span>, <span><span>uint64_t</span></span> …)
  • Pointers (<span><span>T*</span></span>)
  • Other types that are trivially copyable (i.e., can be copied byte-for-byte, such as POD types)

Common Operations πŸ› 

<span><span>std::atomic</span></span> provides several common operations:

  • <span><span>load()</span></span>: Read value
  • <span><span>store()</span></span>: Write value
  • <span><span>fetch_add(x)</span></span> / <span><span>fetch_sub(x)</span></span>: Add/Subtract
  • <span><span>exchange(x)</span></span>: Replace value and return old value

These operations are atomic, making them more efficient than using locks.

Example ✨

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

int main() {
    std::atomic<int> value{0};

    // 1. store and load
    value.store(10);
    std::cout << "Initial value: " << value.load() << std::endl;

    // 2. fetch_add
    int old = value.fetch_add(5);  // old value = 10, new value = 15
    std::cout << "After fetch_add(5): old = " << old << ", new = " << value.load() << std::endl;

    // 3. fetch_sub
    old = value.fetch_sub(3);      // old value = 15, new value = 12
    std::cout << "After fetch_sub(3): old = " << old << ", new = " << value.load() << std::endl;

    // 4. exchange
    old = value.exchange(42);      // replace with 42
    std::cout << "After exchange(42): old = " << old << ", new = " << value.load() << std::endl;

    // 5. Multithreaded counter
    std::atomic<int> counter{0};
    auto worker = [&counter]() {
        for (int i = 0; i < 1000; i++) {
            counter.fetch_add(1);
        }
    };

    std::vector<std::thread> threads;
    for (int i = 0; i < 4; i++) {
        threads.emplace_back(worker);
    }
    for (auto &t : threads) {
        t.join();
    }
    std::cout << "Final counter (should be 4000): " << counter.load() << std::endl;

    return 0;
}

After running, you will see:

Initial value: 10
After fetch_add(5): old = 10, new = 15
After fetch_sub(3): old = 15, new = 12
After exchange(42): old = 12, new = 42
Final counter (should be 4000): 4000

Further Reading πŸ”

Here we have only introduced the most common usages, but <span><span>std::atomic</span></span> also has some more powerful features:

  • Compare and Swap (CAS): <span><span>compare_exchange_weak</span></span> / <span><span>compare_exchange_strong</span></span>, the cornerstone of lock-free data structures.
  • Memory Order: Control the read and write order between different threads, such as <span><span>memory_order_relaxed</span></span>, <span><span>memory_order_acquire</span></span>, <span><span>memory_order_release</span></span><code><span><span>.</span></span>

Conclusion 🎯

Next time you write a multithreaded counter or flag, don’t rush to lock it; try using <span><span>std::atomic</span></span>, it’s simple and efficient!πŸš€

Leave a Comment