티스토리 뷰
🚀 1. 원자적 접근(Atomic Access) 이란?
원자적 접근(Atomic Access)이란 작업이 중단되지 않고 하나의 연산 단위로 실행되는 것을 의미합니다.
즉, 한 스레드가 연산을 수행하는 동안 다른 스레드가 끼어들 수 없으며, 실행이 완료되기 전까지는 어떤 중단이나 간섭도 허용되지 않습니다.
✅ 멀티스레드 환경에서 동시성을 보장하기 위해 필수적인 개념입니다.
✅ CPU가 지원하는 원자적 연산(Atomic Operations)을 이용하여 구현됩니다.
🔹 2. 원자적 연산(Atomic Operation)의 특징
- 중단 불가능 (Indivisible):
- 연산이 시작되면 도중에 인터럽트(Interrupt)되거나, 다른 스레드가 끼어들 수 없음
- 경쟁 조건(Race Condition) 방지:
- 여러 스레드가 동일한 메모리를 수정하려 할 때 발생하는 충돌을 방지
- 락 없이 동기화 가능:
- std::atomic과 같은 기능을 사용하면 락 없이도(Thread-safe) 안전한 데이터 업데이트 가능
🔹 3. 원자적 연산 종류
CPU 및 컴파일러는 몇 가지 기본 원자적 연산을 제공합니다.
연산 설명
Load (읽기) | 원자적으로 값을 읽음 |
Store (쓰기) | 원자적으로 값을 저장 |
Increment (증가) | x++ 원자적 증가 |
Decrement (감소) | x-- 원자적 감소 |
Compare-And-Swap (CAS) | 메모리 값이 예상 값과 같으면 변경 |
Fetch-And-Add | 현재 값을 가져온 후 증가 |
🔹 4. 원자적 접근을 위한 CPU 명령어
현대 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++에서 원자적 접근 구현
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 문제
🚨 6.1. ABA 문제란?
CAS를 사용할 때 값이 변경되었다가 다시 원래 값으로 돌아오면 문제가 발생할 수 있음.
🔹 예제 시나리오
- 스레드 A가 value=10을 확인
- 다른 스레드 B가 value=10 → 20 → 10 변경
- 스레드 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)
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)은 블로킹 없이 데이터 동기화를 보장하는 강력한 방법
✅ std::atomic을 활용하면 락(lock) 없이 동시성 제어 가능
✅ Compare-And-Swap (CAS) 기반의 Lock-Free 알고리즘 구현 가능
✅ ABA 문제는 Stamped Atomic 기법으로 해결 가능
✅ 메모리 순서를 활용하여 성능 최적화 가능 🚀
'개발 > 그 외 개발관련' 카테고리의 다른 글
자동차 CAN 통신 개요 (0) | 2025.02.07 |
---|---|
일반 증가 연산 vs 원자적 증가 연산 차이점 ( counter++ ) (0) | 2025.02.05 |
CanTp (CAN Transport Protocol) 모듈 (0) | 2025.02.03 |
BSW(Basic Software)에서 ComM 모듈 (0) | 2025.02.03 |
AUTOSAR Communication Manager(ComM) 네트워크 상태 자세히 알아보기 (1) | 2025.01.24 |