세마포어는 동시성 문제를 해결하는데 유용하지만 프로그래머가 올바르게 사용하지 않으면 타이밍 문제
가 발생할 수 있습니다.
타이밍 문제란 타이밍에 따라 문제가 발생할 수도 발생하지 않을 수도 있는 것을 의미하는데 이러한 문제는 대처하기 까다롭고 디버깅에 어려움을 줍니다.
이러한 타이밍 문제를 해결하기 위해 모니터라는 기법이 개발되었습니다.
모니터의 뼈대가 되는 코드
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를 하나만 가진다.
자바의 모니터의 세 가지 동작
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 에는 동기화 기능이 탑재된 여러 클래스들이 있어서 참고
출처 | 쉬운코드 유튜브