메모리 4영역을 스레드 관점으로 보기
영역별 특성 정리
| 영역 | 대표 예시 | 프로세스 내 공유 여부 | 멀티스레드 위험도 | 핵심 포인트 |
|---|
| 코드(Text) | 함수 본문, 명령어 | 공유 | 낮음 | 보통 읽기 전용 |
| 스택(Stack) | 지역 변수, 함수 인자 | 스레드별 독립 | 낮음 | 각 스레드가 자기 스택 사용 |
| 힙(Heap) | new, make_shared로 만든 객체 | 공유 가능 | 높음 | "공유된 힙 객체"가 위험 |
| 데이터(Data/BSS) | 전역 변수, static 변수 | 공유 | 높음 | 동시 쓰기 시 레이스 발생 |
안전성 판단 2문제
- 질문 1: 같은 메모리 주소를 둘 이상 스레드가 접근하는가?
- 질문 2: 그 접근 중 쓰기(write)가 하나라도 있는가?
- 두 질문이 모두 Yes면 동기화(
mutex, atomic 등)가 필요합니다.
- 여러 스레드의 읽기 전용 접근은 일반적으로 안전합니다.
포인터와 힙을 정확히 구분하기
int* p = new int(100);
p 변수 자체는 스택에 있으므로 스레드마다 독립일 수 있습니다.
- 하지만
p가 가리키는 힙 주소를 공유하면 즉시 공유 자원이 됩니다.
- 핵심은 "힙이냐 아니냐"보다 "같은 객체를 공유하느냐"입니다.
레이스 컨디션 재현과 해결
레이스 컨디션 재현
#include <iostream>
#include <thread>
int counter = 0;
void Work() {
for (int i = 0; i < 1'000'000; ++i) {
counter++;
}
}
int main() {
std::thread t1(Work);
std::thread t2(Work);
t1.join();
t2.join();
std::cout << counter << '\n';
}
counter++는 읽기-수정-쓰기의 복합 연산이라 경쟁 상태가 발생합니다.
- 결과가 매번 다르게 나오는 것 자체가 레이스의 대표 신호입니다.
해결 1 - mutex
#include <mutex>
std::mutex m;
int counter = 0;
void Work() {
for (int i = 0; i < 1'000'000; ++i) {
std::lock_guard<std::mutex> lock(m);
++counter;
}
}
- 장점: 여러 변수/불변식(invariant)을 함께 보호하기 쉽습니다.
- 단점: 락 경합이 커지면 성능이 떨어질 수 있습니다.
해결 2 - atomic
#include <atomic>
std::atomic<int> counter{0};
void Work() {
for (int i = 0; i < 1'000'000; ++i) {
counter.fetch_add(1, std::memory_order_relaxed);
}
}
- 장점: 단순 카운터/플래그에 매우 유용하고 락 오버헤드가 적습니다.
- 주의: 복합 규칙(예: "HP 감소 + 상태 전환")은 atomic만으로 안전하지 않을 수 있습니다.
atomic vs mutex 선택 기준
| 도구 | 적합한 상황 | 장점 | 주의점 |
|---|
| atomic | 카운터, 플래그, 단일 변수 상태 | 빠르고 간결 | 복합 불변식 보호 어려움 |
| mutex | 여러 변수 동시 보호, 트랜잭션성 갱신 | 표현력 높음, 범용 | 락 경합/교착 가능성 관리 필요 |
강의 시 유의사항
강조 포인트
- "힙 = 무조건 위험"이 아니라 "공유 + 쓰기"가 위험 조건입니다.
counter++가 원자적이지 않다는 사실을 반드시 시연하세요.
- 락은 "최대한 짧게" 잡되, 데이터 불변식이 깨지지 않게 설계해야 합니다.
자주 하는 오해
| 오해 | 바로잡기 |
|---|
atomic은 항상 mutex보다 좋다 | 단일 상태에는 유리하지만 복합 상태 보호에는 한계가 있음 |
| 지역 변수는 항상 안전하다 | 지역 포인터가 공유 객체를 가리키면 위험할 수 있음 |
| 읽기만 해도 무조건 락이 필요하다 | 불변 데이터에 대한 동시 읽기는 일반적으로 안전 |
체크 질문 (스스로 답해보기)
- "힙 + 공유 + 쓰기" 조건을 실제 코드에서 찾을 수 있는가?
counter++가 왜 원자적 연산이 아닌지 설명할 수 있는가?
- 어떤 상황에서
atomic 대신 mutex를 선택해야 하는가?