• os
  • concurrency
  • thread

뮤텍스와 세마포, 모니터

뮤텍스와 세마포의 동작과 차이, 대기 큐로 Busy waiting을 없애는 방법, 모니터의 구조와 Java의 모니터를 정리한다.

시리즈 · 운영체제11 / 18
  1. 운영체제의 유형
  2. System Call과 Dual Mode
  3. 인터럽트와 폴링
  4. 프로세스와 PCB, 프로세스의 상태 변화
  5. 프로세스 주소 공간과 Stack이 높은 주소부터 할당되는 이유
  6. 스레드: 사용자·커널 수준 스레드와 Thread Pool, Fork-Join
  7. Context Switching
  8. 프로세스 스케줄링: 단계와 알고리즘
  9. Thread Scheduling: 경합 범위 PCS와 SCS
  10. 경쟁 상태와 상호 배제: Thread Safe, 데커·피터슨 알고리즘
  11. 뮤텍스와 세마포, 모니터
  12. 교착 상태: 발생 조건과 해결 방법
  13. 메모리 계층 구조와 캐시 메모리
  14. 메모리 관리: 주소 바인딩과 연속 메모리 할당
  15. 가상 메모리: Paging과 Segmentation, TLB
  16. 요구 페이징과 페이지 교체 알고리즘, Thrashing
  17. File Descriptor와 File System, I-Node
  18. 동기/비동기와 블로킹/논블로킹, 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()를 이용하면 우선적으로 실행되어야 하는 스레드가 있을 때 스레드 간 제어권의 순서를 개발자가 정할 수 있다.

Java 모니터의 Entry Set과 Wait Set, wait()와 notify()에 따른 스레드 이동

JVM은 객체 헤더의 Mark Word에 락 상태를 기록하며, 경쟁 정도에 따라 락의 무게를 달리한다. 경쟁이 없을 때는 Biased Lock, 경쟁이 가벼울 때는 Light-weight Lock을 쓰고, 경쟁이 심해지면 Heavy-weight Lock, 즉 모니터 방식의 상호 배제로 전환한다.

참고