C++ Atomic Operations | 'Atomic Operations' Guide
이 글의 핵심
C++ std::atomic and how to prevent data races in multithreads using atomic operations. Explains the advantages over mutexes and practical code patterns.
What is an atomic operation?
Atomic Operation is an indivisible operation, meaning that intermediate states are not observed. Prevents data races in multithreaded environments.
#include <atomic>
std::atomic<int> counter{0};
// atomic increment
counter++; // thread safe
// non-atomic
int counter2 = 0;
counter2++; // not thread safe
Why do you need it?:
- 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
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 |
| // atomic: simple operation | ||
| std::atomic | ||
| counter++; |
// mutex: complex operation std::mutex mtx; std::map<int, int> data;
{ std::lock_guard lock(mtx); data[key] = value; }
## std::atomic
The following example demonstrates the concept in cpp:
```cpp
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
The following example demonstrates the concept in cpp:
```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
Here is the `Large` implementation:
```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
Here is the `push` implementation:
```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)
The following example demonstrates the concept in cpp:
```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.
The following example demonstrates the concept in cpp:
```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/)
---
## 같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- [C++ Atomic | '메모리 순서' 완벽 가이드](/blog/cpp-atomic-memory-order/)
- [C++ Lock-Free Programming | '락 프리 프로그래밍' 가이드](/blog/cpp-lock-free-programming/)
- [C++ Memory Order | '메모리 순서' 가이드](/blog/cpp-memory-order/)
---
## 이 글에서 다루는 키워드 (관련 검색어)
**C++**, **atomic**, **lock-free**, **C++11**, **concurrency** 등으로 검색하시면 이 글이 도움이 됩니다.