뮤텍스와 세마포, 모니터
뮤텍스와 세마포의 동작과 차이, 대기 큐로 Busy waiting을 없애는 방법, 모니터의 구조와 Java의 모니터를 정리한다.
시리즈 · 운영체제11 / 18
- 운영체제의 유형
- System Call과 Dual Mode
- 인터럽트와 폴링
- 프로세스와 PCB, 프로세스의 상태 변화
- 프로세스 주소 공간과 Stack이 높은 주소부터 할당되는 이유
- 스레드: 사용자·커널 수준 스레드와 Thread Pool, Fork-Join
- Context Switching
- 프로세스 스케줄링: 단계와 알고리즘
- Thread Scheduling: 경합 범위 PCS와 SCS
- 경쟁 상태와 상호 배제: Thread Safe, 데커·피터슨 알고리즘
- 뮤텍스와 세마포, 모니터
- 교착 상태: 발생 조건과 해결 방법
- 메모리 계층 구조와 캐시 메모리
- 메모리 관리: 주소 바인딩과 연속 메모리 할당
- 가상 메모리: Paging과 Segmentation, TLB
- 요구 페이징과 페이지 교체 알고리즘, Thrashing
- File Descriptor와 File System, I-Node
- 동기/비동기와 블로킹/논블로킹, I/O Multiplexing
공유 자원에 여러 프로세스가 동시에 접근하면 경쟁 상태가 발생한다. 이를 막으려면 한 번에 정해진 수의 프로세스만 접근하도록 제한하는 동기화 도구가 필요하다. 대표적인 것이 뮤텍스(Mutex)와 세마포(Semaphore), 그리고 모니터(Monitor)다.
뮤텍스 (Mutex)
- 임계 영역을 가진 스레드들의 실행 시간이 서로 겹치지 않고 단독으로 실행되게 하는(상호 배제, Mutual Exclusion) 기술이다. 동기화 대상이 하나다.
- 한 프로세스(스레드)가 소유할 수 있는 Key를 기반으로 한다. Key에 해당하는 객체를 소유한 스레드만 공유 자원에 접근할 수 있고, 두 스레드가 뮤텍스 객체를 동시에 가질 수 없다.
개념을 코드로 표현하면 다음과 같다. 실제 구현에서는 값을 확인하고 바꾸는 부분이 원자적으로 수행되어야 한다.
mutex = 1;
void lock() {
while (mutex != 1) {
/* mutex 값이 1이 될 때까지 기다린다 */
}
/* 여기에 도달했다면 mutex 값이 1이다.
값을 0으로 만들어 다른 프로세스(스레드)가 접근하지 못하게 막는다 */
mutex = 0;
}
void unlock() {
/* 임계 영역에서 나오면서 다른 프로세스가 접근할 수 있도록 락을 해제한다 */
mutex = 1;
}
세마포 (Semaphore)
다익스트라가 제안한 방법으로, 공유 자원에 접근할 수 있는 프로세스(스레드)의 수를 나타내는 값 하나를 공통으로 관리해 상호 배제를 달성한다. 동기화 대상이 하나 이상이며, 일반적으로 비교적 긴 시간 동안 확보하는 리소스에 사용한다.
세마포 변수 S는 초기화와 두 연산으로만 접근할 수 있다.
- P (Proberen, 검사): wait 동작
- V (Verhogen, 증가): 대기 중인 프로세스를 깨우려고 신호를 보내는 signal 동작
가장 단순한 형태는 다음과 같은데, 이 구현에는 Busy waiting이 남아 있다.
P(S) {
while (S <= 0) do
endwhile;
S <- S - 1;
}
V(S) {
S <- S + 1;
}
대기 큐를 추가한 세마포
세마포에 큐를 두고, 진입하지 못한 프로세스를 큐에 넣어 중단시키면 OS의 도움으로 Busy waiting이 사라진다(Block-Wakeup 방식). 값을 확인한 뒤 감소시키는 것이 아니라, 먼저 감소시킨 뒤 자원이 부족하면(음수이면) 대기 큐에 추가한다.
struct semaphore {
int count;
queueType queue;
};
semaphore S;
// wait 연산 (P)
wait(S) {
S->count--;
if (S->count < 0) {
// 현재 프로세스가 공유 자원에 접근할 수 없다는 뜻
add this process to S->queue; // 프로세스를 세마포의 큐에 추가
block(); // 프로세스를 블록 상태로 전이
}
}
// signal 연산 (V)
signal(S) {
S->count++;
if (S->count <= 0) {
// count가 0 이하라면 대기 중인 프로세스가 있다는 뜻
remove a process P from S->queue; // 큐에서 프로세스를 꺼냄
wakeup(P); // 준비 상태로 전이시켜 ready 큐에 연결
}
}
임계 영역은 wait와 signal로 감싸서 보호한다.
semaphore s = 1;
void P(int i) {
while (true) {
wait(s);
/* 임계 영역 */
signal(s);
/* 임계 영역 이후 코드 */
}
}
이진 세마포
값으로 0과 1만 가질 수 있는 세마포다. 뮤텍스와 비슷하게 동작하지만, 아래의 소유권 차이가 있다.
뮤텍스와 세마포의 차이
| 구분 | 뮤텍스 | 세마포 |
|---|---|---|
| 동기화 대상 | 1개 | 1개 이상 |
| 방식 | 소유할 수 있는 Key 기반의 락 | 접근 가능한 수를 나타내는 값으로 조율하는 signaling mechanism |
| 소유 | 락을 획득한 스레드가 소유하고 책임진다 | 소유 개념이 없다 |
| 해제 | 소유한 스레드만 해제할 수 있다 | 다른 스레드도 해제(signal)할 수 있다 |
모니터
세마포는 P와 V가 반드시 짝을 이루어 실행되어야 하는데, 코드가 많아지면 이를 지키기가 매우 복잡해진다. 이 문제를 해결하는 것이 모니터다.
- 상호 배제와, 특정 조건이 만족될 때까지 대기하는 기능을 함께 제공하는 동기화 구조다.
- 추상 데이터 타입(Abstract Data Type)으로, 객체지향 개념과 유사하다. 공유 자원과 그 자원에 접근하는 프로시저로 구성된다.
- 클래스 형태로 제공되어 사용하기 간편하다. 스레드는 공유 자원에 직접 접근할 수 없고 반드시 프로시저를 통해서만 접근한다.
모니터의 구조는 다음과 같다.
- Entry queue (진입 큐): 모니터 내의 procedure 수만큼 존재한다.
- Mutual exclusion: 모니터 안에는 항상 하나의 프로세스만 진입할 수 있다.
- Information hiding (정보 은폐): 공유 데이터는 모니터 안의 프로세스만 접근할 수 있다.
- Condition queue (조건 큐): 모니터 안에서 특정 이벤트를 기다리는 프로세스가 대기한다.
- Signaler queue (신호 제공자 큐): 모니터에 항상 하나 존재하며,
signal()을 실행한 프로세스가 임시로 대기한다.
Monitor monitor_name
{
// 공유 데이터 변수 선언
procedure P1( ... ) { ... }
procedure P2( ... ) { ... }
...
procedure Pn( ... ) { ... }
initialization_code( ... ) { ... } // 초기화
}

Java의 모니터
Java는 모니터로 스레드를 동기화한다.
- 모든 Java 객체는 모니터를 하나씩 가지고 있고, 모니터는 한 번에 하나의 스레드만 소유할 수 있다.
synchronized키워드가 사용된 객체가 공유 객체가 되며, JVM은 그 객체의 모니터로 상호 배제를 수행한다.- 다른 스레드가 소유한 모니터를 획득하려는 스레드는, 소유한 스레드가 모니터를 해제할 때까지 큐에서 대기한다.
wait()와notify()를 이용하면 우선적으로 실행되어야 하는 스레드가 있을 때 스레드 간 제어권의 순서를 개발자가 정할 수 있다.

JVM은 객체 헤더의 Mark Word에 락 상태를 기록하며, 경쟁 정도에 따라 락의 무게를 달리한다. 경쟁이 없을 때는 Biased Lock, 경쟁이 가벼울 때는 Light-weight Lock을 쓰고, 경쟁이 심해지면 Heavy-weight Lock, 즉 모니터 방식의 상호 배제로 전환한다.
참고
- 운영체제: 그림으로 배우는 구조와 원리 (한빛아카데미)
- worthpreading.tistory.com/90