C++ Atomic Operations | "Atomic Operations" Guide

C++ Atomic Operations | "Atomic Operations" Guide

이 글의 핵심

A practical guide to C++ Atomic Operations.

What is an atomic operation?

Atomic Operation is an indivisible operation, meaning that intermediate states are not observed. Prevents data races in multithreaded environments.```cpp #include

std::atomic counter{0};

// atomic increment counter++; // thread safe

// non-atomic int counter2 = 0; counter2++; // not thread safe

- Thread Safety: Prevent data races
- Performance: Faster than mutex
- Lock-Free: No deadlock
- Synchronization: Guaranteed memory order```cpp
// ❌ Non-atomic: race condition
int counter = 0;

void increment() {
    counter++;  // 1. load, 2. add, 3. store (3 steps)
}

// Thread 1: load(0) -> add(1) -> store(1)
// Thread 2: load(0) -> add(1) -> store(1)
// Result: 1 (Expected: 2)

// ✅ Atomic: Safe
std::atomic<int> counter{0};

void increment() {
    counter++;  // Atomic (Level 1)
}

// Both Thread 1 and 2 safely increase
// Result: 2
```How ​​atomic operations work:```mermaid
flowchart TD
A[Thread 1: counter++] --> B{atomic?}
B -->|Yes| C[hardware atomic instruction]
    B -->|No| D[1. load]
    D --> E[2. add]
    E --> F[3. store]
C --> G[complete]
F --> H{Intervention of Thread 2?}
H -->|Yes| I[race condition]
    H -->|No| G
```Atomic operations vs mutex:

| Features | `std::atomic` | `std::mutex` |
|------|---------------|-------------|
| Performance | Fast | slow |
| Complexity | Low (simple operation) | High (complex operations) |
| Lock-Free | ✅ Available | ❌ Not possible |
| Deadlock | ❌ None | ✅ Available |
| Use cases | Counter, flag | Complex data structure |```cpp
// atomic: simple operation
std::atomic<int> counter{0};
counter++;

// mutex: complex operation
std::mutex mtx;
std::map<int, int> data;

{
    std::lock_guard lock(mtx);
    data[key] = value;
}

std::atomic

std::atomic<int> x{0};
std::atomic<bool> flag{false};
std::atomic<double> d{0.0};

// basic operations
x.store(10);
int value = x.load();
x.exchange(20);
```## Practical example

### Example 1: Counter```cpp
#include <atomic>
#include <thread>
#include <vector>

std::atomic<int> counter{0};

void increment() {
    for (int i = 0; i < 1000; i++) {
counter++;  // atomic
    }
}

int main() {
    std::vector<std::thread> threads;
    
    for (int i = 0; i < 10; i++) {
        threads.emplace_back(increment);
    }
    
    for (auto& t : threads) {
        t.join();
    }
    
    std::cout << counter << std::endl;  // 10000
}
```### Example 2: Flags```cpp
std::atomic<bool> done{false};

// Thread 1
void worker() {
    // get the job done
    done.store(true);
}

// Thread 2
void monitor() {
    while (!done.load()) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
    std::cout << "Done" << std::endl;
}
```### Example 3: CAS (Compare-And-Swap)```cpp
std::atomic<int> value{0};

void update() {
    int expected = 0;
    int desired = 10;
    
// If expected == value, change to desired
    if (value.compare_exchange_strong(expected, desired)) {
std::cout << "success" << std::endl;
    } else {
std::cout << "Failure: " << expected << std::endl;
    }
}
```### Example 4: Lock-Free Stack```cpp
template<typename T>
class LockFreeStack {
    struct Node {
        T data;
        Node* next;
    };
    
    std::atomic<Node*> head{nullptr};
    
public:
    void push(T value) {
        Node* newNode = new Node{value, head.load()};
        
        while (!head.compare_exchange_weak(newNode->next, newNode)) {
            // retry
        }
    }
    
    bool pop(T& result) {
        Node* oldHead = head.load();
        
        while (oldHead && 
               !head.compare_exchange_weak(oldHead, oldHead->next)) {
            // retry
        }
        
        if (oldHead) {
            result = oldHead->data;
            delete oldHead;
            return true;
        }
        return false;
    }
};
```## atomic operations```cpp
std::atomic<int> x{0};

// read/write
x.store(10);
int value = x.load();

// exchange
int old = x.exchange(20);

