[OS] 경쟁 상태 (Race Condition) & 세마포어 / 뮤텍스
경쟁 상태 (Race Condition)
📌 개념
공유 자원에 대해 여러 프로세스가 동시에 접근할 때, 실행 순서에 따라 결과값이 달라질 수 있는 상태
동시 접근 시 자료의 일관성(Consistency)이 깨질 수 있으며, 이는 예측 불가능한 버그로 이어진다.
간단한 예시:
1
2
3
4
5
6
7
전역 변수 count = 0
Thread A: count = count + 1 (읽기 → 연산 → 쓰기)
Thread B: count = count + 1 (읽기 → 연산 → 쓰기)
기대 결과: count = 2
실제 결과: count = 1 ← 두 스레드가 동시에 읽어 덮어씀
이처럼 연산이 원자적(Atomic)으로 수행되지 않을 때 경쟁 상태가 발생한다.
⚠️ Race Condition이 발생하는 경우
1. 커널 작업 수행 중 인터럽트 발생
- 문제: 커널 모드에서 데이터를 로드하여 작업 중, 인터럽트가 발생해 같은 데이터를 또 조작하는 경우
- 해결: 커널 모드에서 작업하는 동안 인터럽트를 disable 시켜 CPU 제어권을 빼앗기지 않도록 함
1
2
3
4
[커널 작업 중]
1. count 값 읽음 (count = 5)
← 인터럽트 발생! 인터럽트 핸들러가 count를 6으로 변경
2. 원래 작업이 5 기준으로 연산 후 저장 → 인터럽트 결과 덮어씌워짐
2. System Call 중 Context Switching 발생
- 문제: 프로세스 1이 커널 모드에서 데이터를 조작하는 도중, 타임아웃으로 CPU 제어권이 프로세스 2로 넘어가 같은 데이터를 조작하는 경우 → 프로세스 2의 작업이 반영되지 않음
- 해결: 커널 모드에서 작업 중일 때는 타임아웃이 발생해도 CPU 제어권을 넘기지 않도록 함
1
2
3
Process 1: read(count=5) → [time out] → CPU 넘어감
Process 2: read(count=5) → count=6 저장 → CPU 반환
Process 1: count=6 저장 → Process 2의 결과 덮어씌워짐
3. 멀티 프로세서 환경에서 공유 메모리 접근
- 문제: 멀티 프로세서 환경에서 2개의 CPU가 동시에 커널 내부의 공유 데이터에 접근하여 조작하는 경우
- 해결: 공유 데이터에 접근할 때마다 lock / unlock 방식으로 상호 배제를 보장
1
2
CPU 1: lock → 공유 데이터 조작 → unlock
CPU 2: lock 시도 → CPU 1의 unlock 대기 → lock 획득 후 조작
🔐 임계 구역 (Critical Section)
Race Condition을 이해하려면 임계 구역 개념이 필수다.
여러 프로세스가 공유하는 데이터를 접근 및 수정하는 코드 영역
임계 구역 문제를 해결하려면 아래 3가지 조건을 모두 만족해야 한다.
| 조건 | 설명 |
|---|---|
| 상호 배제 (Mutual Exclusion) | 한 프로세스가 임계 구역을 실행 중이면 다른 프로세스는 진입 불가 |
| 진행 (Progress) | 임계 구역에 프로세스가 없을 때 진입 요청한 프로세스는 반드시 진입 가능 |
| 한정 대기 (Bounded Waiting) | 임계 구역 진입 요청 후 무한정 기다리지 않도록 대기 횟수에 한계가 있어야 함 |
임계 구역 코드 구조
1
2
3
4
5
6
do {
[entry section] // 진입 허가 요청
critical section // 임계 구역 (공유 자원 접근)
[exit section] // 임계 구역 종료 알림
remainder section // 나머지 코드
} while (true);
🛠️ 해결 방법 — 세마포어 & 뮤텍스
세마포어 (Semaphore)
멀티프로그래밍 환경에서 공유 자원에 대한 접근을 제한하는 방법으로, 정수 카운터 S를 이용해 동시 접근 가능한 프로세스 수를 제어한다.
P, V 연산
| 연산 | 시점 | 동작 |
|---|---|---|
| P(S) | 임계 구역 진입 전 | S 값을 확인하고 1 감소. S = 0이면 대기 |
| V(S) | 임계 구역 나올 때 | S 값을 1 증가. 대기 프로세스 깨움 |
1
2
3
4
5
6
7
8
9
10
procedure P(S) -- 최초 S값은 1
while S = 0 do wait -- S가 0이면 1이 될 때까지 대기
S := S - 1 -- S를 0으로 만들어 다른 프로세스 진입 차단
end P
--- 임계 구역 ---
procedure V(S) -- 현재 S = 0
S := S + 1 -- S를 1로 복원하여 해제
end V
동작 예시 (프로세스 A, B, 초기 S = 1):
- A가
P(S)실행 → S = 0, 임계 구역 진입 - B가
P(S)실행 → S = 0이므로 대기 - A가 임계 구역 수행 후
V(S)실행 → S = 1 - B가 대기 해제되어 임계 구역 진입
P와 V 수행 중에는 인터럽트를 당하지 않아 상호 배제가 보장된다.
세마포어 종류
| 구분 | 설명 |
|---|---|
| 이진 세마포어 (Binary) | S = 0 또는 1만 가짐. 뮤텍스와 유사하게 동작 |
| 계수 세마포어 (Counting) | S ≥ 0의 정수. 여러 프로세스의 동시 접근 허용 |
뮤텍스 (Mutex)
Mutual Exclusion의 약자
임계 구역을 가진 스레드들의 실행 시간이 겹치지 않고 단독으로 실행되도록 보장하는 기술. lock / unlock 두 상태만 가지므로 이진 세마포어라고도 불린다.
- lock : 임계 구역 진입 권한 획득. 다른 스레드가 수행 중이면 종료될 때까지 대기
- unlock : 임계 구역 사용 완료 알림. 대기 중인 스레드가 진입할 수 있게 됨
1
2
3
mutex.lock()
// --- 임계 구역 ---
mutex.unlock()
뮤텍스 구현 알고리즘
1. 데커(Dekker) 알고리즘
flag와 turn 변수를 통해 임계 구역에 진입할 프로세스를 결정하는 방식. 두 변수를 모두 사용한다는 점에서 복잡하지만, 최초의 소프트웨어적 상호 배제 해결법이다.
flag[i]: 프로세스 i가 임계 구역 진입을 원하는지 나타냄turn: 누가 임계 구역에 들어갈 차례인지 나타냄
1
2
3
4
5
6
7
8
9
10
11
12
13
while(true) {
flag[i] = true; // 프로세스 i가 진입 시도
while(flag[j]) { // 프로세스 j가 임계 구역에 있는지 확인
if(turn == j) {
flag[i] = false; // 진입 취소
while(turn == j); // turn이 바뀔 때까지 대기
flag[i] = true; // 다시 진입 시도
}
}
// --- 임계 구역 ---
turn = j; // turn을 상대에게 양보
flag[i] = false; // 사용 완료
}
2. 피터슨(Peterson) 알고리즘
데커 알고리즘을 개선한 방식. 상대방에게 먼저 기회를 양보(turn = j) 함으로써 코드가 훨씬 단순해진다.
1
2
3
4
5
6
7
while(true) {
flag[i] = true; // 프로세스 i가 진입 시도
turn = j; // 상대에게 먼저 기회 양보
while(flag[j] && turn == j); // 상대가 진입 시도 중이면 대기
// --- 임계 구역 ---
flag[i] = false; // 사용 완료
}
데커 vs 피터슨 비교:
| 구분 | 데커 | 피터슨 |
|---|---|---|
| 양보 방식 | 상대 turn이 끝날 때까지 flag 내리고 대기 | turn을 상대에게 직접 넘김 |
| 코드 복잡도 | 복잡 | 단순 |
3. 제과점(Bakery) 알고리즘
데커/피터슨이 2개 프로세스만 처리 가능한 것과 달리, n개의 프로세스를 처리할 수 있는 알고리즘. 빵집에서 번호표를 뽑아 순서를 기다리는 것에서 유래했으며, 가장 작은 번호표를 가진 프로세스가 임계 구역에 진입한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
while(true) {
isReady[i] = true; // 번호표 받을 준비
number[i] = max(number[0..n-1]) + 1; // 현재 최대 번호 + 1 배정
isReady[i] = false; // 번호표 수령 완료
for(j = 0; j < n; j++) {
while(isReady[j]); // j가 번호표 받을 때까지 대기
while(number[j] && number[j] < number[i] && j < i);
// j의 번호가 존재하고, j 번호 < i 번호인 동안 대기
}
// --- 임계 구역 ---
number[i] = 0; // 임계 구역 사용 종료
}
세마포어 vs 뮤텍스 비교
| 구분 | 세마포어 | 뮤텍스 |
|---|---|---|
| 접근 허용 수 | 하나 이상 (Counting) 또는 하나 (Binary) | 오직 하나 |
| 소유권 | 소유 개념 없음 (다른 스레드가 signal 가능) | Lock을 획득한 스레드만 해제 가능 |
| 사용 범위 | 프로세스 간 동기화 가능 | 주로 스레드 간 동기화 |
| 종류 | 이진 / 계수 세마포어 | 단일 (이진 세마포어와 유사) |
| 교착 상태 | 잘못 사용 시 발생 가능 | 잘못 사용 시 발생 가능 |
모니터 (Monitor) — 보충
뮤텍스와 세마포어의 단점(직접 lock/unlock 관리 → 실수 가능)을 보완한 고수준 동기화 도구. 임계 구역과 동기화 로직을 내부에 캡슐화하여 프로그래머가 명시적으로 제어하지 않아도 된다.
Java의 synchronized 키워드가 대표적인 예시다.
1
2
3
synchronized void increment() {
count++; // JVM이 자동으로 상호 배제 보장
}
🔄 Race Condition vs DeadLock 비교
| 구분 | Race Condition | DeadLock |
|---|---|---|
| 원인 | 동시에 공유 자원 접근 | 서로 자원을 점유한 채 대기 |
| 결과 | 데이터 불일치, 예측 불가능한 동작 | 프로세스 무한 대기 (시스템 중단) |
| 핵심 문제 | 실행 순서 의존성 | 자원 순환 대기 |
| 해결 | 동기화 (뮤텍스, 세마포어 등) | 예방, 회피, 탐지, 회복 |
✅ 요약 정리
| 항목 | 내용 |
|---|---|
| Race Condition | 공유 자원에 여러 프로세스가 동시 접근 시 결과가 달라지는 현상 |
| 발생 상황 | 커널 인터럽트 / System Call 중 Context Switching / 멀티 프로세서 공유 메모리 접근 |
| 임계 구역 | 공유 데이터를 접근하는 코드 영역. 상호 배제 / 진행 / 한정 대기 조건 필요 |
| 세마포어 | P/V 연산으로 접근 수를 제어. 소유권 없음 |
| 뮤텍스 | lock/unlock으로 단독 실행 보장. lock 획득자만 해제 가능 |
| 뮤텍스 알고리즘 | 데커(복잡, 최초) → 피터슨(단순, 2개) → 제과점(n개 프로세스) |
💬 면접 예상 질문
- Race Condition(경쟁 상태)란 무엇이고, 언제 발생하나요?
- 임계 구역(Critical Section) 문제를 해결하기 위한 3가지 조건을 설명하세요.
- 세마포어의 P, V 연산이란 무엇인가요?
- 뮤텍스와 세마포어의 차이점은 무엇인가요?
- 데커, 피터슨, 제과점 알고리즘의 차이를 설명하세요.
- Race Condition과 DeadLock의 차이를 설명하세요.
참고: