개발 공부

손승열(Son Seungyeol)

[운영체제][OSTEP] 세마포어

세마포어가 무엇인지, 락과 컨디션 변수 대신 세마포어를 사용하는 방법은 무엇인지에 대해 공부해봅니다.

손승열(Son Seungyeol)
[운영체제][OSTEP] 세마포어

1️⃣ 세마포어: 정의

세마포어는 정수 값을 갖는 객체로서 두 개의 루틴으로 조작할 수 있다.
➡️ POSIX 표준에서 sem_wait()sem_post()임.

🌱 세마포어는 초기값에 의해 동작이 결정
➡️ 사용하기 전 "제일 먼저" 값을 초기화해야 함.

다음은 세마포어를 초기화하는 코드이다.

c
#include <semaphore.h>
sem_t s; // 세마포어 선언
sem_init(&s, 0, 1); // 세마포어의 값을 1로 초기화

sem_init()의 두 번째 인자는 같은 프로세스 내의 쓰레드 간에 세마포어를 공유한다는 것을 의미한다.

위 에서 제시한 두 루틴은 다음과 같이 나타낼 수 있다.

c
int sem_wait(sem_t *s) {
  decrement the value of semaphore s by one;
  wait if value of semaphore s is negative;
}

int sem_post(sem_t *s) {
  increment the value of semaphore s by one;
  if there are one or more threads waiting, wake one;
}

이 루틴들은 다수 쓰레드들에 의해 동시에 호출되는 것을 가정한다.
➡️ 임계 영역은 적절히 보호되어야 함.

위 루틴들의 핵심적인 성질을 살펴보자.

  • sem_wait() 함수는 즉시 리턴하거나(세마포어의 값 ≥ 1), 해당 세마포어 값이 1이상이 될 때까지 호출자를 대기시킴.

  • 다수의 쓰레드들이 sem_wait()를 호출할 수 있으므로 대기큐에는 다수의 쓰레드가 존재할 수 있음.

  • sem_wait() 함수와 달리 sem_post() 함수는 대기하지 않고 세마포어 값을 증가시키고 대기 중인 쓰레드 중 하나를 깨움.

  • 세마포어가 음수라면 그 값은 현재 대기 중인 쓰레드의 개수와 같음.

    ➡️ 일반적으로 세마포어 사용자는 이 값을 알 수 없음.


2️⃣ 이진 세마포어(락)

다음에서 "락"에 세마포어를 적용하는 모습을 살펴보자.

c
sem_t m;
sem_init(&m, 0, X); // X로 세마포어를 초기화하기. 이때 X가 가져야 할 값은?
sem_wait(&m);
// 임계 영역 부분은 여기에 배치
sem_post(&m);

sem_wait()/sem_post() 쌍으로 임계 영역 부분을 둘러싼 것을 확인할 수 있다.

이때, 위 코드가 동작하기 위한 핵심은 세마포어 m의 초기값(위의 코드에서 X)임.

💡 두 루틴의 정의에 따라 초기값은 1이 되어야 한다는 것을 알 수 있음.

다음과 같이 쓰레드가 두 개인 경우를 가정하여 이를 살펴보자.

[1] 한 쓰레드가 임계 영역 내에 있을 때 다른 쓰레드가 락을 획득하려고 하지 않는 경우

Value of Semaphore     Thread 0             Thread 1
────────────────────────────────────────────────────
         1    
         1             call sem_wait()
         0             sem_wait() returns
         0             (crit sect)
         0             call sem_post()
         1             sem_post() returns

[2] 한 쓰레드가 임계 영역 내에 있을 때 다른 쓰레드가 락을 획득하려고 하는 경우

