프로세스 동기화 2

Tasker_Jang·2026년 9월 30일
post-thumbnail

1. 생산자-소비자 문제

데이터를 만드는 쪽과 쓰는 쪽이 버퍼를 사이에 두고 협력합니다. 버퍼 칸 수가 정해져 있어서 bounded buffer problem이라고 부릅니다.

여기서 지켜야 할 것이 세 가지입니다.

  1. 생산자는 버퍼가 꽉 차면 기다려야 합니다
  2. 소비자는 버퍼가 비면 기다려야 합니다
  3. 둘이 같은 칸을 동시에 건드리면 안 됩니다

세 번째는 상호 배제이고, 앞의 둘은 남은 자원의 개수를 세는 문제입니다. 그래서 세마포어 세 개를 씁니다. mutex는 초기값 1로 상호 배제용, empty는 초기값 N으로 빈 칸 수, full은 초기값 0으로 찬 칸 수입니다.

producer:                     consumer:
do {                          do {
    produce item                  P(full);      // 찬 칸 있나
    P(empty);   // 빈 칸 있나      P(mutex);
    P(mutex);                     ... 버퍼에서 꺼냄 ...
    ... 버퍼에 넣음 ...            V(mutex);
    V(mutex);                     V(empty);     // 빈 칸 하나 늘었다
    V(full);    // 찬 칸 하나 늘었다  consume item
} while (1);                  } while (1);

P(empty)와 P(mutex)의 순서가 중요합니다. 뒤집으면 버퍼가 꽉 찬 상태에서 생산자가 mutex를 잡은 채로 empty를 기다리고, 소비자는 mutex를 얻지 못해 아무것도 꺼내지 못합니다. 서로 기다리며 멈춥니다.

작은 예제

버퍼 3칸으로 값 변화를 따라갑니다.

동작emptyfull결과
초기 상태30-
생산 1회21성공
생산 2회12성공
생산 3회03버퍼 꽉 참
생산 4회 시도03P(empty)에서 대기
소비 1회12대기하던 생산자가 깨어남

2. Readers-Writers 문제

공유 데이터베이스에 읽는 프로세스와 쓰는 프로세스가 접근합니다. 성질이 다릅니다.

  • 읽기끼리는 동시에 해도 됩니다. 아무도 값을 바꾸지 않기 때문입니다
  • 쓰는 중에는 아무도 접근하면 안 됩니다

전체를 하나의 mutex로 묶으면 정확하긴 한데, 읽기끼리도 줄을 서게 되어 손해입니다. 그래서 읽는 프로세스가 몇 명인지 세는 readcount 변수를 둡니다. 세마포어는 db(공유 데이터 접근권)와 mutex(readcount 보호용) 둘입니다.

writer:                        reader:
P(db);                         P(mutex);
... 쓰기 ...                    readcount++;
V(db);                         if (readcount == 1) P(db);   // 첫 독자가 잠금
                               V(mutex);
                               ... 읽기 ...
                               P(mutex);
                               readcount--;
                               if (readcount == 0) V(db);   // 마지막 독자가 품
                               V(mutex);

핵심은 첫 번째 독자만 잠그고 마지막 독자만 푼다는 것입니다. 중간 독자들은 그냥 들어갑니다. readcount 자체도 공유 데이터이므로 mutex로 보호해야 합니다.

이 방식에는 알려진 문제가 있습니다. 독자가 끊임없이 들어오면 readcount가 0이 되지 않아 쓰는 프로세스가 영원히 기다립니다.

3. 세마포어의 문제점

두 코드를 보면 알 수 있듯이, 세마포어는 동작은 하지만 쓰기 까다롭습니다.

실수결과
P와 V의 순서를 바꿈교착 상태
V를 빠뜨림다음 프로세스가 영원히 대기
P 대신 V를 먼저 호출상호 배제가 깨져 여럿이 동시 진입
P를 두 번 호출자기 자신이 영원히 대기

게다가 이런 오류는 특정 실행 순서에서만 드러나므로 재현과 디버깅이 어렵습니다. 한 줄 실수가 시스템 전체에 영향을 줍니다. 동기화 코드가 여기저기 흩어져 있다는 점도 문제입니다. P와 V가 서로 다른 함수, 다른 파일에 있으면 짝이 맞는지 눈으로 확인할 수 없습니다.

4. 모니터

모니터(monitor)는 동시에 수행 중인 프로세스들 사이에서 추상 자료형(abstract data type)의 안전한 공유를 보장하기 위한 고수준 동기화 구조입니다.

발상은 단순합니다. 공유 데이터와 그것을 다루는 연산을 하나로 묶어 두고, 모니터 안의 연산은 한 번에 하나의 프로세스만 수행할 수 있게 언어 차원에서 보장합니다. 상호 배제가 이미 걸려 있으므로 프로그래머가 직접 락을 걸 필요가 없습니다. 3절의 실수들이 애초에 불가능해집니다.

남는 문제는 "자원이 없어서 기다려야 하는" 경우입니다. 이때 쓰는 것이 조건 변수(condition variable)이고 연산은 둘입니다.

  • x.wait(): 호출한 프로세스를 x의 큐에 매달고 재웁니다
  • x.signal(): x의 큐에서 기다리던 프로세스 하나를 깨웁니다

조건 변수는 값을 가지지 않습니다. 프로세스를 큐에 매달아 재우거나 큐에서 깨우는 역할만 합니다. 세마포어와 결정적으로 다른 점입니다.

구분세마포어모니터
상호 배제프로그래머가 P/V로 직접언어가 보장
값정수값을 가짐조건 변수는 값 없음
signal을 아무도 안 기다릴 때값이 올라가 다음 P가 통과아무 일도 일어나지 않고 사라짐
실수 가능성높음낮음

5. 모니터로 쓴 식사하는 철학자

식사하는 철학자 문제를 모니터로 풀면 이렇게 됩니다. 젓가락을 하나씩 집는 대신, 양쪽이 모두 비었을 때만 한꺼번에 집습니다.

monitor dining_philosopher {
    enum { thinking, hungry, eating } state[5];
    condition self[5];

    void pickup(int i) {
        state[i] = hungry;
        test(i);                          // 먹을 수 있나 확인
        if (state[i] != eating)
            self[i].wait();               // 안 되면 잠든다
    }

    void putdown(int i) {
        state[i] = thinking;
        test((i + 4) % 5);                // 왼쪽 이웃을 깨워 본다
        test((i + 1) % 5);                // 오른쪽 이웃을 깨워 본다
    }

    void test(int i) {
        if (state[(i + 4) % 5] != eating &&
            state[i] == hungry &&
            state[(i + 1) % 5] != eating) {
            state[i] = eating;
            self[i].signal();
        }
    }
}

test()는 양옆 이웃이 식사 중이 아닐 때만 상태를 eating으로 바꿉니다. 따라서 "왼쪽만 집은 채로 멈추는" 상태가 아예 생기지 않습니다. 식사를 마친 철학자는 양옆 이웃에게 기회를 넘겨 줍니다. 락을 거는 코드가 한 줄도 없다는 점을 눈여겨볼 만합니다.

profile
ML Engineer 🧠 | AI 모델 개발과 최적화 경험을 기록하며 성장하는 개발자 🚀 The light that burns twice as bright burns half as long ✨

0개의 댓글