SW 사관학교 정글(Jungle)/운영체제-PintOS

[Pintos : 동기화] 모니터(Monitors)

jinsang-2 2024. 9. 25. 21:29

모니터(Monitors)

모니터는 세마포어나 락보다 더 높은 수준의 동기화 방법입니다. 모니터는 동기화되는 데이터(모니터 락이라고 불림), 그리고 하나 이상의 조건 변수로 구성됩니다. 스레드는 보호된 데이터에 접근하기 전에 먼저 모니터 락을 획득합니다. 이때 스레드는 "모니터에 있다"고 합니다. 모니터 안에서는 스레드가 모든 보호된 데이터를 자유롭게 검사하거나 수정할 수 있습니다. 데이터 접근이 완료되면 모니터 락을 해제합니다.

모니터의 기본 개념

  • 모니터 락: 모니터에서 보호되는 데이터에 접근하려면 먼저 이 락을 획득해야 합니다. 즉, 스레드가 데이터를 사용하려면 이 락을 잠가 다른 스레드가 동시에 접근하지 못하게 막습니다.
  • 조건 변수: 조건 변수를 사용하면 특정 조건이 만족될 때까지 스레드가 기다릴 수 있습니다. 예를 들어, 어떤 데이터가 도착했는지 기다릴 때 조건 변수를 사용합니다.

모니터 동작 방식

  1. 락 획득: 스레드가 보호된 데이터에 접근하려고 하면 먼저 락을 획득해야 합니다. 락을 획득한 스레드는 보호된 데이터에 대해 독점적인 권한을 가지게 됩니다.
    • 예를 들어, 여러 스레드가 공유 자원에 접근하려고 할 때, 먼저 락을 얻는 스레드만 자원을 사용할 수 있습니다. 이 스레드가 작업을 끝내고 락을 풀어주기 전까지 다른 스레드는 기다려야 합니다.
  2. 데이터 작업: 락을 획득한 스레드는 모니터 내부에서 자유롭게 데이터를 읽고 수정할 수 있습니다. 락을 가지고 있는 동안에는 다른 스레드가 이 데이터를 건드릴 수 없습니다.
  3. 조건 대기 및 신호: 만약 스레드가 조건이 만족될 때까지 기다려야 한다면, 조건 변수를 사용해 락을 풀고 기다립니다. 이때, 스레드는 락을 해제하면서 조건이 만족될 때까지 대기 상태에 들어갑니다.
    • 예를 들어, A 스레드는 특정 조건이 만족될 때까지 기다려야 합니다. 이때 A는 조건 변수를 사용해 락을 해제하고 대기 상태에 들어갑니다. B 스레드는 조건을 만족시킨 후 조건 변수를 통해 A를 깨워줄 수 있습니다.
  4. 조건 만족 후 재실행: 조건이 만족되면 조건 변수가 신호를 보내고, 기다리던 스레드는 락을 다시 획득한 후 실행을 계속할 수 있습니다. 이 과정에서 락을 다시 잠금으로써 안전하게 보호된 데이터를 사용할 수 있게 됩니다.

구체적인 동작 예시

1. 스레드 A의 동작 (조건을 기다림)

  • 스레드 A는 자원에 접근하려고 락을 얻습니다.
  • 스레드 A가 원하는 조건이 아직 만족되지 않았다면, 락을 해제하고 조건 변수를 통해 기다립니다.
  • 이때, 스레드 A는 조건이 만족될 때까지 대기하고, 다른 스레드가 자원을 사용할 수 있도록 락을 풀어줍니다.

2. 스레드 B의 동작 (조건을 만족시킴)

  • 스레드 B는 자원에 접근하려고 역시 락을 얻습니다.
  • 스레드 B가 작업을 완료해 조건을 만족시키면, 조건 변수에 신호를 보내 대기 중인 스레드 A를 깨웁니다.
  • 이후 락을 해제해 다른 스레드가 자원에 접근할 수 있도록 합니다.