Val │ Thread 0                State │ Thread 1           State
────┼───────────────────────────────┼─────────────────────────
 1  │                          Run  │                    Ready
 1  │ call sem_wait()          Run  │                    Ready
 0  │ sem_wait() returns       Run  │                    Ready
 0  │ (crit sect begin)        Run  │                    Ready
 0  │ Interrupt; Switch → T1  Ready │                     Run
 0  │                         Ready │ call sem_wait()     Run
 -1 │                         Ready │ decr sem            Run
 -1 │                         Ready │ (sem<0) → sleep    Sleep
 -1 │                          Run  │ Switch → T0        Sleep
 -1 │ (crit sect end)          Run  │                    Sleep
 -1 │ call sem_post()          Run  │                    Sleep
 0  │ incr sem                 Run  │                    Sleep
 0  │ wake(T1)                 Run  │                    Ready
 0  │ sem_post() returns       Run  │                    Ready
 0  │ Interrupt; Switch → T1  Ready │                     Run
 0  │                         Ready │ sem_wait() returns  Run
 0  │                         Ready │ (crit sect)         Run
 0  │                         Ready │ call sem_post()     Run
 1  │                         Ready │ sem_post() returns  Run

락은 두 개의 상태(사용 가능, 사용 중)만 존재하므로 이진 세마포어(binary semaphore)라고도 불림.


3️⃣ 컨디션 변수로서의 세마포어

대기 중인 쓰레드(들)가 프로그램에서의 어떤 조건(condition)이 만족되기를 대기하기 때문에, 세마포어를 컨디션 변수처럼 사용할 수 있음.

다음과 같이 부모 쓰레드가 자식 쓰레드를 생성한 후 자식 쓰레드의 종료를 대기하고자 하는 예시를 살펴보자.

parent: begin
child
parent: end
c
sem_t s;

void *child(void *arg) {
    printf("child\n");
    sem_post(&s); // 시그널 전달: 자식의 동작이 끝남
    return NULL;
}

int main(int argc, char *argv[]) {
    sem_init(&s, 0, X); // x의 값은 무엇이 되어야 할까?
    printf("parent: begin\n");
    pthread_t c;
    Pthread_create(c, NULL, child, NULL);
    sem_wait(&s); //자식을 여기서 대기
    printf("parent: end\n");
    return 0;
}

부모 프로세스는 자식 프로세스 생성 후 sem_wait()를 호출하여 자식의 종료를 대기함.

자식은 sem_post()를 호출하여 종료되었음을 알림.

💡 이때 세마포어의 초기값은 0이 되어야 한다.

다음에서 발생할 수 있는 두 가지 상황을 통해 그 이유를 알아보자.

[1] 자식 프로세스 생성 후, 아직 자식 프로세스가 실행을 시작하지 않은 경우

Val │ Thread 0            State │ Thread 1                   State
────┼───────────────────────────┼─────────────────────────────────
 0  │ create(Child)        Run  │ (Child exists, can run)    Ready
 0  │ call sem_wait()      Run  │                            Ready
 -1 │ decr sem             Run  │                            Ready
 -1 │ (sem<0) → sleep     Sleep │                            Ready
 -1 │ Switch → Child      Sleep │ child runs                  Run
 -1 │                     Sleep │ call sem_post()             Run
 0  │                     Sleep │ incr sem                    Run
 0  │                     Ready │ wake(Parent)                Run
 0  │                     Ready │ sem_post() returns          Run
 0  │                     Ready │ Interrupt → Parent         Ready
 0  │ sem_wait() returns   Run  │                            Ready

[2] 부모 프로세스가 sem_wait()를 호출하기 전에 자식 프로세스의 실행이 종료된 경우

Val │ Thread 0            State │ Thread 1                   State
────┼───────────────────────────┼─────────────────────────────────
 0  │ create(Child)        Run  │ (Child exists, can run)    Ready
 0  │ Interrupt → Child   Ready │ child runs                  Run
 0  │                     Ready │ call sem_post()             Run
 1  │                     Ready │ inc sem                     Run
 1  │                     Ready │ wake(nobody)                Run
 1  │                     Ready │ sem_post() returns          Run
 1  │ parent runs          Run  │ Interrupt → Parent         Ready
 1  │ call sem_wait()      Run  │                            Ready
 0  │ decr sem             Run  │                            Ready
 0  │ (sem≥0) → awake      Run  │                            Ready
 0  │ sem_wait() returns   Run  │                            Ready

