프로세스 동기화 - 모니터(Monitor)

강민승·2023년 4월 13일
0

세마포어는 동시성 문제를 해결하는데 유용하지만 프로그래머가 올바르게 사용하지 않으면 타이밍 문제가 발생할 수 있습니다.

타이밍 문제란 타이밍에 따라 문제가 발생할 수도 발생하지 않을 수도 있는 것을 의미하는데 이러한 문제는 대처하기 까다롭고 디버깅에 어려움을 줍니다.

이러한 타이밍 문제를 해결하기 위해 모니터라는 기법이 개발되었습니다.

mutex

  • critical section 에서 mutual exclusion을 보장하는 장치이며, critical section에 진입하기 위해서 mutex lock을 취득해야함.
  • mutex lock을 취득하지 못한 스레드는 큐에 들어간 후 대기(waiting) 상태로 전환한다.
  • mutex lock 을 쥔 스레드가 lock을 반환하면 락을 기다리며 큐에 대기 상태로 있던 스레드 중 하나가 실행.

condition variable

  1. waiting queue를 가짐. 조건이 충족되길 기다리는 스레드들이 대기 상태로 머무는 곳
    1. 주요동작
    • wait
      • → thread가 자기 자신을 condition variable의 waiting queue에 넣고 대기 상태로 전환.
    • signal
      • → waiting queue에서 대기중인 스레드 중 하나를 깨움.
    • broadcast
      • → waiting queue에서 대기중인 스레드 전부를 깨움

모니터의 뼈대가 되는 코드

entry queue

m : mutex lock , cv = condition variable

컨슈머 프로듀서 문제

컨슈머와 프로슈서 사이에 Buffer라는 것을 두고 공유해서 사용하기 때문에 Buffer을 사용할 때는 critical section 에서 mutual exclusion이 보장될 수 있도록 사용해야한다.

보장이 안되면 여러 스레드나 프로세스가 Buffer에서 작업을 하려고 하다가 race condition이 발생할 수도 있다.

코드에서의 락은 mutex lock을 의미

signal & continue, signal & wait 으로 두가지 방식이 있따.

우선 p1과 c1 이 있다고 가정을 하고 c1 이 먼저 lock을 쥐었다고 가정하면, p1 은 lock을 못쥐기 때문에 c1이 관리하는 entry queue에 들어가게 된다. 또한 c1은 자신의 일을 하다가 buffer가 비었기 때문에 wait 상태로 들어가게 되고 매개변수인 lock, emptyCV 에서 lock 을 풀어주고, emptyCV가 관리하는 wait queue에 자신이 들어가게 되고 p1이 실행되게 된다.


 사진에 있는 설명은 wait 하고 있고 조건이 맞을 때, 서로 signal, broadcast를 사용해 producer은 wait 한 consumer을 깨우 consumer은 producer을 깨우는 작업을 하는 것이다.

만약 entry queue에 c1, p2, c2 가 있는데 이것은 어떻게 구현하느냐에 따라 달라질 수 있다. c1에 우선순위를 줘서 구현할 수도 있고, 아니면 경쟁을 시켜서 실행시킬 수 도 있다.

wait 함수는 무조건 while 문 안에서 실행이 되어야 한다.

→ 깨어나서 lock을 취득한 뒤에도 내가 기다렸던 조건이 충족이 되었는지 확인을 해줘야 한다. :star

자바에서 모든 객체는 내부적으로 모니터를 가진다.

모니터의 mutual exclusion 기능은 synchronized 키워드로 사용한다.

자바의 모니터는 condition variable를 하나만 가진다.

자바의 모니터의 세 가지 동작

  • wait = wait
  • notify = signal
  • notifyAll = broadcast

Bounded Producer and Consumer 문제는, Producer가 제한된 크기의 Buffer에 데이터를 쓰고, Consumer가 Buffer에서 데이터를 읽어가는 상황에서 발생하는 문제. 이 문제는 Producer와 Consumer가 서로 다른 속도로 데이터를 생산하고 소비하는 상황에서, Buffer가 가득 차거나 비어있을 때 발생한다.

모니터 동기화를 많이 사용하는 Java에서 이 문제를 해결하기 위해서는, Producer와 Consumer가 공유하는 Buffer를 생성하고, 이를 동기화하여 Producer가 Buffer에 데이터를 쓸 때는 Buffer가 가득 차 있지 않은지, Consumer가 Buffer에서 데이터를 읽어갈 때는 Buffer가 비어있지 않은지를 확인해야 합니다. 이를 위해 Java에서는 wait(), notify() 및 synchronized 키워드를 이용하여 Thread 간의 동기화를 구현가능.

아래는 코드



import java.util.LinkedList;

// BoundedBuffer 클래스 정의
public class BoundedBuffer {
    // 정수형 LinkedList 버퍼 생성
    private LinkedList<Integer> buffer = new LinkedList<Integer>();
    // 버퍼 크기 정의
    private int bufferSize = 5;

    // put 메소드 (생산자 메소드)
    public synchronized void put(int data) throws InterruptedException {
        // 버퍼가 가득 차면 대기
        while (buffer.size() == bufferSize) {
            wait();
        }
        // 버퍼에 데이터 추가
        buffer.add(data);
        // 대기중인 모든 쓰레드 깨우기
        notifyAll();
    }

    // get 메소드 (소비자 메소드)
    public synchronized int get() throws InterruptedException {
        // 버퍼가 비어있으면 대기
        while (buffer.isEmpty()) {
            wait();
        }
        // 버퍼에서 데이터 제거 후 반환
        int data = buffer.remove();
        // 대기중인 모든 쓰레드 깨우기
        notifyAll();
        return data;
    }
}

// Producer 클래스 정의 (생산자 쓰레드)
public class Producer extends Thread {
    private BoundedBuffer buffer;

    public Producer(BoundedBuffer buffer) {
        this.buffer = buffer;
    }

    // run 메소드 정의 (쓰레드 실행 메소드)
    public void run() {
        try {
            for (int i = 0; i < 10; i++) {
                // 버퍼에 데이터 추가
                buffer.put(i);
                // 데이터 추가 메시지 출력
                System.out.println("Produced: " + i);
                // 1초 대기
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

// Consumer 클래스 정의 (소비자 쓰레드)
public class Consumer extends Thread {
    private BoundedBuffer buffer;

    public Consumer(BoundedBuffer buffer) {
        this.buffer = buffer;
    }

    // run 메소드 정의 (쓰레드 실행 메소드)
    public void run() {
        try {
            for (int i = 0; i < 10; i++) {
                // 버퍼에서 데이터 제거 후 반환
                int data = buffer.get();
                // 데이터 제거 메시지 출력
                System.out.println("Consumed: " + data);
                // 1초 대기
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

// Main 클래스 정의
public class Main {
    public static void main(String[] args) {
        // BoundedBuffer 객체 생성
        BoundedBuffer buffer = new BoundedBuffer();
        // Producer 객체 생성
        Producer producer = new Producer(buffer);
        // Consumer 객체 생성
        Consumer consumer = new Consumer(buffer);
        // Producer 쓰레드 시작
        producer.start();
        // Consumer 쓰레드 시작
        consumer.start();
    }
}

자바에서의 프로듀서, 컨슈머 프로블럼

만약 자바 모니터를 사용할 때 두 가지 이상의 condition variable 이 필요하다면 따로 구현이 필요함..

java.util.concurrent 에는 동기화 기능이 탑재된 여러 클래스들이 있어서 참고

출처 | 쉬운코드 유튜브

profile
Step by Step goes a long way. 꾸준하게 성장하는 개발자 강민승입니다.

0개의 댓글