2025-2 운영체제론 강의안 Synchronization II : Semaphore and Condition Variable 일부 정리한 내용입니다 ʕ/·ᴥ·ʔ/
1. 문제 상황 개요
Producer-Consumer 문제는 생산자 스레드와 소비자 스레드가 하나의 공유 버퍼를 사용하는 상황을 말한다.
뮤텍스(lock)는 공유 자원에 대한 상호 배제는 보장하지만, 버퍼가 비어 있거나 꽉 찬 상황에서의 "기다림"이나 "순서 제어"는 제공하지 못한다.
이를 해결하기 위해 Block()/Unblock()으로 기다림을 구현하려 시도한 버전이 바로 6페이지의 코드이다.
2. 사용된 코드 구조
# insert (생산자):
if (count == N)
Block();
buf[++rear % N] = item;
Acquire(&lock);
count++;
Release(&lock);
if (count == 1)
Unblock(Consumer);
# remove (소비자):
int item;
if (!count)
Block();
item = buf[++front % N];
Acquire(&lock);
count--;
Release(&lock);
if (count == N-1)
Unblock(Producer);
return item;
3. 문제점 1: 임계 구역 밖에서 Block()을 호출함
Block()은 스레드를 "잠재우는" 기능이며, 기다림을 구현하는 핵심 동작이다. 그러나 이 코드에서는 Block()이 lock을 잡지 않은 상태에서 호출된다.
if (count == N)
Block();
count 검사와 Block() 호출이 임계 구역 밖에서 일어나기 때문에 여러 스레드가 동시에 count 값을 검사할 수 있다.
예시:
- 버퍼가 가득 찬 상태(count = N)
- 생산자 A가 count == N 확인 → Block()
- 생산자 B가 거의 동시에 count == N 확인 → Block()
- 두 스레드 모두 Block()으로 잠든다
- 이후 소비자가 count--을 해도 깨울 타이밍이 맞지 않아 둘 다 깨어나지 않는 문제가 발생한다.
즉, Block()을 lock 없이 호출한 순간 race condition이 다시 발생한다. Block() 여부를 결정하는 조건 검사(count == N 또는 count == 0)가 원자적이지 않기 때문이다.
4. 문제점 2: Block()과 count 조작이 atomic(원자적)이지 않음
Block()은 스레드를 잠재우는 동작이고, count++ 또는 count--는 버퍼 상태를 변경하는 동작이다. 이 두 동작은 반드시 "하나의 묶음"으로 atomic하게 실행되어야 한다.
그러나 코드에서는 Block()과 count 변경 사이에 임계 구역(lock)이 존재하며 시간이 띄어져 있다. 이로 인해 다음 문제가 발생한다.
예시:
- count == N 상태에서 생산자 A가 Block()
- 같은 순간 생산자 B도 count == N을 보고 Block()
- 두 생산자 모두 Block() 되어 잠든다
- 소비자가 remove()를 실행해 count-- 했지만 Unblock(Producer) 조건이 맞지 않아 아무도 깨우지 못함
- 결과적으로 생산자 두 스레드는 영원히 Block 상태가 되어 시스템이 정지한다.
즉, Block() 직전의 조건 검사와 스레드 상태 변화가 atomic하지 않으므로 순서가 틀리면 깨워야 할 스레드를 깨우지 못하고, 결국 양쪽 모두 Block 상태가 된다.
5. 구조적 문제 요약
(1) 임계 구역 밖에서 조건을 검사함
count의 검사와 Block 결정이 lock 없이 동시에 여러 스레드에서 실행될 수 있어 race condition 발생.
(2) Block()과 count 변경이 원자적이지 않음
스레드를 잠재우는 시점과 버퍼 상태 업데이트가 atomically 묶여 있지 않아 timing mismatch 발생.
(3) Block/Unblock은 올바른 시점 보장이 없음
count가 특정 값일 때만 Unblock이 실행되므로 타이밍이 조금만 틀려도 깨울 스레드가 사라지거나, 반대로 Block 중인 스레드가 깨어나지 않음.
(4) 최종적으로 생산자와 소비자 모두 Block되어 영원히 깨어나지 않는 상태가 발생
이는 데드락과 유사한 상황이며 "진행 불가능 상태"가 된다.
6. 버퍼 구조 (예시)
# 버퍼 크기 N=8, count=4인 경우:
[x][x][x][x][ ][ ][ ][ ]
front = 3
rear = 3
count = 4
버퍼가 꽉 찼는지, 비었는지 여부가 insert/remove의 Block 여부를 결정하는 핵심 요소이며, 이 조건 검사가 atomic하지 않으면 안정적인 동작이 불가능하다.
7. 결론
Block()을 단순한 "wait" 기능처럼 사용하면 다음 문제가 필연적으로 발생한다.
- count 검사와 Block의 실행이 atomic하지 않음
- lock 없이 Block()을 호출하여 race condition을 다시 유발
- 올바른 시점에 Unblock되지 않아 스레드가 영원히 대기
- 생산자와 소비자가 모두 잠들어 시스템이 멈춤
따라서 OS는 Block()/Unblock() 만으로 기다림을 구현하지 않으며, 반드시 atomic한 동작(P/V 연산)과 조건대기(wait/signal)를 제공하는 세마포어 또는 조건변수가 필요하다.
'concept' 카테고리의 다른 글
| Google Dev Challenge: C 언어 타입 변환의 함정 (1) | 2025.11.25 |
|---|---|
| [운영체제론] Constant Time Coalescing (Case 1-4) 정리 (0) | 2025.11.08 |
| tmux (수정 예정) (0) | 2025.11.03 |
| [운영체제론] 가상 메모리 (0) | 2025.10.31 |
| [운영체제론] Dynamic Memory Allocation (0) | 2025.10.29 |