4️⃣ 생산자/소비자(유한 버퍼) 문제

첫 번째 시도

먼저, emptyfull이라는 두 개의 세마포어를 사용하여 문제 해결을 시도해보자.

쓰레드는 emptyfull을 사용하여 버퍼 공간이 비었는지 채워졌는지를 표시한다.

다음과 같은 상황을 가정한 예시를 살펴보자.

  • MAX=1인 상황(버퍼의 크기가 1인 경우)

  • 생산자와 소비자 쓰레드가 각 하나씩 존재

  • CPU도 한 개 존재

put()get() 코드는 다음과 같다.

c
int buffer[MAX];
int fill = 0;
int use = 0;

void put(int value) {
  buffer[fill] = value;    // f1 라인
  fill = (fill + 1) % MAX; // f2 라인
}

int get() {
  int tmp = buffer[use];   // g1 라인
  use = (use + 1) % MAX;   // g2 라인
  return tmp;
}

다음은 해결을 위해 시도한 코드이다.

c
sem_t empty;
sem_t full;

void *producer(void *arg) {
    int i;
    for (i = 0; i < loops; i++) {
        sem_wait(&empty);          // P1 라인
        put(i);                    // P2 라인
        sem_post(&full);           // P3 라인
    }
}

void *consumer(void *arg) {
    int i, tmp = 0;
    while (tmp !=1) {
        sem_wait(&full);           // C1 라인
        tmp = get();               // C2 라인
        sem_post(&empty);          // C3 라인
        printf("%d\n", tmp);
    }
}

int main(int argc, char *argv[]) {
    // ...
    sem_init(&empty, 0, MAX);  // MAX 버퍼는 비어 있는 상태로 시작...
    sem_init(&full, 0, 0);     // ... 그리고 0이 가득 참
    // ...
}

소비자가 먼저 실행한다면, 소비자 쓰레드는 sem_wait(&full)을 호출(C1)한다.
➡️ 변수 full의 값은 0으로 초기화되었으므로 full의 값은 -1이 되고, 소비자는 대기한다.

이후 생산자 쓰레드가 실행하여 sem_wait(&empty) 루틴을 호출(P1)한다.
➡️ empty 변수가 MAX(이 예시에서는 1)로 설정되었으므로 대기하지 않고 계속 실행한다.

empty 변수는 0이 되고 생산자가 데이터 값을 버퍼의 첫 번째 공간에 넣는다.(P2)

그 후 생산자는 sem_post(&full)를 호출(P3)하여 full의 값을 -1에서 0으로 변경하고 소비자 쓰레드를 깨운다.
➡️ 소비자 쓰레드는 대기 상태에서 준비 상태로 바뀜.

💡 이제 둘 중 하나의 상황이 발생할 수 있다.

[1] 생산자가 계속 실행하는 상황

empty 세마포어의 값이 0이므로 대기 상태로 들어간다.(P1)

[2] 생산자 쓰레드가 인터럽트에 걸리고 소비자 쓰레드가 실행하는 상황

sem_wait(&full) 문(C1)을 호출하여 버퍼가 찼다는 것을 발견하고 데이터를 소비한다.

🤔 그렇다면 MAX 값이 1보다 크고, 생산자와 소비자 쓰레드들이 여러 개 있다면 어떨까?

이때는 경쟁 조건이 발생한다.

두 개의 생산자 Pa, Pb가 put()을 거의 동시에 호출하고 Pa가 먼저 실행된 경우

  1. Pa가 버퍼의 첫 공간에 값을 넣기 시작(f1 라인에서 fill=0)

  2. Pa 쓰레드가 fill 카운터 변수를 1로 변경하기 전에 인터럽트에 걸림.

  3. Pb가 실행되어 f1 라인에서 마찬가지로 버퍼의 첫 번째 공간에 데이터를 삽입한다.

    ➡️ ⚠️ Pa가 기록한 이전의 값은 새로운 값으로 대체됨.