// CAS
int expected = 10;
x.compare_exchange_strong(expected, 20);

// Arithmetic
x.fetch_add(5);
x.fetch_sub(3);
x++;
x--;
```## Frequently occurring problems

### Problem 1: Non-atomic operations```cpp
std::atomic<int> x{0};

// ❌ Non-atomic
x = x + 1;  // load + add + store (step 3)

// ✅ Atomic
x++;
x.fetch_add(1);
```### Issue 2: ABA Issues```cpp
// A -> B -> A change not detected
std::atomic<Node*> head;

Node* oldHead = head.load();
// Another thread: A -> B -> A
head.compare_exchange_strong(oldHead, newNode);  // success (problem)

// ✅ Add version counter
struct Pointer {
    Node* ptr;
    size_t version;
};
std::atomic<Pointer> head;
```### Problem 3: Memory order```cpp
// ❌ relaxed (order not guaranteed)
data.store(42, std::memory_order_relaxed);
ready.store(true, std::memory_order_relaxed);

// ✅ acquire-release
data.store(42, std::memory_order_relaxed);
ready.store(true, std::memory_order_release);
```### Issue 4: Size limitations```cpp
// ✅ Check lock-free
std::atomic<int> x;
if (x.is_lock_free()) {
    std::cout << "Lock-free" << std::endl;
}

// Large types may not be lock-free
struct Large { int data[100]; };
std::atomic<Large> large;  // May not be lock-free
```## lock-free programming```cpp
// Pros: No locking, fast
// Cons: Complex, difficult to debug

// Use only in simple cases
std::atomic<int> counter;  // OK

// If it's complicated, it's a mutex
std::mutex mtx;
std::map<int, int> data;  // protected by mutex
```## Practice pattern

### Pattern 1: Spinlock```cpp
#include <atomic>
#include <thread>

class SpinLock {
    std::atomic<bool> flag_{false};
    
public:
    void lock() {
        while (flag_.exchange(true, std::memory_order_acquire)) {
// spin
            std::this_thread::yield();
        }
    }
    
    void unlock() {
        flag_.store(false, std::memory_order_release);
    }
};

// use
SpinLock spinlock;

void criticalSection() {
    spinlock.lock();
// critical section
    spinlock.unlock();
}
```### Pattern 2: Singleton (Double-Checked Locking)```cpp
#include <atomic>
#include <mutex>

class Singleton {
    static std::atomic<Singleton*> instance_;
    static std::mutex mutex_;
    
    Singleton() = default;
    
public:
    static Singleton* getInstance() {
        Singleton* tmp = instance_.load(std::memory_order_acquire);
        
        if (tmp == nullptr) {
            std::lock_guard lock(mutex_);
            tmp = instance_.load(std::memory_order_relaxed);
            
            if (tmp == nullptr) {
                tmp = new Singleton();
                instance_.store(tmp, std::memory_order_release);
            }
        }
        
        return tmp;
    }
};

std::atomic<Singleton*> Singleton::instance_{nullptr};
std::mutex Singleton::mutex_;
```### Pattern 3: Task Queue```cpp
#include <atomic>
#include <queue>
#include <mutex>

template<typename T>
class WorkQueue {
    std::queue<T> queue_;
    std::mutex mutex_;
    std::atomic<size_t> size_{0};
    
public:
    void push(T item) {
        {
            std::lock_guard lock(mutex_);
            queue_.push(std::move(item));
        }
        size_.fetch_add(1, std::memory_order_release);
    }
    
    bool pop(T& item) {
        if (size_.load(std::memory_order_acquire) == 0) {
            return false;
        }
        
        std::lock_guard lock(mutex_);
        if (queue_.empty()) {
            return false;
        }
        
        item = std::move(queue_.front());
        queue_.pop();
        size_.fetch_sub(1, std::memory_order_release);
        return true;
    }
    
    size_t size() const {
        return size_.load(std::memory_order_acquire);
    }
};
```## FAQ

### Q1: When do you use atomic?

**A**: 
- **Counter**: Thread safe increment/decrement
- **Flag**: Share state between threads
- **Lock-Free Data Structure**: High-performance synchronization```cpp
std::atomic<int> counter{0};
std::atomic<bool> done{false};
```### Q2: What is the performance?

**A**: 
- **Lock-Free**: Faster than mutex
- **Complex Type**: Can be slow (uses lock)```cpp
// Check lock-free
std::atomic<int> x;
if (x.is_lock_free()) {
std::cout << "fast\n";
}
```### Q3: What is the memory order?

**A**: 
- **`relaxed`**: fast, no order guarantee
- **`acquire/release`**: general, order guaranteed
- **`seq_cst`**: safe, slow (default)```cpp
// relaxed: fast
x.store(42, std::memory_order_relaxed);

// acquire/release: general
x.store(42, std::memory_order_release);
int v = x.load(std::memory_order_acquire);

// seq_cst: safe (default)
x.store(42);
```### Q4: What is CAS?

**A**: **Compare-And-Swap**, which uses `compare_exchange`. This is the core of lock-free programming.```cpp
std::atomic<int> value{0};

int expected = 0;
int desired = 10;

if (value.compare_exchange_strong(expected, desired)) {
std::cout << "Success\n";
}
```### Q5: What about ABA?

**A**: **The problem is that the change A → B → A cannot be detected**. Solved with **version counter**.```cpp
struct Pointer {
    Node* ptr;
    size_t version;
};

std::atomic<Pointer> head;
```### Q6: What is the difference between atomic and volatile?

**A**: 
- **`atomic`**: thread-safe, atomic operations
- **`volatile`**: anti-optimization, not thread-safe.```cpp
// ❌ volatile: not thread safe
volatile int counter = 0;
counter++;  // race condition

// ✅ atomic: thread safe
std::atomic<int> counter{0};
counter++;  // atomic
```### Q7: Can all types be made atomic?

**A**: **Only small types are lock-free**. Large types can use locks internally.```cpp
std::atomic<int> x;  // lock-free
std::atomic<std::string> s;  // May not be lock-free

// check
if (x.is_lock_free()) {
    std::cout << "Lock-free\n";
}
```### Q8: What are atomic learning resources?

**A**: 
- "C++ Concurrency in Action" by Anthony Williams
- "The Art of Multiprocessor Programming" by Maurice Herlihy
- [cppreference.com - Atomic](https://en.cppreference.com/w/cpp/atomic/atomic)

**Related article**: mutex, lock-free, [memory-order](/blog/cpp-memory-order/).

One-line summary: Atomic operations are indivisible operations, which prevent data races in a multithreaded environment.

---

## Good article to read together (internal link)

Here's another article related to this topic.

- [C++ Lock-Free Programming | "Lock Free Programming" Guide](/blog/cpp-lock-free-programming/)
- [C++ Memory Order | “Memory Order” Guide](/blog/cpp-memory-order/)
- [C++ Atomic | A complete guide to "Memory Order"](/blog/cpp-atomic-memory-order/)

## Practical tips

These are tips that can be applied right away in practice.

### Debugging tips
- If you run into a problem, check the compiler warnings first.
- Reproduce the problem with a simple test case

### Performance Tips
- Don't optimize without profiling
- Set measurable indicators first

### Code review tips
- Check in advance for areas that are frequently pointed out in code reviews.
- Follow your team's coding conventions

---
## Practical checklist

This is what you need to check when applying this concept in practice.

### Before writing code
- [ ] Is this technique the best way to solve the current problem?
- [ ] Can team members understand and maintain this code?
- [ ] Does it meet the performance requirements?

### Writing code
- [ ] Have you resolved all compiler warnings?
- [ ] Have you considered edge cases?
- [ ] Is error handling appropriate?

### When reviewing code
- [ ] Is the intent of the code clear?
- [ ] Are there enough test cases?
- [ ] Is it documented?

Use this checklist to reduce mistakes and improve code quality.

---

## Keywords covered in this article (related search terms)

This article will be helpful if you search for C++, atomic, lock-free, C++11, concurrency, etc.

---

## Related articles

- [C++ Lock-Free Programming | ](/blog/cpp-lock-free-programming/)
- [C++ Memory Order | ](/blog/cpp-memory-order/)
- [C++ Atomic | ](/blog/cpp-atomic-memory-order/)
- [C++ Lock-Free Programming Practice | CAS, ABA, memory order, high-performance queue [#34-3]](/blog/cpp-series-34-3-lock-free/)
- [C++ Lock-Free Programming Practice | CAS, ABA, memory order, high-performance queue [#51-5]](/blog/cpp-series-51-5-lock-free-programming/)