3. 스레드 A가 다시 실행

  • 조건이 만족되면, 스레드 A는 조건 변수로부터 신호를 받고 다시 락을 획득해 자원에 접근합니다.
  • 조건이 만족되었기 때문에 스레드 A는 데이터를 사용하고, 필요한 작업을 완료한 후 락을 해제합니다.

모니터에서 자주 사용되는 함수들

  1. cond_wait(): 스레드가 조건을 기다리기 위해 사용하는 함수입니다. 이 함수는 락을 해제하고, 조건이 만족될 때까지 스레드를 대기 상태로 만듭니다.
    • 예: 스레드 A는 어떤 조건이 만족될 때까지 기다립니다. 이때 cond_wait()를 호출해 락을 풀고 대기합니다.
  2. cond_signal(): 대기 중인 스레드 중 하나에게 조건이 만족되었음을 알리고 깨우는 함수입니다.
    • 예: 스레드 B는 조건을 만족시킨 후 cond_signal()을 호출해 스레드 A를 깨웁니다.
  3. cond_broadcast(): 대기 중인 모든 스레드에게 조건이 만족되었음을 알리고 깨우는 함수입니다.
    • 예: 특정 조건이 여러 스레드에게 중요할 때 cond_broadcast()를 사용해 모든 스레드를 깨울 수 있습니다.

정리

  • 모니터는 보호된 자원에 대한 동시 접근을 제어하기 위한 고급 동기화 기법입니다.
  • 스레드는 자원에 접근하기 전에 모니터 락을 획득하고, 조건 변수를 사용해 특정 조건이 만족될 때까지 대기할 수 있습니다.
  • 조건이 만족되면 대기 중인 스레드는 락을 다시 획득해 작업을 진행하며, 이 과정에서 동시성 문제를 안전하게 처리할 수 있습니다.

모니터를 사용하면 여러 스레드가 동시에 같은 자원에 접근할 때 발생할 수 있는 충돌을 방지하면서 효율적으로 협력할 수 있게 해줍니다.

 

예제

생산자 - 소비자 문제를 모니터를 사용하여 해결하는 예제 코드

char buf[BUF_SIZE];         /* 버퍼 */
size_t n = 0;              /* 0 <= n <= BUF_SIZE: 버퍼에 있는 문자 수 */
size_t head = 0;           /* 다음에 쓸 문자에 대한 버퍼 인덱스 (BUF_SIZE로 나눈 나머지 사용) */
size_t tail = 0;           /* 다음에 읽을 문자에 대한 버퍼 인덱스 (BUF_SIZE로 나눈 나머지 사용) */
struct lock lock;          /* 모니터 락 */
struct condition not_empty; /* 버퍼가 비어있지 않을 때 신호를 보냄 */
struct condition not_full;  /* 버퍼가 가득 차지 않을 때 신호를 보냄 */

/* 락과 조건 변수를 초기화하는 부분 */
...initialize the locks and condition variables...

void put (char ch) {
    lock_acquire (&lock);               /* 모니터 락을 획득 */
    while (n == BUF_SIZE)               /* 버퍼가 가득 차면 */
        cond_wait (&not_full, &lock);   /* not_full 조건 변수에서 대기 */
    
    buf[head++ % BUF_SIZE] = ch;        /* ch를 버퍼에 추가 */
    n++;                                 /* 버퍼의 문자 수 증가 */
    cond_signal (&not_empty, &lock);     /* 버퍼가 비어있지 않음을 알림 */
    lock_release (&lock);                /* 락 해제 */
}

char get (void) {
    char ch;
    lock_acquire (&lock);                /* 모니터 락을 획득 */
    while (n == 0)                       /* 버퍼가 비어있으면 */
        cond_wait (&not_empty, &lock);   /* not_empty 조건 변수에서 대기 */
    
    ch = buf[tail++ % BUF_SIZE];         /* 버퍼에서 ch를 가져옴 */
    n--;                                  /* 버퍼의 문자 수 감소 */
    cond_signal (&not_full, &lock);       /* 버퍼가 가득 차지 않음을 알림 */
    lock_release (&lock);                 /* 락 해제 */
}