상호 배제의 추가 but, ...

위에서는 상호 배제를 고려하지 않았다.
➡️ 이진 세마포어와 몇 개의 락을 추가하여 해결해보자.

c
sem_t empty;
sem_t full;
sem_t mutex;

void *producer(void *arg) {
    int i;
    for (i = 0; i < loops; i++) {
        sem_wait(&mutex);          // p0 라인(추가됨)
        sem_wait(&empty);          // p1 라인
        put(i);                    // p2 라인
        sem_post(&full);           // p3 라인
        sem_post(&mutex);          // p4 라인(추가됨)
    }
}

void *consumer(void *arg) {
    int i;
    for (i = 0; i < loops; i++) {
        sem_wait(&mutex);          // c0 라인(추가됨)
        sem_wait(&full);           // c1 라인
        int tmp = get();           // c2 라인
        sem_post(&empty);          // c3 라인
        sem_post(&mutex);          // c4 라인(추가됨)
        printf("%d\n", tmp);
    }
}

int main(int argc, char *argv[]) {
    // ...
    sem_init(&empty, 0, MAX);  // MAX 버퍼는 비어 있는 상태로 시작...
    sem_init(&full, 0, 0);     // ... 그리고 0이 가득 참
    sem_init(&mutex, 0, 1);    // 락이기 때문에 mutex=1 (추가됨)
    // ...
}

⚠️ 하지만 이 코드는 교착 상태가 발생할 수 있으므로 동작하지 않는다.

생산자와 소비자 쓰레드가 각 하나씩 있을 때, 소비자가 먼저 실행하는 경우

  1. mutex(c0)를 획득하고 full 변수에 대하여 sem_wait()(c1)를 호출

    ➡️ 아직 데이터가 없으므로 소비자는 대기해야 하고 CPU를 양보함.

    ➡️ ⭐ 소비자가 여전히 락을 획득하고 있음.

  2. 생산자가 실행하여 mutex 세마포어에 대해 sem_wait()(p0)를 호출

    ➡️ 이미 락은 소비자가 획득한 상태이므로 생산자 역시 대기 상태에 진입함.

소비자: mutex를 갖고 다른 full 시그널 발생 대기
🆚
생산자: full 시그널을 발생시켜야 하지만 mutex에서 대기
➡️ 전형적인 교착 상태의 모습

최종, 제대로 된 해법

이 문제를 해결하기 위해서는 락의 범위(scope)를 줄여야 함.

c
sem_t empty;
sem_t full;
sem_t mutex;

void *producer(void *arg) {
    int i;
    for (i = 0; i < loops; i++) {
        sem_wait(&empty);          // p1 라인
        sem_wait(&mutex);          // p1.5 라인(여기로 MUTEX 이동)
        put(i);                    // p2 라인
        sem_post(&mutex);          // p2.5 라인(... 그리고 여기)
        sem_post(&full);           // p3 라인
    }
}

void *consumer(void *arg) {
    int i;
    for (i = 0; i < loops; i++) {
        sem_wait(&full);           // c1 라인
        sem_wait(&mutex);          // c1.5 라인(여기로 MUTEX 이동)
        int tmp = get();           // c2 라인
        sem_post(&mutex);          // c2.5 라인(... 그리고 여기)
        sem_post(&empty);          // c3 라인
        printf("%d\n", tmp);
    }
}

int main(int argc, char *argv[]) {
    // ...
    sem_init(&empty, 0, MAX);  // MAX 버퍼는 비어 있는 상태로 시작...
    sem_init(&full, 0, 0);     // ... 그리고 0이 가득 참
    sem_init(&mutex, 0, 1);    // 락이기 때문에 mutex=1
    // ...
}

mutex를 획득하고 해제하는 코드를 임계 영역 바로 이전과 이후로 이동하였다.
➡️ 드디어 멀티 쓰레드 프로그램에서 사용 가능한 유한 버퍼


