다양한 범주의 병행성 문제 해결을 위해서는 락과 조건 변수가 모두 필요하다. 이번 장에서는 세마포어(semaphore) 라는 동기화 기법에 대해 알아볼 것이다. 세마포어는 락과 컨디션 변수로 모두 사용할 수 있다.
핵심 질문: 세마포어를 어떻게 사용하는가
- 락과 컨디션 변수 대신에 세마포어를 사용하는 방법은 무엇인가?
- 세마포어의 정의는 무엇인가?
- 이진 세마포어는 무엇인가? 락과 컨디션 변수를 사용하여 세마포어를 만드는 것이 가능한가?
- 그 반대로 세마포어를 사용하여 락과 조건 변수를 만드는 것이 가능한가?
세마포어: 정수 값을 갖는 객체이며, 공유자원에 제한된 개수의 프로세스 또는 쓰레드만 접근할 수 있도록 하는 객체.
sem_wait()와 sem_post() 두 개의 루틴으로 조작할 수 있다.#include <semaphore.h>
sem_t s;
sem_init(&s, 0, 1); // 세마포어 변수 s를 1로 초기화sem_wait()와 sem_post()int sem_wait(sem_t *s) {
    decrement the value of semaphore s by one; // 세마포어 값 1 감소
    if(value of semaphore s is negative)
    	wait; // 세마포어가 음수면 대기
}
int sem_post(sem_t *s) {
    increment the value of semaphore s by one; // 세마포어 값 1 증가
    if (there are one or more threads waiting) 
    	wake one; // 하나 이상의 쓰레드가 대기중이면, 그 중 하나 깨우기
}sem_wait(): 세마포어의 값이 1 이상이면 즉시 리턴, 아니면 해당 세마포어 값이 1 이상이 될 때까지 호출자를 대기시킨다.sem_post(): 세마포어 값을 증가시키고 대기 중인 쓰레드 중 하나를 깨운다 (대기하지 않는다).이제 세마포어를 사용할 준비가 되었다. 우리가 처음으로 세마포어를 적용할 곳은 이미 친숙한 “락” 이다.
sem_t m;
sem_init(&m , 0 , X); // X로 세마포어를 초기화하기. 이때 X의 값은?
sem_wait(&m);
// 임계 영역 부분은 여기에 배치
sem_post(&m);sem_wait()와 sem_post() 의 정의를 되새겨보면 초기값은 1이 되어야 한다는 것을 알 수 있다.sem_wait()를 호출한다. 먼저 세마포어 값을 1 감소시켜 0으로 만든다.sem_post()를 불렀을 때 세마포어 값이 다시 1로 설정된다.
sem_wait()를 호출하여 임계 영역 진입을 시도하면, 쓰레드 1이 세마포어 값을 -1로 감수시키고 대기에 들어간다.sem_post()를 호출하고, 세마포어 값을 0으로 증가시키고, 잠자던 쓰레드 1을 깨운다.
세마포어를 락으로 쓸 수 있다는 것을 알았다. 락은 두 개의 상태(사용 가능, 사용 중)만 존재하므로 이진 세마포어 (binary semaphore) 라고도 불린다.
어떤 조건이 참이 되기를 기다리기 위해 현재 쓰레드를 멈출 때에도 세마포어는 유용하게 사용될 수 있다.
즉, 쓰레드들이 어떤 조건(condition)이 만족되기를 대기하기 때문에, 세마포어를 컨디션 변수처럼 사용할 수 있다.
sem_t s;
void *child(void *arg) {
    printf("child\n");
    sem_post(&s); // 시그널 전달: 자식의 동작이 끝남
    return NULL;
}
intmain(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;
}parent: begin
 child
 parent: endsem_wait()를 호출하여 자식의 종료를 대기한다.sem_post()를 호출하여 종료되었음을 알린다.sem_post()를 호출하기 전에 부모가 sem_wait()를 호출할 것이다.wait() 호출 전에 세마포어 값이 0보다 같거나 작아야 한다.
sem_wait()를 호출하기 전에 자식 프로세스의 실행이 종료된 경우sem_post()를 호출하여 세마포어의 값을 0에서 1로 증가시킨다. sem_wait()를 호출한다. 세마포어 값이 1인 것을 발견할 것이다. sem_wait()	에서 대기 없이 리턴한다.
생산자/소비자 문제 (유한버퍼 문제): 공유 자원의 동기화 문제. 생산자가 자원을 생산할 수 없거나(꽉 참) 소비자가 자원을 소비할 수 없을(비어있음) 수 있다.
생산자/소비자 문제도 세마포어로 해결할 수 있다.
empty와 full이라는 두 개의 세마포어를 사용하여, 버퍼 공간이 비었는지 채워졌는지를 표시한다.
put/get 루틴
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;
}해법
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)을 호출한다.sem_wait(&empty) 루틴을 호출한다. sem_post(&full)를 호출하여 세마포어의 full의 세마포어의 값을 -1에서 0으로 변경하고 소비자 쓰레드를 깨운다.put()을 거의 동시에 호출하였다고 가정하자.위의 문제에서는 상호 배제를 고려하지 않았다. 아래의 코드는 상호배제를 추가했지만 잘못된 방법이다.
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 (추가됨)
    // ...
}sem_wait()를 호출한다(c1). sem_wait()를 실행한다(p0 라인). 이 문제를 해결하기 위해서는 락의 범위 (scope) 를 줄여야 한다.
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 (추가됨)
    // ...
}또 하나의 고전적인 문제가 있다. 좀 더 융통성 있는 락 기법이 필요하다. 다양한 자료 구조를 접근하는 데 여러 종류의 락 기법이 필요하다.
예를 들어 리스트 삽입/검색에 대한 병행 연산이 여러 개 있다고 하자. 삽입 연산이 없다는 보장만 된다면, 다수의 검색 작업을 동시에 수행할 수 있다.
이와 같은 경우를 위해 만들어진 락이 reader-writer 락이다.
간단한 reader-writer 락
// rw락 구조체
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() rwlock_acquire_readlock(), reader 변수 증가rwlock_release_readlock(), reader 변수 감수다익스트라가 제기한 유명한 세마포어 문제이다.
다섯 명의 “철학자”가 식탁 주위를 둘러앉았다. 총 다섯 개의 포크가 철학자와 철학자 사이에 하나씩 놓여 있다. 자신의 왼쪽과 오른쪽에 있는 포크를 들어야 식사를 할 수 있다. 이 포크를 잡기 위한 경쟁과 그에 따른 동기화 문제가 병행 프로그래밍에서 다루려는 식사하는 철학자 문제이다. 여기 각 철학자의 동작을 나타낸 기본 반복문이 있다.
주요 쟁점은 getfork()와 putfork()의 루틴을 작성하되 교착 상태의 발생을 방지해야 하고, 어떤 철학자도 못 먹어서 굶주리면 안되며 병행성이 높아야 한다 (즉,가능한 많은 철학자가 동시에 식사를 할 수 있어야 한다).

int left(int p) { return p; }
int right(int p) { return (p + 1) % 5; }void getforks() {
    sem_wait(forks[left(p)]);
    sem_wait(forks[right(p)]);
}
void putforks() {
    sem_post(forks[left(p)]);
    sem_post(forks[right(p)]);
}가장 간단한 방법은 최소한 하나의 철학자가 다른 순서로 포크를 집도록 하면 된다.
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)]);
    }
}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);
}