생산자-소비자 문제는 운영체제 동기화의 가장 실용적인 예제입니다. Kafka, RabbitMQ 같은 메시지 큐의 기본 원리이기도 합니다.


문제 정의

생산자(Producer) 가 데이터를 만들어 공유 버퍼 에 넣고, 소비자(Consumer) 가 버퍼에서 데이터를 꺼내 처리합니다.

생산자-소비자 문제 구조

PLAINTEXT
Producer → [버퍼 (크기 N)] → Consumer

제약 조건:
1. 버퍼가 가득 차면 Producer는 대기
2. 버퍼가 비어있으면 Consumer는 대기
3. 동시에 버퍼에 접근하면 안 됨 (상호 배제)

세마포어를 이용한 해법

C
#define BUFFER_SIZE 10

int buffer[BUFFER_SIZE];
int in = 0, out = 0;

semaphore mutex = 1;           // 버퍼 접근 상호 배제
semaphore empty = BUFFER_SIZE; // 빈 슬롯 수
semaphore full = 0;            // 채워진 슬롯 수

// 생산자
void producer() {
    while (true) {
        int item = produce_item();  // 아이템 생산

        wait(empty);                // 빈 슬롯이 있을 때까지 대기
        wait(mutex);                // 버퍼 잠금

        buffer[in] = item;          // 버퍼에 넣기
        in = (in + 1) % BUFFER_SIZE;

        signal(mutex);              // 버퍼 잠금 해제
        signal(full);               // 채워진 슬롯 수 증가
    }
}

// 소비자
void consumer() {
    while (true) {
        wait(full);                 // 채워진 슬롯이 있을 때까지 대기
        wait(mutex);                // 버퍼 잠금

        int item = buffer[out];     // 버퍼에서 꺼내기
        out = (out + 1) % BUFFER_SIZE;

        signal(mutex);              // 버퍼 잠금 해제
        signal(empty);              // 빈 슬롯 수 증가

        consume_item(item);         // 아이템 소비
    }
}

세마포어의 역할

세마포어초기값역할
mutex1버퍼 동시 접근 방지 (이진 세마포어)
emptyN빈 슬롯 수 추적 — 0이면 Producer 대기
full0채워진 슬롯 수 추적 — 0이면 Consumer 대기

wait/signal 순서가 중요한 이유

C
// 잘못된 순서 — 데드락 발생!
void producer_wrong() {
    wait(mutex);    // 먼저 잠금
    wait(empty);    // 빈 슬롯 대기 → 버퍼가 꽉 차면 여기서 블록
    // Consumer가 mutex를 기다림 → 데드락!
}

반드시 세마포어(empty/full)를 먼저 wait하고, mutex를 나중에 wait 해야 합니다.


Java로 구현

BlockingQueue 사용 (가장 간단)

JAVA
BlockingQueue<Integer> buffer = new ArrayBlockingQueue<>(10);

// 생산자
new Thread(() -> {
    while (true) {
        int item = produceItem();
        buffer.put(item);  // 가득 차면 자동 대기
    }
}).start();

// 소비자
new Thread(() -> {
    while (true) {
        int item = buffer.take();  // 비어있으면 자동 대기
        consumeItem(item);
    }
}).start();

모니터(synchronized + wait/notify) 사용

JAVA
class BoundedBuffer<T> {
    private final Queue<T> queue = new LinkedList<>();
    private final int capacity;

    public BoundedBuffer(int capacity) {
        this.capacity = capacity;
    }

    public synchronized void put(T item) throws InterruptedException {
        while (queue.size() == capacity) {
            wait();  // 버퍼가 가득 차면 대기
        }
        queue.add(item);
        notifyAll();  // 소비자 깨움
    }

    public synchronized T take() throws InterruptedException {
        while (queue.isEmpty()) {
            wait();  // 버퍼가 비어있으면 대기
        }
        T item = queue.poll();
        notifyAll();  // 생산자 깨움
        return item;
    }
}

생산자-소비자 패턴

시스템생산자버퍼소비자
Kafka이벤트 발행 서비스토픽(파티션)이벤트 처리 서비스
스레드 풀작업 제출자작업 큐워커 스레드
로깅애플리케이션로그 버퍼로그 기록 스레드
파이프라인이전 단계파이프 버퍼다음 단계

핵심 포인트

  • 세마포어 3개의 역할: mutex(상호 배제), empty(빈 공간 추적), full(데이터 추적)
  • wait 순서 실수 → 데드락: mutex를 먼저 잡으면 안 됨
  • while vs if: wait()에서 깨어날 때 조건을 다시 확인해야 함 (Spurious Wakeup)
  • 활용: Java의 BlockingQueue가 이 문제의 완전한 추상화

정리

생산자-소비자 문제는 동기화의 기본이면서, 실제로 메시지 큐와 스레드 풀의 근간이 되는 패턴입니다. 세마포어 3개의 역할과 wait 순서를 정확히 이해하고 있으면 실제로 충분히 설명할 수 있습니다.

댓글 로딩 중...