atomic


Include file: <atomic>

The <atomic> header in C++ provides a standardized way to perform atomic operations on data types. Atomic operations are indivisible, meaning they appear to occur instantaneously to other threads. This header is crucial for writing thread-safe code and implementing lock-free algorithms in concurrent programming.

Key Characteristics

Example 1: Basic Usage of std::atomic<int>

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

std::atomic<int> counter(0);

void increment_counter() {
    for (int i = 0; i < 1000; ++i) {
        counter++;
    }
}

int main() {
    std::thread t1(increment_counter);
    std::thread t2(increment_counter);

    t1.join();
    t2.join();

    std::cout << "Final counter value: " << counter << std::endl;

    return 0;
}

Explanation:

Example 2: Using std::atomic_flag for Simple Synchronization

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

std::atomic_flag lock = ATOMIC_FLAG_INIT;
int shared_resource = 0;

void increment_resource() {
    for (int i = 0; i < 100; ++i) {
        while (lock.test_and_set(std::memory_order_acquire)) { } // Spin lock
        shared_resource++;
        lock.clear(std::memory_order_release);
    }
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(increment_resource);
    }

    for (auto& t : threads) {
        t.join();
    }

    std::cout << "Final shared_resource value: " << shared_resource << std::endl;

    return 0;
}

Explanation:

Example 3: Memory Ordering with std::atomic<bool>

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

std::atomic<bool> x(false);
std::atomic<bool> y(false);
std::atomic<int> z(0);

void write_x_then_y() {
    x.store(true, std::memory_order_relaxed);
    y.store(true, std::memory_order_release);
}

void read_y_then_x() {
    while (!y.load(std::memory_order_acquire));
    if (x.load(std::memory_order_relaxed)) {
        ++z;
    }
}

int main() {
    std::thread a(write_x_then_y);
    std::thread b(read_y_then_x);
    a.join();
    b.join();

    std::cout << "z = " << z << std::endl;

    return 0;
}

Explanation:

Example 4: Lock-free Stack Using std::atomic

#include <atomic>
#include <memory>
#include <iostream>

template<typename T>
class lock_free_stack {
    struct node {
        T data;
        node* next;
        node(const T& data) : data(data), next(nullptr) {}
    };

    std::atomic<node*> head;

public:
    lock_free_stack() : head(nullptr) {}

    void push(const T& data) {
        node* new_node = new node(data);
        new_node->next = head.load(std::memory_order_relaxed);
        while (!head.compare_exchange_weak(new_node->next, new_node,
                                           std::memory_order_release,
                                           std::memory_order_relaxed));
    }

    bool pop(T& result) {
        node* old_head = head.load(std::memory_order_relaxed);
        while (old_head && !head.compare_exchange_weak(old_head, old_head->next,
                                                       std::memory_order_acquire,
                                                       std::memory_order_relaxed));
        if (old_head) {
            result = old_head->data;
            delete old_head;
            return true;
        }
        return false;
    }
};

int main() {
    lock_free_stack<int> stack;

    stack.push(1);
    stack.push(2);
    stack.push(3);

    int value;
    while (stack.pop(value)) {
        std::cout << "Popped: " << value << std::endl;
    }

    return 0;
}

Explanation:

Additional Considerations

  1. Performance: Atomic operations can be more expensive than non-atomic operations, but they're often faster than using mutexes for simple operations.

  2. ABA Problem: In lock-free algorithms, be aware of the ABA problem where a value changes from A to B and back to A, potentially causing issues in compare-and-swap operations.

  3. Memory Management: When using atomic pointers in lock-free data structures, proper memory management (like hazard pointers or reference counting) is crucial to avoid memory leaks and use-after-free bugs.

  4. Portability: While <atomic> is part of the C++ standard, the actual implementation of atomic operations may vary across different architectures.

Summary

The <atomic> header in C++ provides essential tools for concurrent programming:

By using atomic operations, developers can write more efficient and correct concurrent code, avoiding common pitfalls like race conditions and deadlocks. However, it's important to use these tools judiciously and understand their implications on performance and system behavior.

Related

Previous Page | Course Schedule | Course Content