5️⃣ Reader-Writer 락

다양한 자료 구조를 접근하는 데 여러 종류의 락 기법이 필요

리스트에 삽입하고 간단한 검색을 하는 것과 같은 병행 연산이 여러 개 있다고 가정하자.

  • 삽입 연산: 리스트의 상태를 변경(전통적인 임계 영역 보호 방식으로 해결 가능)

  • 검색 연산: 자료 구조를 단순히 읽음.

💡 삽입 연산이 없다는 보장만 된다면 다수의 검색 작업을 동시에 수행할 수 있음.
➡️ 이와 같은 경우를 위해 만들어진 락: reader-writer 락

코드는 다음과 같다.

c
typedef struct _rwlock_t {
    sem_t lock; // 이진 세마포어(기본 락)
    sem_t writelock; // 하나의 쓰기 또는 다수의 읽기 락을 위한 락
    int readers; // 임계 영역 내에 읽기를 수행 중인 reader의 수
} rwlock_t;

void rwlock_init(rwlock_t *rw) {
    rw−>readers = 0;
    sem_init(&rw−>lock, 0, 1);
    sem_init(&rw−>writelock, 0, 1);
}

void rwlock_acquire_readlock(rwlock_t *rw) {
    sem_wait(&rw−>lock);
    rw−>readers++;
    if (rw−>readers == 1)
        sem_wait(&rw−>writelock); // 읽기용 락이 writelock을 획득
    sem_post(&rw−>lock);
}

void rwlock_release_readlock(rwlock_t *rw) {
    sem_wait(&rw−>lock);
    rw−>readers−−;
    if (rw−>readers == 0)
        sem_post(&rw−>writelock); // 마지막으로 읽기용 락이 writelock 해제
    sem_post(&rw−>lock);
}

void rwlock_acquire_writelock(rwlock_t *rw) {
    sem_wait(&rw−>writelock);
}

void rwlock_release_writelock(rwlock_t *rw) {
    sem_post(&rw−>writelock);
}

자료 구조를 "갱신"하려면 동기화 연산 쌍을 사용한다.

  • 락을 획득하기 위해서 rwlock_acquire_writelock()을 사용

  • 락을 해제하기 위해서 rwlock_release_writelock()을 사용

내부적으로는 writelock 세마포어를 사용하여 하나의 쓰기 쓰레드만이 락을 획득할 수 있도록 함.

⭐ 읽기 락(reader lock)의 획득과 해제 과정을 조금 더 살펴보자.

  1. 읽기 락 획득 시 읽기 쓰레드가 먼저 락을 획득

  2. 읽기 중인 쓰레드의 수를 표현하는 reader 변수를 증가시킴.

  3. writelock 세마포어에 대해 sem_wait()을 호출하여 쓰기 락을 함께 획득

  4. 읽기 락을 해제할 때 sem_post()로 쓰기 락을 다시 해제

이 과정을 통해서 읽기 락을 획득한 후, 다른 읽기 쓰레드들이 읽기 락을 획득할 수 있도록 함.

반면, 쓰기 락을 획득하려는 쓰기 쓰레드들은 모든 읽기 쓰레드가 끝날 때까지 대기해야 함.

⚠️ 하지만 이 방식은 공정성에 문제가 있음.
➡️ 상대적으로 쓰기 쓰레드가 불리

⚠️ 또한 이 방식은 오버헤드가 큼.
➡️ 기법이 정교하기 때문


6️⃣ 식사하는 철학자(Dining Philosopher)

dining philosopher

식사하는 철학자 문제는 다음과 같은 상황을 나타낸다.

  • 다섯 명의 "철학자"가 식탁 주위를 둘러앉았다.

  • 총 다섯 개의 포크가 철학자와 철학자 사이에 하나씩 놓여 있다.

  • 철학자는 식사하는 때가 있고 생각하는 때가 있다.

  • 생각 중일 때는 포크가 필요없다.

  • 식사를 위해서는 자신의 왼쪽과 오른쪽에 있는 포크를 들어야 한다.

