C++ Atomic Operations | "원자적 연산" 가이드

C++ Atomic Operations | "원자적 연산" 가이드

이 글의 핵심

C++ Atomic Operations에 대한 실전 가이드입니다.

원자적 연산이란?

원자적 연산 (Atomic Operation)분할 불가능한 연산으로, 중간 상태가 관찰되지 않습니다. 멀티스레드 환경에서 데이터 경쟁을 방지합니다.

#include <atomic>

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

// 원자적 증가
counter++;  // 스레드 안전

// 비원자적
int counter2 = 0;
counter2++;  // 스레드 안전하지 않음

왜 필요한가?:

  • 스레드 안전: 데이터 경쟁 방지
  • 성능: 뮤텍스보다 빠름
  • Lock-Free: 데드락 없음
  • 동기화: 메모리 순서 보장
// ❌ 비원자적: 경쟁 조건
int counter = 0;

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

// Thread 1: load(0) -> add(1) -> store(1)
// Thread 2: load(0) -> add(1) -> store(1)
// 결과: 1 (예상: 2)

// ✅ 원자적: 안전
std::atomic<int> counter{0};

void increment() {
    counter++;  // 원자적 (1단계)
}

// Thread 1, 2 모두 안전하게 증가
// 결과: 2

원자적 연산 동작 원리:

flowchart TD
    A[Thread 1: counter++] --> B{원자적?}
    B -->|Yes| C[하드웨어 원자적 명령]
    B -->|No| D[1. load]
    D --> E[2. add]
    E --> F[3. store]
    C --> G[완료]
    F --> H{Thread 2 개입?}
    H -->|Yes| I[경쟁 조건]
    H -->|No| G

원자적 연산 vs 뮤텍스:

특징std::atomicstd::mutex
성능빠름느림
복잡도낮음 (단순 연산)높음 (복잡한 연산)
Lock-Free✅ 가능❌ 불가
데드락❌ 없음✅ 가능
사용 사례카운터, 플래그복잡한 자료구조
// atomic: 단순 연산
std::atomic<int> counter{0};
counter++;

// mutex: 복잡한 연산
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};

// 기본 연산
x.store(10);
int value = x.load();
x.exchange(20);

실전 예시

예시 1: 카운터

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

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

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

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
}

예시 2: 플래그

std::atomic<bool> done{false};

// Thread 1
void worker() {
    // 작업 수행
    done.store(true);
}

// Thread 2
void monitor() {
    while (!done.load()) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
    std::cout << "완료" << std::endl;
}

예시 3: CAS (Compare-And-Swap)

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

void update() {
    int expected = 0;
    int desired = 10;
    
    // expected == value면 desired로 변경
    if (value.compare_exchange_strong(expected, desired)) {
        std::cout << "성공" << std::endl;
    } else {
        std::cout << "실패: " << expected << std::endl;
    }
}

예시 4: Lock-Free 스택

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)) {
            // 재시도
        }
    }
    
    bool pop(T& result) {
        Node* oldHead = head.load();
        
        while (oldHead && 
               !head.compare_exchange_weak(oldHead, oldHead->next)) {
            // 재시도
        }
        
        if (oldHead) {
            result = oldHead->data;
            delete oldHead;
            return true;
        }
        return false;
    }
};

atomic 연산

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

// 읽기/쓰기
x.store(10);
int value = x.load();

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

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

// 산술
x.fetch_add(5);
x.fetch_sub(3);
x++;
x--;

자주 발생하는 문제

문제 1: 비원자적 연산

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

// ❌ 비원자적
x = x + 1;  // load + add + store (3단계)

// ✅ 원자적
x++;
x.fetch_add(1);

문제 2: ABA 문제

// A -> B -> A 변경 탐지 못함
std::atomic<Node*> head;

Node* oldHead = head.load();
// 다른 스레드: A -> B -> A
head.compare_exchange_strong(oldHead, newNode);  // 성공 (문제)

// ✅ 버전 카운터 추가
struct Pointer {
    Node* ptr;
    size_t version;
};
std::atomic<Pointer> head;

문제 3: 메모리 순서

// ❌ relaxed (순서 보장 안됨)
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);

문제 4: 크기 제한

