티스토리 뷰

반응형

🚀 1. 일반적인 counter++ 연산 과정

일반 증가 연산 vs 원자적 증가 연산

C++에서 counter++은 단순한 한 줄 코드처럼 보이지만, 실제로는 여러 개의 기계어 명령어로 분해됩니다.

1.1. counter++의 내부 연산

int counter = 0;

void increment() {
    counter++; // 일반 증가 연산
}

🔹 어셈블리 코드 변환 (x86-64)

일반 증가 연산 vs 원자적 증가 연산

mov eax, [counter]   ; counter 값을 레지스터로 로드
add eax, 1           ; 1 증가
mov [counter], eax   ; 증가된 값을 메모리에 저장

이 연산은 3단계로 이루어지며, 한 스레드가 실행하는 동안 다른 스레드가 동시에 접근할 경우 경쟁 조건(Race Condition)이 발생할 수 있음.

 

2. 일반적인 counter++에서 발생할 수 있는 문제

일반 증가 연산 vs 원자적 증가 연산

🔹 2.1. 경쟁 조건 (Race Condition)

만약 counter++가 멀티스레드 환경에서 동시에 실행된다면, 이전 값이 덮어씌워지는 문제가 발생할 수 있음.

int counter = 0;

void increment() {
    for (int i = 0; i < 1000000; ++i) {
        counter++;  // 경쟁 조건 발생 가능!
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    t1.join();
    t2.join();
    
    std::cout << "최종 카운터 값: " << counter << std::endl; 
    return 0;
}

🔹 예상 출력 값

최종 카운터 값: 2000000 (예상)

🔹 실제 출력 값 (문제 발생!)

최종 카운터 값: 1456325 (스레드 충돌 발생)

❌ counter++ 연산 중 스레드 충돌로 인해 일부 증가 연산이 손실됨.

🔹 2.2. 실행 순서 문제 (Interleaving Problem)

만약 두 개의 스레드가 counter++을 실행하면 다음과 같은 순서로 실행될 수도 있습니다.

  1. 스레드 A: mov eax, [counter] (counter = 0)
  2. 스레드 B: mov eax, [counter] (counter = 0) ⬅ A와 B가 같은 값 읽음
  3. 스레드 A: add eax, 1 (eax = 1)
  4. 스레드 B: add eax, 1 (eax = 1) ⬅ A와 B가 독립적으로 1을 더함
  5. 스레드 A: mov [counter], eax (counter = 1)
  6. 스레드 B: mov [counter], eax (counter = 1) ⬅ B가 A의 변경 사항을 덮어씀!

결과적으로, 두 번 증가해야 할 값이 한 번만 증가하는 문제 발생!

 

🚀 3. 원자적 증가 연산 (std::atomic)

일반 증가 연산 vs 원자적 증가 연산

이 문제를 해결하기 위해 std::atomic을 사용하면, counter++이 **하나의 원자적 연산(Atomic Operation)**으로 처리됩니다.

3.1. 원자적 연산을 이용한 해결 방법

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

std::atomic<int> counter(0);  // 원자적 변수 선언

void increment() {
    for (int i = 0; i < 1000000; ++i) {
        counter++;  // 원자적 증가 연산 (동기화 문제 없음)
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    t1.join();
    t2.join();
    
    std::cout << "최종 카운터 값: " << counter.load() << std::endl;
    return 0;
}

🔹 출력 결과

최종 카운터 값: 2000000 (정확한 결과)

모든 증가 연산이 정확하게 수행됨.

 

🔹 4. 원자적 연산(std::atomic) 내부 동작

일반 증가 연산 vs 원자적 증가 연산

std::atomic<int>를 사용하면 CPU가 LOCK XADD 명령어를 사용하여 원자적으로 연산을 수행합니다.

🔹 어셈블리 변환 (x86-64)

LOCK XADD [counter], 1

✔ LOCK 프리픽스는 다른 스레드가 접근하지 못하도록 방지하여 counter++을 단일 명령어로 실행할 수 있도록 합니다.

 

🔹 5. std::atomic vs mutex 차이

일반 증가 연산 vs 원자적 증가 연산

비교 항목 std::atomic std::mutex
실행 속도 빠름 (CPU 레벨) 느림 (OS 컨텍스트 스위칭)
데드락 발생 ❌ 없음 ✅ 가능성 있음
스레드 경합 시 성능 ✅ 우수 ❌ 경합이 심할수록 성능 저하
구현 난이도 쉬움 (counter++) 상대적으로 어려움 (lock/unlock)

std::atomic은 std::mutex보다 훨씬 빠르고 효율적입니다.
✔ 하지만 **복잡한 데이터 구조(예: 큐, 리스트)**에서는 락이 필요할 수도 있음.

 

🚀 6. fetch_add() vs counter++ 차이

일반 증가 연산 vs 원자적 증가 연산

C++에서는 counter++을 원자적으로 실행하는 방법으로 fetch_add()를 사용할 수 있습니다.

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

// 일반 counter++
counter++;

// fetch_add() 방식
counter.fetch_add(1, std::memory_order_relaxed);

둘 다 같은 동작을 수행하지만, fetch_add()가 보다 명확한 방식이며 메모리 순서를 직접 지정 가능

 

🔹 7. 정리

✅ counter++은 멀티스레드 환경에서 경쟁 조건(Race Condition)을 일으킬 수 있음.
✅ std::atomic을 사용하면 락 없이도 안전한 동기화가 가능하며, CPU의 LOCK XADD 같은 원자적 명령어를 사용하여 빠르게 실행됨.
복잡한 데이터 구조(큐, 스택 등)는 lock-free 알고리즘을 적용해야 할 수도 있음.

 

🔥 결론: 멀티스레드 환경에서는 std::atomic을 사용하여 counter++을 안전하게 실행해야 한다! 🚀

 

'개발 > 그 외 개발관련' 카테고리의 다른 글

자동차 통신 CAN FD란?  (0) 2025.02.07
자동차 CAN 통신 개요  (0) 2025.02.07
원자적 접근 (Atomic Access) C++?  (0) 2025.02.05
CanTp (CAN Transport Protocol) 모듈  (0) 2025.02.03
BSW(Basic Software)에서 ComM 모듈  (0) 2025.02.03