💡 이 포크를 잡기 위한 경쟁과 그에 따른 동기화 문제가 병행 프로그래밍에서 다루려는 식사하는 철학자 문제이다.

다음은 각 철학자의 동작을 나타낸 기본 반복문이다.

c
while (1) {
  think();
  getforks();
  eat();
  putforks();
}

주요 쟁점은 getfork()putfork()의 루틴을 작성하되,
[1] 교착 상태의 발생을 방지해야 하고,
[2] 어떤 철학자도 못 먹어서 굶주리면 안되며
[3] 병행성이 높아야 한다.
➡️ 즉, 가능한 많은 철학자가 동시에 식사를 할 수 있어야 함.

문제 해결을 위해서는 Downey의 해법과 같이 몇 가지 함수를 사용함.

c
int left(int p) { return p; } // 철학자 p가 자신의 왼쪽 포크를 잡기 위해 호출
int right(int p) { return (p + 1) % 5; } // 철학자 p가 자신의 오른쪽 포크를 잡기 위해 호출

문제를 해결하기 위해 세마포어가 필요하므로, 각 포크마다 한 개씩하여 sem_t fork[5]로 정의한다.

[첫 번째 시도 - 불완전한 해답]

forks 배열에 있는 각 포크에 대한 세마포어를 1로 초기화하고 각 철학자는 자신의 순번(p)을 알고 있다고 가정하자.

c
void getforks() {
  sem_wait(forks[left(p)]);
  sem_wait(forks[right(p)]);
}

void putforks() {
  sem_post(forks[left(p)]);
  sem_post(forks[right(p)]);
}

원리는 다음과 같다.

  • 포크가 필요할 때 단순히 하나의 "락"을 획득

  • 먼저 왼쪽의 것을 잡고 그 다음에 오른쪽의 것을 잡음.

  • 식사가 끝나면 잡은 순서대로 놓음.

🚫 문제는 교착 상태이다.
➡️ 만약 각 철학자가 자신의 왼쪽 포크를 다른 철학자가 자신의 오른쪽 포크를 잡기 전에 먼저 잡았다면, 각 철학자는 하나의 포크만 들고서 다른 포크를 잡기를 평생 기다림.

다음의 그림을 통해 이러한 상황을 이해해보자.

dining philosopher 2

[두 번째 시도 - 의존성 제거]

위 문제를 해결하는 가장 간단한 방법은 최소한 하나의 철학자가 다른 순서로 포크를 집도록 하는 것이다.

가장 높은 순번의 철학자 4가 포크를 다른 순서로 획득한다고 가정하자. 그때의 코드는 다음과 같다.

c
void getforks() {
  if (p == 4) {
    sem_wait(forks[right(p)]);
    sem_wait(forks[left(p)]);
  } else {
    sem_wait(forks[left(p)]);
    sem_wait(forks[right(p)]);
  }
}

💡 마지막 철학자가 오른쪽 포크를 먼저 잡기 때문에 각 철학자가 하나의 포크를 든 채 다른 포크를 기다리는 대기 상황은 발생하지 않는다.
➡️ 환형 대기 상태가 끊어짐.

이와 비슷한 "유명한" 문제들은 다음과 같다.


7️⃣ 쓰레드 쓰로틀링

세마포어의 또 다른 간단한 사용 사례가 때때로 발생함.
➡️ 프로그래머가 "너무 많은" 쓰레드들이 동시에 작업을 수행하여 시스템이 중단되는 것을 어떻게 방지할 수 있을까?

💡 "너무 많은"에 대한 임계 값을 결정한 후 세마포어를 사용하여 동시에 실행하는 쓰레드 수를 제한한다.
➡️ 이러한 접근 방식을 쓰로틀링(throttling)이라고 부름.
➡️ 승인 제어(admission control)의 한 형태라고 여김.

