빵집 진열대를 떠올리면 된다. 제빵사(생산자)는 빵을 구워 진열대(버퍼)에 놓고, 손님(소비자)은 하나씩 가져간다. 진열대가 꽉 차면 제빵사는 기다리고, 비어 있으면 손님이 기다린다. 두 사람이 동시에 같은 칸을 만지면 안 된다. 세마포어 셋으로 깔끔하게 풀리지만, semWait 두 개의 순서를 바꾸기만 해도 둘 다 영원히 멈춘다.
예시로 보기
버퍼 칸이 4개인 진열대에서 생산자와 소비자가 일한다. 필요한 것은 세 가지다.
- 한 번에 한 사람만 진열대를 만진다(상호 배제).
- 가득 차면 생산자가 기다린다.
- 비어 있으면 소비자가 기다린다.
이 셋을 각각 세마포어 하나로 맡긴다.
| 세마포어 | 처음 값 | 뜻 |
|---|---|---|
s |
1 | 진열대를 만지는 권한 (상호 배제) |
n |
0 | 빵이 놓인 칸 수 |
e |
4 (버퍼 크기) | 빈 칸 수 |
정확히 말하면
버퍼가 무한할 때
배열 b가 끝없이 길다고 하자. 생산자는 b[in]에 넣고 in을 늘리고, 소비자는 in > out일 때만 b[out]을 꺼낸다.
/* 생산자 */ /* 소비자 */
while (true) { while (true) {
/* 물건 v 생산 */; while (in <= out) /* 기다림 */;
b[in] = v; w = b[out];
in++; out++;
} /* 물건 w 소비 */;
}
세마포어로 바꾸면 n(놓인 물건 수)이 in - out 역할을 한다.
semaphore s = 1, n = 0;
void producer() { void consumer() {
while (true) { while (true) {
produce(); semWait(n);
semWait(s); semWait(s);
append(); take();
semSignal(s); semSignal(s);
semSignal(n); consume();
} }
} }
이진 세마포어와 정수 변수로 만들면 그럴듯하지만 틀린 해답이 나오기 쉽다. 소비자가 버퍼가 비었는지 확인하는 시점과 그 값을 쓰는 시점 사이에 생산자가 끼어들면, 소비자가 빈 버퍼에서 꺼내게 된다. 확인한 값을 임계 구역 안에서 지역 변수에 저장해 두는 것으로 고친다.
버퍼가 유한할 때
버퍼 칸을 \(n\)개로 두고 원형으로 쓴다. 포인터를 \(n\)으로 나눈 나머지로 돌린다. 가득 찬 조건은 (in + 1) % n == out, 빈 조건은 in == out이다. 세마포어 해답에는 빈 칸 수 e를 더한다.
칸 번호 0 1 2 3 4
+-----+-----+-----+-----+-----+
b | | 빵 | 빵 | | |
+-----+-----+-----+-----+-----+
^ ^
out in
out: 소비자가 다음에 꺼낼 칸 / in: 생산자가 다음에 넣을 칸
4번 칸 다음은 다시 0번 칸이다: in = (in + 1) % 5
칸이 5개이고 빵 두 개가 1·2번 칸에 있는 순간이다. 생산자는 in을, 소비자는 out을 오른쪽으로 밀고, 둘 다 끝에 닿으면 0번 칸으로 돌아온다1.
const int sizeofbuffer = /* 버퍼 크기 */;
semaphore s = 1, n = 0, e = sizeofbuffer;
void producer() { void consumer() {
while (true) { while (true) {
produce(); semWait(n);
semWait(e); semWait(s);
semWait(s); take();
append(); semSignal(s);
semSignal(s); semSignal(e);
semSignal(n); consume();
} }
} }
순서가 중요한 곳, 상관없는 곳
| 바꾼 것 | 결과 |
|---|---|
생산자의 semSignal(s)와 semSignal(n) |
상관없다. 둘 다 막히지 않는 연산이다 |
소비자의 semWait(n)과 semWait(s) |
교착상태가 생긴다. 버퍼가 비었을 때 소비자가 s를 쥔 채 n을 기다리면, 생산자는 s를 얻지 못해 물건을 넣을 수 없다 |
생산자의 semWait(e)와 semWait(s) |
같은 이유로 교착상태. 버퍼가 가득 찼을 때 생산자가 s를 쥔 채 e를 기다리면 소비자가 꺼낼 수 없다 |
활용
- 운영체제 안의 키보드 입력 버퍼, 프린터 스풀러, 네트워크 패킷 큐가 생산자-소비자 구조다.
- 파이썬
queue.Queue(maxsize=n), 자바BlockingQueue는 이 해답을 미리 만들어 둔 것이다. 가득 차면put이, 비면get이 기다린다. - 유닉스 파이프(
ls | grep)도 앞 프로그램이 생산자, 뒤 프로그램이 소비자인 유한 버퍼다.
연결
확인 문제
C1 유한 버퍼 해답의 세마포어 세 개와 각각의 처음 값, 뜻을 쓰라.
답: s = 1(버퍼 접근 상호 배제), n = 0(찬 칸 수), e = 버퍼 크기(빈 칸 수).
C2 소비자가 semWait(s)를 먼저 하고 semWait(n)을 나중에 하면 버퍼가 비었을 때 무슨 일이 생기는가?
답: 소비자가 s를 얻고(0), n에서 막힌다. 생산자는 물건을 넣으려고 semWait(s)를 하지만 s가 0이라 막힌다. 생산자는 소비자가 s를 풀기를, 소비자는 생산자가 n을 올리기를 기다린다. 교착상태다.
C3 칸이 5개인 원형 버퍼에서 in = 3, out = 4다. 버퍼는 가득 찼는가, 비었는가, 둘 다 아닌가? 이 방식으로 실제로 쓸 수 있는 칸은 몇 개인가?
답: \((3 + 1) \bmod 5 = 4 =\) out이므로 가득 찼다. 이 방식은 in == out을 “빔”으로 쓰므로 가득 찬 상태와 구별하려고 한 칸을 비워 둔다. 실제로는 4칸만 쓴다.
-
칸 수 5와 in·out 값은 보기를 위해 고른 값이다. ↩