💋 인트로
지난 포스팅에 이어, 스레드 동기화 메커니즘 중 하나인 모니터에 대해 설명하겠습니다.
💋 모니터(Monitor)
✔️ 개념
baeldung의 모니터에 대한 글을 보면, 모니터를 아래와 같이 정의합니다.
모니터는 mutual exclusion과 cooperation을 통해 스레드 간의 안전한 공유자원 접근을 보장하는데 사용됩니다.
- mutual exclusion ⇒ 동시에 하나의 스레드만 진입해서 실행 가능
- cooperation ⇒ 조건에 따라 스레드가 대기 상태로 전환 가능
스레드가 어떤 공유자원에 접근하는 것을 말 그대로 모니터할 수 있기 때문에 모니터라고 부른다고 합니다.
✔️ 동작 원리
모니터는 뮤텍스
와 condition variable
로 구성되어 있으며, waiting queue
와 entry queue
를 통해 스레드들을 관리합니다.
condition variable과 관련된 주요 동작은 아래와 같습니다.
wait
: 스레드가 자기 자신을 condition variable의 waiting queue에 넣고 대기 상태로 전환합니다.signal
: waiting queue에서 대기중인 스레드 중 하나를 깨웁니다.broadcast
: waiting queue에서 대기중인 스레드 전부를 깨웁니다.
코드로 이해해 봅시다!
acquire(mutexLock); // 뮤텍스 락 취득 -> 락 취득 못하면 entry queue에서 대기
while (!p) { // 조건 확인 -> 조건이 충족되지 않으면 waiting queue에서 대기
wait(mutexLock, conditionVariable);
}
// ...
signal(conditionVariable2); // 앞의 conditionVariable과 다를 수도 있음.
// broadcast(conditionVariable2);
release(mutexLock); // 뮤텍스 락 반환
위 코드는 대충의 뼈대이고, 각각을 실행하는 과정을 풀어서 설명하면 아래와 같습니다.
acquire(mutexLock);
- 뮤텍스 락을 취득하지 못한 스레드는 entry queue에서 대기하게 됩니다.
- 조건 확인
mutexLock
이 파라미터인 이유 ⇒ 대기 상태로 전환된 스레드가 뮤텍스 락을 가지고 있으면 다른 스레드들이 뮤텍스 락을 취득할 수 없기 때문에,wait
를 호출과 함께 락을 반환하기 위해서입니다.conditionVariable
이 파라미터인 이유 ⇒conditionVariable
별로 waiting queue를 가지고 있기 때문입니다.
- 조건이 충족되지 않은 스레드는
wait(mutexLock, conditionVariable);
호출해 waiting queue에서 대기하게 됩니다. - 조건까지 확인된 스레드는 자신의 할 일을 수행하는데, 이 코드는 위에서 …입니다.
signal(conditionVariable2);
- 어떤 waiting queue의 스레드를 깨울지 그 condition variable을 통해서 관리할 수 있습니다.
conditionVariable2
은 앞서 등장한conditionVariable
와 반드시 같을 필요는 없습니다.broadcast
를 사용하는 경우에는conditionVariable2
를 통해 관리하는 waiting queue에 있는 스레드 모두를 깨울 수 있습니다.
conditionVariable2
를 통해 관리하는 waiting queue에 있는 스레드 중 하나를 깨웁니다.
이때 등장하는 두 가지 Queue를 정리해보면 다음과 같습니다.
✔️ Queue
- entry queue: 뮤텍스 락을 취득하기 위해 기다리는 큐
- waiting queue: 조건이 충족되길 기다리는 큐
💋 Bounded buffer problem
(Producer-Consumer Problem이라고도 합니다.)
하나의 문제 예시를 설명하고, 앞서서 열심히 공부한 모니터를 사용해서 이 문제를 어떻게 해결할 수 있는지 보여드리겠습니다.
✔️ 구성
- Producer: 데이터를 생성하는 역할
- Consumer: 데이터를 소비하는 역할
- Buffer: 데이터가 저장되고, 공유되는 공간인데, limited capacity
✔️ 목표
- Producer는 버퍼가 꽉 찼을 때 더이상 데이터를 생산하지 않습니다.
- Consumer는 이미 소비한 데이터를 또 소비하지 않습니다.
- Buffer는 제한된 capacity를 효율적으로 사용합니다.
모니터를 사용해서 이 목표를 이룰 수 있습니다.
Producer, Consumer는 Buffer를 공유
해서 사용하기 때문에, Buffer에 접근할 때는 critical section 안에서, mutual exclusion이 보장된 상태여야만 합니다.
Producer, Consumer 코드에서 공통적으로 lock.acquire()
, lock.release()
사이의 부분은 critical section이 됩니다.
Producer의 스레드는 Buffer가 다 찼다면(q.isFull()
), wait
를 하게 됩니다. Consumer 스레드가 데이터를 소비한 후에 signal()
을 호출해 Producer의 스레드가 깨어날 수 있게 됩니다. Producer 스레드는 데이터를 생산한 후에 signal()을 호출해서 Consumer의 스레드를 깨울 수 있습니다.
Producer 스레드는 자신의 작업을 완료한 후 Consumer 스레드를 깨우고,
Consumer 스레드는 자신의 작업을 완료한 후 Producer 스레드를 깨웁니다. (크로스!!!)
💋 Monitor in Java
자바의 모든 객체는 내부적으로 모니터를 가집니다.
자바의 모니터의 mutual exclusion 기능은 synchronized
키워드로 사용할 수 있으며, condition variable은 딱 하나만 가집니다.
✔️ 모니터 동작
자바 모니터는 세 가지 동작이 가능합니다.
- wait
- notify →
signal()
과 동일 - notifyAll →
broadcast()
과 동일
✔️ 자바로 구현한 Bounded buffer problem
public class BoundedBuffer {
private final int[] buffer = new int[5];
private int count = 0;
public synchronized void produce(int item) {
while (count == 5) {
try {
wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
buffer[count++] = item;
notifyAll();
}
public void consume() {
int item = 0;
synchronized (this) {
while (count == 0) {
try {
wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
item = buffer[--count];
notifyAll();
}
}
}
produce()
, consume()
의 두 메서드가 동시에 실행되지 않도록 하기 위해 synchroized
키워드를 통해 mutual exclusion을 보장합니다.
synchroized
키워드는 메서드에 붙일 수도 있고, 메서드 내부의 block에서만 사용할 수도 있습니다.
synchroized
을 block으로 사용하는 경우에 파라미터를 전달해 주어야 하는데, 이 파라미터를 통해 뮤텍스 락을 전달해주어야 합니다.
this
는 객체 자신을 가리는데, 이걸 통해 뮤텍스 락을 전달할 수 있을까요?
자바의 모든 객체는 모니터를 가지고 있고 그 모니터에는 뮤텍스 락이 있기 때문에 this
를 전달하는 것은 뮤텍스 락을 전달한다는 의미와 동일합니다.
wait()
, notifyAll()
은 자신의 객체가 속한 모니터의 condition variable에 대한 동작입니다. wait()
과 notifyAll()
은 synchronized
메서드 또는 블록 내에서 호출되어야 합니다.
위의 예제에서는 여러 개의 condition variable이 사용되었기 때문에, 각 조건에 따라 다른 condition variable을 파라미터로 함께 사용했는데, 자바에서는 딱 1개의 condition variable만을 가지기 때문에 별도로 condition variable을 파라미터로 보내지는 않습니다.
public class Main {
public static void main(String[] args) throws InterruptedException {
final BoundedBuffer boundedBuffer = new BoundedBuffer();
final Thread consumer = new Thread(() -> boundedBuffer.consume());
final Thread producer = new Thread(() -> boundedBuffer.produce(100));
consumer.start();
producer.start();
consumer.join();
producer.join();
System.out.println("Bounded Buffer Test Done.");
}
}
여러 번 시도하더라도, 항상 Producer가 데이터를 생산한 후에야 Consumer가 데이터를 소비하기 때문에, 어쨌든 Consumer가 100이라는 데이터를 소비한 후에 종료되는 것을 확인할 수 있습니다.
이외에도 java.util.concurrent
에는 동기화 기능이 탑재된 여러 클래스가 있으니, 안심하고 사용할 수 있습니다.
💋 아웃트로
오늘 포스팅을 작성하면서 나름대로의 언어로 정리한 후에 GPT 선생님의 검토를 받았는데, 극찬을 받았어요ㅋㅋ
💋 참고자료
- https://www.baeldung.com/cs/monitor
- https://www.baeldung.com/cs/bounded-buffer-problem
- https://binaryterms.com/producer-consumer-problem.html
- https://stackoverflow.com/questions/3362303/whats-a-monitor-in-java
- https://www.youtube.com/watch?v=Dms1oBmRAlo&list=PLcXyemr8ZeoQOtSUjwaer0VMJSMfa-9G-&index=7
도움이 되었다면, 공감/댓글을 달아주면 깃짱에게 큰 힘이 됩니다!🌟
비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!
'Computer Science > Operating System' 카테고리의 다른 글
[OS] OS, Java에서 프로세스의 상태 (State of Process in OS and Java) (0) | 2023.11.23 |
---|---|
[OS] 데드락(Deadlock)은 언제 발생하고, OS, Java에서 어떻게 해결할까? (1) | 2023.11.22 |
[OS] 동기화 메커니즘(Synchronization Mechanisms)(1): 스핀락(Spinlock), 뮤텍스(Mutex), 세마포어(Semaphore) (0) | 2023.11.20 |
[OS] 동기화(synchronization)의 필요성: 경쟁 조건(race condition), 임계 영역(critical section) (0) | 2023.11.12 |
[OS] CPU Bound VS IO Bound: 스레드는 몇 개가 좋을까? (0) | 2023.11.10 |