[2026] C++ Atomic Operations | 원자적 연산 가이드

[2026] C++ Atomic Operations | 원자적 연산 가이드

이 글의 핵심

C++ std::atomic과 원자적 연산으로 멀티스레드에서 데이터 경쟁을 막는 방법. 뮤텍스 대비 장점과 실전 코드 패턴을 설명합니다.

원자적 연산이란?

원자적 연산 (Atomic Operation)분할 불가능한 연산으로, 중간 상태가 관찰되지 않습니다. 멀티스레드 환경에서 데이터 경쟁을 방지합니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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

원자적 연산 동작 원리: 아래 코드는 mermaid를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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✅ 가능❌ 불가
데드락❌ 없음✅ 가능
사용 사례카운터, 플래그복잡한 자료구조
아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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: 카운터

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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: 플래그

다음은 cpp를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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)

아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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 스택

다음은 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)) {
            // 재시도
        }
    }
    
    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 연산

다음은 cpp를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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: 비원자적 연산

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

std::atomic<int> x{0};
// ❌ 비원자적
x = x + 1;  // load + add + store (3단계)
// ✅ 원자적
x++;
x.fetch_add(1);

문제 2: ABA 문제

아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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: 메모리 순서

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ❌ 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: 크기 제한

아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ✅ 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 프로그래밍

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// 장점: 락 없음, 빠름
// 단점: 복잡함, 디버깅 어려움
// 간단한 경우만 사용
std::atomic<int> counter;  // OK
// 복잡하면 mutex
std::mutex mtx;
std::map<int, int> data;  // mutex로 보호

실무 패턴

패턴 1: 스핀락

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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: 작업 큐

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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 사용) 아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// lock-free 확인
std::atomic<int> x;
if (x.is_lock_free()) {
    std::cout << "빠름\n";
}

Q3: 메모리 순서는?

A:

  • relaxed: 빠름, 순서 보장 없음
  • acquire/release: 일반적, 순서 보장
  • seq_cst: 안전, 느림 (기본값) 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 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 프로그래밍의 핵심입니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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 변경을 탐지 못하는 문제입니다. 버전 카운터로 해결합니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.

struct Pointer {
    Node* ptr;
    size_t version;
};
std::atomic<Pointer> head;

Q6: atomic과 volatile의 차이는?

A:

  • atomic: 스레드 안전, 원자적 연산
  • volatile: 최적화 방지, 스레드 안전하지 않음 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ volatile: 스레드 안전하지 않음
volatile int counter = 0;
counter++;  // 경쟁 조건
// ✅ atomic: 스레드 안전
std::atomic<int> counter{0};
counter++;  // 원자적

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

A: 작은 타입만 lock-free입니다. 큰 타입은 내부적으로 lock을 사용할 수 있습니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

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

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

관련 글

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3