// ✅ lock-free 확인
std::atomic<int> x;
if (x.is_lock_free()) {
    std::cout << "Lock-free" << std::endl;
}

// 큰 타입은 lock-free 아닐 수 있음
struct Large { int data[100]; };
std::atomic<Large> large;  // lock-free 아닐 수 있음

lock-free 프로그래밍

// 장점: 락 없음, 빠름
// 단점: 복잡함, 디버깅 어려움

// 간단한 경우만 사용
std::atomic<int> counter;  // OK

// 복잡하면 mutex
std::mutex mtx;
std::map<int, int> data;  // mutex로 보호

실무 패턴

패턴 1: 스핀락

#include <atomic>
#include <thread>

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

// 사용
SpinLock spinlock;

void criticalSection() {
    spinlock.lock();
    // 임계 영역
    spinlock.unlock();
}

패턴 2: 싱글톤 (Double-Checked Locking)

#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_;

패턴 3: 작업 큐

#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: atomic은 언제 사용하나요?

A:

  • 카운터: 스레드 안전한 증가/감소
  • 플래그: 스레드 간 상태 공유
  • Lock-Free 자료구조: 고성능 동기화
std::atomic<int> counter{0};
std::atomic<bool> done{false};

Q2: 성능은?

A:

  • Lock-Free: 뮤텍스보다 빠름
  • 복잡한 타입: 느릴 수 있음 (lock 사용)
// lock-free 확인
std::atomic<int> x;
if (x.is_lock_free()) {
    std::cout << "빠름\n";
}

Q3: 메모리 순서는?

A:

  • relaxed: 빠름, 순서 보장 없음
  • acquire/release: 일반적, 순서 보장
  • seq_cst: 안전, 느림 (기본값)
// relaxed: 빠름
x.store(42, std::memory_order_relaxed);

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

// seq_cst: 안전 (기본)
x.store(42);

Q4: CAS는 무엇인가요?

A: Compare-And-Swap으로, compare_exchange를 사용합니다. Lock-Free 프로그래밍의 핵심입니다.

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

int expected = 0;
int desired = 10;

if (value.compare_exchange_strong(expected, desired)) {
    std::cout << "성공\n";
}

Q5: ABA 문제는?

A: A → B → A 변경을 탐지 못하는 문제입니다. 버전 카운터로 해결합니다.

struct Pointer {
    Node* ptr;
    size_t version;
};

std::atomic<Pointer> head;

Q6: atomic과 volatile의 차이는?

A:

  • atomic: 스레드 안전, 원자적 연산
  • volatile: 최적화 방지, 스레드 안전하지 않음
// ❌ volatile: 스레드 안전하지 않음
volatile int counter = 0;
counter++;  // 경쟁 조건

// ✅ atomic: 스레드 안전
std::atomic<int> counter{0};
counter++;  // 원자적

Q7: 모든 타입을 atomic으로 만들 수 있나요?

A: 작은 타입만 lock-free입니다. 큰 타입은 내부적으로 lock을 사용할 수 있습니다.

std::atomic<int> x;  // lock-free
std::atomic<std::string> s;  // lock-free 아닐 수 있음

// 확인
if (x.is_lock_free()) {
    std::cout << "Lock-free\n";
}

Q8: atomic 학습 리소스는?

A:

  • “C++ Concurrency in Action” by Anthony Williams
  • “The Art of Multiprocessor Programming” by Maurice Herlihy
  • cppreference.com - Atomic

관련 글: mutex, lock-free, memory-order.

한 줄 요약: 원자적 연산은 분할 불가능한 연산으로, 멀티스레드 환경에서 데이터 경쟁을 방지합니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++ Lock-Free Programming | “락 프리 프로그래밍” 가이드
  • C++ Memory Order | “메모리 순서” 가이드
  • C++ Atomic | “메모리 순서” 완벽 가이드

관련 글

  • C++ Lock-Free Programming |
  • C++ Memory Order |
  • C++ Atomic |
  • C++ Lock-Free 프로그래밍 실전 | CAS·ABA·메모리 순서·고성능 큐 [#34-3]
  • C++ Lock-Free 프로그래밍 실전 | CAS·ABA·메모리 순서·고성능 큐 [#51-5]