멀티스레드 프로그래밍을 하다 보면 생산자-소비자 문제(Producer-Consumer Problem)를 자주 만나게 됩니다. 이 문제는 생산자가 데이터를 생성하고, 소비자가 그 데이터를 소비하는 과정에서 발생하는 동시성 문제입니다.

1. 생산자-소비자 문제란?

생산자(Producer)와 소비자(Consumer)가 한정된 버퍼(Buffer)를 통해 데이터를 주고받는 상황을 가정합니다.

  • 생산자(Producer): 데이터를 생성하여 버퍼에 저장하는 역할.
  • 소비자(Consumer): 버퍼에서 데이터를 가져와 사용하는 역할.
  • 버퍼(Buffer): 생산자가 생성한 데이터를 일시적으로 저장하는 공간. 크기가 제한됨.

문제 상황

  • 생산자가 너무 빠름 → 버퍼가 가득 차면 더 이상 데이터를 넣을 수 없음.
  • 소비자가 너무 빠름 → 버퍼가 비어있으면 소비할 데이터가 없음.

이를 해결하기 위해 생산자는 버퍼가 가득 찼을 때 대기, 소비자는 버퍼가 비었을 때 대기해야 합니다.

2. 한정된 버퍼 문제 (Bounded-Buffer Problem)

생산자-소비자 문제는 결국 버퍼의 크기가 제한되어 있기 때문에 발생합니다. 이를 해결하기 위해 스레드 동기화(Synchronization)가 필요합니다.

3. 해결 방법: wait()와 notify() 사용

자바에서는 wait()notify()를 사용하여 이 문제를 해결할 수 있습니다.

wait() 메서드

  • 현재 스레드가 가진 락(Lock)을 반납하고 대기.
  • synchronized 블록 안에서만 호출 가능.
  • 다른 스레드가 notify()를 호출하면 다시 실행됨.

notify() 메서드

  • wait() 상태에서 대기 중인 스레드 중 하나를 깨움.
  • synchronized 블록 안에서 호출해야 함.

notifyAll() 메서드

  • wait() 상태에서 대기 중인 모든 스레드를 깨움.

4. 구현 코드 (Java)

아래는 wait()notify()를 사용하여 생산자가 버퍼가 가득 찼을 때 대기하고, 소비자가 버퍼가 비었을 때 대기하는 코드입니다.

package thread.bounded;

import java.util.ArrayDeque;
import java.util.Queue;

import static util.MyLogger.log;

public class BoundedQueueV3 implements BoundedQueue {

    private final Queue<String> queue = new ArrayDeque<>();
    private final int max;

    public BoundedQueueV3(int max) {
        this.max = max;
    }

    @Override
    public synchronized void put(String data) {
        while (queue.size() == max) {
            log("[put] 큐가 가득 참. 생산자 대기");
            try {
                wait(); // 큐가 가득 찼으므로 대기
                log("[put] 생산자 깨어남");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        queue.offer(data);
        log("[put] 생산자 데이터 저장, notify() 호출");
        notify(); // 대기 중인 소비자 깨움
    }

    @Override
    public synchronized String take() {
        while (queue.isEmpty()) {
            log("[take] 큐에 데이터가 없음, 소비자 대기");
            try {
                wait(); // 큐가 비었으므로 대기
                log("[take] 소비자 깨어남");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        String data = queue.poll();
        log("[take] 소비자 데이터 획득, notify() 호출");
        notify(); // 대기 중인 생산자 깨움
        return data;
    }

    @Override
    public String toString() {
        return queue.toString();
    }
}

5. 코드 설명

1) 생산자 (put 메서드)

  • synchronized를 사용하여 동기화.
  • 큐가 가득 차면 wait()을 호출하여 락을 반납하고 대기.
  • 빈 공간이 생기면 notify()를 호출하여 대기 중인 소비자를 깨움.

2) 소비자 (take 메서드)

  • synchronized를 사용하여 동기화.
  • 큐가 비어있으면 wait()을 호출하여 락을 반납하고 대기.
  • 데이터가 들어오면 notify()를 호출하여 대기 중인 생산자를 깨움.

6. 실행 흐름

  1. 생산자 스레드가 데이터를 넣으려 하지만 버퍼가 가득 차면 대기 (wait()).
  2. 소비자가 데이터를 가져가면 생산자에게 공간이 생겼다고 알림 (notify()).
  3. 소비자 스레드는 데이터를 가져가려 하지만 버퍼가 비어 있으면 대기 (wait()).
  4. 생산자가 새로운 데이터를 넣으면 소비자에게 데이터를 사용할 수 있다고 알림 (notify()).

7. 결론

  • wait()notify()를 사용하면 생산자-소비자 문제를 효과적으로 해결할 수 있다.
  • 스레드가 필요할 때만 대기하고, 필요할 때만 실행되므로 성능이 향상된다.
  • synchronized 블록 내에서 wait()notify()를 사용해야 한다.

이렇게 하면 생산자가 데이터를 버리거나, 소비자가 빈 데이터를 가져가는 문제를 해결할 수 있습니다! 🚀

profile
배움을 추구하는 개발자

0개의 댓글