티스토리 뷰

개발/그 외 개발관련

원자적 접근 (Atomic Access) C++?

부캐: 개발하는 조대리 2025. 2. 5. 12:46
반응형

🚀 1. 원자적 접근(Atomic Access) 이란?

원자적 접근 (Atomic Access)

원자적 접근(Atomic Access)이란 작업이 중단되지 않고 하나의 연산 단위로 실행되는 것을 의미합니다.
즉, 한 스레드가 연산을 수행하는 동안 다른 스레드가 끼어들 수 없으며, 실행이 완료되기 전까지는 어떤 중단이나 간섭도 허용되지 않습니다.

멀티스레드 환경에서 동시성을 보장하기 위해 필수적인 개념입니다.
CPU가 지원하는 원자적 연산(Atomic Operations)을 이용하여 구현됩니다.

 

🔹 2. 원자적 연산(Atomic Operation)의 특징

원자적 접근 (Atomic Access)

  1. 중단 불가능 (Indivisible):
    • 연산이 시작되면 도중에 인터럽트(Interrupt)되거나, 다른 스레드가 끼어들 수 없음
  2. 경쟁 조건(Race Condition) 방지:
    • 여러 스레드가 동일한 메모리를 수정하려 할 때 발생하는 충돌을 방지
  3. 락 없이 동기화 가능:
    • std::atomic과 같은 기능을 사용하면 락 없이도(Thread-safe) 안전한 데이터 업데이트 가능

 

🔹 3. 원자적 연산 종류

원자적 접근 (Atomic Access)

CPU 및 컴파일러는 몇 가지 기본 원자적 연산을 제공합니다.

연산 설명

Load (읽기) 원자적으로 값을 읽음
Store (쓰기) 원자적으로 값을 저장
Increment (증가) x++ 원자적 증가
Decrement (감소) x-- 원자적 감소
Compare-And-Swap (CAS) 메모리 값이 예상 값과 같으면 변경
Fetch-And-Add 현재 값을 가져온 후 증가

 

🔹 4. 원자적 접근을 위한 CPU 명령어

원자적 접근 (Atomic Access)

현대 CPU는 원자적 연산을 수행하기 위해 특수한 명령어를 지원합니다.

x86 / x86-64 CPU의 주요 원자적 명령어

  • LOCK XADD: 원자적 증가 연산 (Fetch-And-Add)
  • LOCK CMPXCHG: 비교 후 교환 (Compare-And-Swap)
  • LOCK INC/DEC: 원자적 증가/감소

🔹 예제: CMPXCHG 명령어 (CAS)

LOCK CMPXCHG [mem], reg
  • 메모리 [mem] 값이 EAX 레지스터와 같으면 reg 값으로 변경

 

🔹 5. C++에서 원자적 접근 구현

원자적 접근 (Atomic Access)

C++에서는 std::atomic을 사용하여 원자적 연산을 수행할 수 있습니다.

5.1. 원자적 증가/감소 연산 (fetch_add, fetch_sub)

#include <iostream>
#include <atomic>
#include <thread>

std::atomic<int> counter(0);

void increment() {
    for (int i = 0; i < 1000; ++i) {
        counter.fetch_add(1, std::memory_order_relaxed); // 원자적 증가
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);

    t1.join();
    t2.join();

    std::cout << "최종 카운터 값: " << counter.load() << std::endl; // 2000
    return 0;
}

🔹 설명

  • std::atomic<int> counter(0);
    → counter 변수를 원자적으로 접근 가능하게 선언
  • fetch_add(1, std::memory_order_relaxed);
    → counter++을 원자적으로 실행 (경쟁 조건 없음)

5.2. Compare-And-Swap (CAS)

#include <iostream>
#include <atomic>

std::atomic<int> value(10);

void compare_and_swap(int expected, int new_value) {
    if (value.compare_exchange_strong(expected, new_value)) {
        std::cout << "변경 성공! 새로운 값: " << value.load() << std::endl;
    } else {
        std::cout << "변경 실패! 현재 값: " << value.load() << std::endl;
    }
}

int main() {
    int expected = 10;
    compare_and_swap(expected, 20); // 성공
    compare_and_swap(expected, 30); // 실패 (expected != 현재 값)

    return 0;
}

🔹 설명

  • compare_exchange_strong(expected, new_value);
    • 현재 값이 expected와 같으면 new_value로 변경
    • 다르면 실패하고 expected 값이 현재 값으로 자동 업데이트됨

 

🔹 6. ABA 문제

원자적 접근 (Atomic Access)

🚨 6.1. ABA 문제란?

CAS를 사용할 때 값이 변경되었다가 다시 원래 값으로 돌아오면 문제가 발생할 수 있음.

🔹 예제 시나리오

  1. 스레드 A가 value=10을 확인
  2. 다른 스레드 B가 value=10 → 20 → 10 변경
  3. 스레드 A가 CAS(10 → 30)을 실행하면 성공하지만 실제로 값이 변경되었음을 감지할 수 없음

6.2. ABA 해결 방법: Tag 추가

struct AtomicStamped {
    std::atomic<int> value;
    std::atomic<int> tag;
    
    bool compare_and_swap(int expected_value, int expected_tag, int new_value) {
        if (value == expected_value && tag == expected_tag) {
            value = new_value;
            tag++;
            return true;
        }
        return false;
    }
};

변경 횟수를 기록하는 tag 추가하여 ABA 문제 해결

 

🔹 7. 원자적 연산의 메모리 순서 (Memory Order)

원자적 접근 (Atomic Access)

C++에서는 메모리 순서를 제어하여 원자적 연산의 동작 방식을 지정할 수 있습니다.

순서 설명

memory_order_relaxed 연산 순서를 보장하지 않음 (최고 성능)
memory_order_acquire 이전 연산이 완료될 때까지 기다림
memory_order_release 이후 연산이 시작되기 전까지 실행
memory_order_seq_cst 모든 스레드에서 순서를 보장

예제

std::atomic<int> x(0), y(0);

void thread1() {
    x.store(1, std::memory_order_relaxed);
    y.store(2, std::memory_order_release);
}

void thread2() {
    while (y.load(std::memory_order_acquire) != 2);
    std::cout << "x: " << x.load(std::memory_order_relaxed) << std::endl;
}

memory_order_release + memory_order_acquire를 사용하여 순서를 제어할 수 있음

 

🔹 8. 결론

원자적 접근 (Atomic Access)

원자적 접근(Atomic Access)블로킹 없이 데이터 동기화를 보장하는 강력한 방법
std::atomic을 활용하면 락(lock) 없이 동시성 제어 가능
Compare-And-Swap (CAS) 기반의 Lock-Free 알고리즘 구현 가능
ABA 문제는 Stamped Atomic 기법으로 해결 가능
메모리 순서를 활용하여 성능 최적화 가능 🚀