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
// 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/)