코드 설명

  1. 버퍼 정의 및 초기화:
    • char buf[BUF_SIZE];: 이 배열은 생산자와 소비자가 사용할 버퍼입니다. BUF_SIZE는 버퍼의 크기입니다.
    • size_t n = 0;: 현재 버퍼에 저장된 문자 수를 나타내며, 0에서 BUF_SIZE까지의 값을 가질 수 있습니다.
    • size_t head = 0;, size_t tail = 0;: head는 다음에 쓸 문자의 인덱스, tail은 다음에 읽을 문자의 인덱스를 나타냅니다. 두 인덱스는 원형으로 사용되며, BUF_SIZE로 나눈 나머지를 이용해 인덱스의 범위를 유지합니다.
    • struct lock lock;: 모니터 락을 나타내며, 보호된 데이터에 대한 동시 접근을 제어합니다.
    • struct condition not_empty; 및 struct condition not_full;: 각각 버퍼가 비어있지 않음을 알리는 신호와 버퍼가 가득 차지 않음을 알리는 신호를 위해 사용됩니다.
  2. put 함수 (생산자):
    • lock_acquire (&lock);: 모니터 락을 획득하여 버퍼에 대한 독점 권한을 확보합니다.
    • while (n == BUF_SIZE): 버퍼가 가득 차면, 추가 작업을 수행할 수 없으므로 대기 상태에 들어갑니다.
    • cond_wait (&not_full, &lock);: not_full 조건 변수에서 대기합니다. 이때 락을 해제하고, 다른 스레드가 버퍼에 문자를 추가할 수 있게 합니다.
    • buf[head++ % BUF_SIZE] = ch;: head 인덱스를 사용하여 버퍼에 문자를 추가합니다. 추가한 후 head 인덱스를 증가시킵니다.
    • n++;: 버퍼의 문자 수를 증가시킵니다.
    • cond_signal (&not_empty, &lock);: 이제 버퍼가 비어있지 않으므로 not_empty 조건 변수를 통해 대기 중인 소비자를 깨웁니다.
    • lock_release (&lock);: 작업이 끝난 후 락을 해제하여 다른 스레드가 버퍼에 접근할 수 있도록 합니다.
  3. get 함수 (소비자):
    • lock_acquire (&lock);: 모니터 락을 획득하여 버퍼에 대한 독점 권한을 확보합니다.
    • while (n == 0): 버퍼가 비어있으면, 읽기 작업을 수행할 수 없습니다.
    • cond_wait (&not_empty, &lock);: not_empty 조건 변수에서 대기합니다. 이때 락을 해제하여 다른 스레드가 버퍼에 문자를 추가할 수 있게 합니다.
    • ch = buf[tail++ % BUF_SIZE];: tail 인덱스를 사용하여 버퍼에서 문자를 가져옵니다. 가져온 후 tail 인덱스를 증가시킵니다.
    • n--;: 버퍼의 문자 수를 감소시킵니다.
    • cond_signal (&not_full, &lock);: 이제 버퍼가 가득 차지 않으므로 not_full 조건 변수를 통해 대기 중인 생산자를 깨웁니다.
    • lock_release (&lock);: 작업이 끝난 후 락을 해제하여 다른 스레드가 버퍼에 접근할 수 있도록 합니다.

주의사항

  • BUF_SIZE는 SIZE_MAX + 1로 나누어 떨어져야 합니다. 그렇지 않으면 head가 0으로 감싸는 첫 번째 시점에서 문제가 발생할 수 있습니다. 실제로는 BUF_SIZE가 2의 제곱수인 경우가 많습니다.

이와 같은 방식으로 모니터를 사용하면 생산자와 소비자가 안전하게 데이터를 공유할 수 있습니다. 모니터는 락과 조건 변수를 통해 스레드 간의 동기화를 효과적으로 관리합니다.