어떤 문제를 병렬로 처리하기 위해 수백 개의 쓰레드를 생성하는 상황을 가정하자.

이때, 코드의 특정 부분에서 각 쓰레드는 계산의 일부를 수행하기 위해 많은 양의 메모리를 확보함.
➡️ 이 부분을 메모리 집약 영역(memory-intensive region)이라고 부르자.

⚠️ 모든 쓰레드가 동시에 메모리 집약 영역에 진입하면 모든 메모리 할당 요청의 합계가 시스템의 실제 메모리 양을 초과하게 됨.
➡️ 결과적으로 컴퓨터는 쓰레싱(thrashing)(즉, 디스크에서 페이지 교환)을 시작하고 전체 계산이 매우 느려짐.
🔖 쓰레싱: 지속적인 페이징 및 페이지 폴트(page fault) 상태로 이어져 CPU 이용률이 급격히 떨어지는 현상

✅ 간단한 세마포어가 이 문제를 해결할 수 있음.

세마포어의 값을 메모리 집약 영역에 한 번에 진입시키려는 최대 쓰레드 수로 초기화한 후 해당 영역 주위에 sem_wait()sem_post()를 배치하여 세마포어가 자연스럽게 쓰레드 수를 조절할 수 있도록 함.


8️⃣ 세마포어 구현

저수준 동기화 기법인 락과 컨디션 변수를 사용하여 우리만의 세마포어인 "제마포어(Zemaphore)"를 만들어 보자.

c
typedef struct __Zem_t {
    int value;
    pthread_cond_t cond;
    pthread_mutex_t lock;
} Zem_t;

// 오직 하나의 쓰레드만 이 문장을 호출할 수 있음
void Zem_init(Zem_t *s, int value) {
    s−>value = value;
    Cond_init(&s−>cond);
    Mutex_init(&s−>lock);
}

void Zem_wait(Zem_t *s) {
    Mutex_lock(&s−>lock);
    while (s−>value <= 0)
        Cond_wait(&s−>cond, &s−>lock);
    s−>value−−;
    Mutex_unlock(&s−>lock);
}

void Zem_post(Zem_t *s) {
    Mutex_lock(&s−>lock);
    s−>value++;
    Cond_signal(&s−>cond);
    Mutex_unlock(&s−>lock);
}

하나의 락과 하나의 컨디션 변수를 사용하고 세마포어의 값을 나타내는 상태 변수 하나를 사용하고 있다.

💡 Dijkstra가 정의한 세마포어와 여기서 정의한 제마포어 간의 중요한 차이가 있다.
➡️ 세마포어는 음수 값이 대기 중인 쓰레드의 수를 나타낸다는 것
➡️ 제마포어에서는 이 값이 0보다 작을 수가 없음.

이 방식이 구현하기도 쉽고 현재 Linux에 구현된 방식이기도 함.

세마포어를 사용하여 락과 컨디션 변수를 만드는 것은 까다로운 문제


📚 참고 문헌

Operating Systems: Three Easy Pieces ― 31: Semaphores

운영체제 아주 쉬운 세 가지 이야기 ― 34: 세마포어

관련있는 게시물

[운영체제][OSTEP] 컨디션 변수
개발 공부

[운영체제][OSTEP] 컨디션 변수

멀티 쓰레드 프로그램에서 쓰레드가 진행을 위해 컨디션 변수를 통해 특정 조건을 기다리는 방법에 대해 공부해봅니다.

손승열(Son Seungyeol)
손승열(Son Seungyeol)
[운영체제][OSTEP] 병행성 관련 오류
개발 공부

[운영체제][OSTEP] 병행성 관련 오류

일반적인 병행성 관련 오류들을 어떻게 처리하는지 공부해봅니다.

손승열(Son Seungyeol)
손승열(Son Seungyeol)

Made with React, Gatsby and DatoCMS by @smastrom

Contribute or star on GitHub

© 2022-2023 손승열 for 🅒🅞🅝🅣🅔🅝🅣🅢

모두 좋은 하루 보내세요! 😊