모니터
모니터(monitor)
- mutual exclusion을 보장
- 조건에 따라 스레드가 대기(wating) 상태로 전환 가능
모니터는 1) 한번에 하나의 스레드만 실행돼야 할 때, 그리고 2) 여러 스레드와 협업이 필요할 때 사용된다.
구성 요소로는 mutex
, condition variable(s)
가 있다.
1. mutex
아래 링크를 참고하자.
2. condition variable
waiting queue
를 가지며, 조건이 충족되길 기다리는 스레드들이 대기 상태로 머무는 곳이다.
condition variable의 주요 동작(operation)
은 다음과 같다.
wait
설정한 조건에 thread가 충족되지 못했을 경우 자기 자신을 condition variable의 waiting queue에 넣고 대기 상태로 전환한다.
signal
waiting queue에서 대기 중인 스레드 중 하나를 깨운다.
broadcast
waiting queue에서 대기 중인 스레드 전부를 깨운다.
모니터의 뼈대
모니터(monitor)는 mutex와 condition variable을 가지고 있다고 했다.
mutex
여기서 mutex는 acquire(m);
과 release(m);
는 critical section에 들어가기 위한 lock을 관리하는 부분이고, 그 외 나머지는 critical section으로 볼 수 있다.
그래서 이 만약 한 스레드가 들어와 critical section에서 작업을 하고 있던 중 다른 스레드가 접근을 시도한다면, mutex가 관리하는 queue에 들어가 대기하게 되고, 미리 들어와있던 스레드가 작업이 끝나면 queue에 대기하던 스레드를 깨우게 된다.
이 때 mutex가 관리하는 queue는 critical section에 진입하는데 관련이 있다는 뜻으로, entry queue
라고 불린다.
다음은 monitor의 condition variable에 관련된 부분이다.
condition variable
mutex가 관리하는 acquire(m) 과 release(m)을 제외하고 살펴본다면, 반복문과 그 안에 있는 wait, 그리고 signal과 broadcast가 눈에 뛸 것 이다.
이 부분들을 차례차례 살펴보자.
락을 쥐고 있는 스레드가 만약 while() 문으로 지정해놓은 조건에 맞지 않는다면, wait를 호출하게 된다.
wait 호출할 때 보내는 파라미터는 (m, cv)가 있는데 여기서 m은 mutex, cv는 condition variable을 뜻한다.
wait를 호출한 스레드는 조건에 맞지 않으므로 cv가 관리하는 waiting queue
에 들어가게 되고, 자신을 대기 상태로 전환시키게 된다.
그 다음, 대기 상태가 된 스레드가 lock을 쥐고 있으면 다른 스레드가 들어오지 못하기 때문에 이를 해결하기 위해 mutex를 파라미터로 받아 lock을 반환하게 한다. 또 만약 대기 상태가 된 스레드가 어떠한 조건에 의해 다시 깨어나게 된다면, lock을 다시 취득해야 된다.
그래서 wait에 파라미터로 보낸 mutex는 이러한 2가지, lock을 반환
하거나 lock을 다시 취득
하는 용도로 쓰인다.
만약 조건에 충족된 스레드가 들어와서 wait를 호출하지 않았다면, critical section에 있는 어떠한 작업을 하게 된다.
이러한 작업으로 인해 어떤 조건이 충족되어 다른 스레드들을 깨워줘야 할 수도 있다.
그럴 때 사용하는 것이 하나의 스레드만 깨울 것이면 : signal
, 모든 스레드를 깨울 것이면 : broadcast
이다. 그리고 파라미터로 보내는 cv는 기존의 wait가 가지고 있던 cv일 수도 있고, 혹은 새로운 cv2일 수도 있다. 이건 상황에 맞춰서 하자.
다른 예를 통해 더 자세히 알아보자.
bounded producer/consumer problem 상황의 monitor
bounded producer/consumer problem 상황을 해결하는 monitor를 사용할 수 있다.
bounded producer/consumer problem란, 고정된 buffer가 있다고 가정하자.
그때 producer는 item을 계속 생성해서 buffer에 넣고, consumer는 그 item을 꺼내 필요한 처리를 한다.
그런데 만약 producer가 item을 계속 buffer에 넣다가, buffer가 가득 찼을 때 문제가 발생하고, consumer는 buffer가 비어 빼낼 수 있는 item이 없다고 가정해보자.
그때, producer의 경우에는 buffer에 빈 자리가 생길 때 까지, consumer의 경우 item이 buffer에 들어올 때 까지 계속 확인해야 되는 문제가 발생한다.
이러한 문제를 monitor를 통해 해결해보자.
예제)
위 코드에서 Buffer는 critical section 안에서 mutual exclusion이 보장되어야 한다. 그렇지 않는다면, 여러 스레드에서 Buffer에 접근해 race condition이 발생할 수 있다.
그래서 mutex의 lock을 이용하면 이런 문제를 해결할 수 있다.
critical section 안에서는 producer의 경우 buffer가 가득 찼다면 waiting 상태에 들어가고, consumer의 경우 buffer가 비어있다면 waiting 상태로 들어가게 된다.
그리고 각각 producer는 consumer의 signal, broadcast를 가지고, consumer의 경우 producer의 signal, broadcast를 가져 waiting queue에 들어가 있는 스레드를 깨워주게 된다. producer의 경우 item을 버퍼에 넣었기 때문에 buffer가 비어서 waiting queue에 들어가있는 consumer의 스레드를 깨워줄 수 있는 것이고 consumer는 그 반대로 꽉 차있던 buffer를 비워줬기 때문에 waiting queue에 있는 스레드를 깨워주면 되는 것이다.
여기서 signal 혹은 broadcast를로 waiting 상태인 스레드를 깨운 뒤, 그 다음 어떤 식으로 실행될 것인지는 2가지로 나뉜다.
signal and continue
signal을 호출한 스레드가 계속해서 이어서 실행
signal and wait
signal으로 깨운 스레드가 실행된 뒤 signal을 호출한 스레드가 실행
이러한 차이가 있고 구현에 따라 다르기 때문에 원하는 구현에 맞춰서 사용하면 된다.
또 wait를 사용할 때 지켜야할 룰이 있는 그것은 반드시 while 루프 안에서 호출이 되어야한다.
그래야 lock을 획득했을 때 해당 조건에 충족되는지 확인할 수 있다. 그렇지 않는다면, lock을 획득했을 땐 분명 조건에 충족이 되었는데, lock 취득해서 실행하려고 했을 때 그 상태가 바뀌어있을 수 있다는 위험이 있다. 이에 대한 자세한 내용은 아래 출처에 있는 영상을 통해 따로 자세히 살펴보자.
자바에서의 모니터
자바에서는 모든 객체가 내부적으로 모니터를 가진다.
모니터의 mutual exclusion 기능은 synchronized 키워드로 사용이 가능하고, 자바의 모니터는 condition variable를 하나만 가진다.
자바 모니터의 세가지 기능을 다음과 같이 정의했다.
- wait
- notify (signal)
- notifyAll (broadcast)
예제)
위에서 살펴본 bounded producer/consumer problem 을 자바에서 위와 같이 해결할 수 있다.
BoundedBuffer 안에서 produce와 consume은 동시에 사용될 수 있는 메소드기 때문에 mutual exclusion을 보장해줘야 되는데 그때 synchronized 키워드를 통해 해결할 수 있다.
synchronized는 메서드 앞에 붙이는 방법이 있고, 메서드 안에서 synchronized 블록을 통해 사용할 수 있다. 블록을 통해 사용할 때는 자기 자신 객체를 뜻하는 this를 파라미터로 보내줘야 한다.
여기서 main 문의 consumer.start와 producer.start는 거의 동시에 실행된다고 봐도 무방하기 때문에 뭘 먼저 호출하는가는 상관이 없다.
정리
만약 위 글을 읽었을 때 이해가 되지 않거나 헷갈리는 부분이 있다면 출처의 영상을 찾아가서 한번 다시 보자. 자세히 설명되어있다.
출처)