운영체제, lock에 관해 알아보자 3편 - 세마포어
[ 세마포어 ]
앞에서 mutex와 condition variable을 이용해서 동시성과 순서 제어를 한 코드를 살펴보았다. 그런데, 분리된 Lock과 Condition Variable를 한번에 구현하는 방법이 없을까? 이 생각을 가지고서 만들어진게 세마포어이다. 세마포어는 Lock일 수도 Condition Variable일 수도 있다. 어떻게 그게 가능한걸까?
세마포어는 딱 세 가지 함수만 알면 된다.
sem_init()
세마포어의 상태를 초기화하는 것으로 세마포어의 주소, 프로세스 공유 플래그, 초기값까지 총 3개의 파라미터를 받는다. 다른 건 모르겠지만 초기값은 세마포어 내에서 계속 변하는 값이다. 아래는 예시이다.
#include<semaphore.h>
sem_t s;
sem_init(&s, 0, 1);
sem_wait()
세마포어 값을 1 낮춘다. 만약에 세마포어 값이 0이하일 때 이 값이 호출되는 경우, 해당 쓰레드는 sleep 상태로 넘어간다. 양수일 때 호출되는 경우에는 아무런 동작을 하지 않고 넘긴다.
sem_post()
세마포어 값을 1 높인다. 만약에 음수인 경우 sleep 상태인 쓰레드를 하나 깨운다.
[ 세마포어의 활용 ]
세마포어를 Lock으로 사용
세마포어를 Lock으로 사용하는 방법은 시작 값을 1로 두면 된다. 그리고 경계 영역을 이전에 Lock과 Unlock을 두었듯이 아래처럼 sem_wait()과 sem_post()를 두면 Lock 처럼 사용할 수 있다.
sem_wait()
critical_section
sem_post()
이게 가능한 이유는 아래의 그림을 통해 설명이 가능하다. 만약에 임계영역이 끝나기 전에 컨텍스트 스위칭이 되더라도 다른 쓰레드가 임계영역으로 접근하면 sleep 상태로 들어가 CPU 점유를 넘긴다. 그리고 지금 세마포어로 된 Lock을 가진 Thread 0이 다시 제어권을 갖고 0으로 돌아오면 Thread 1을 깨우고 일을 처리한다.
세마포어를 Condition Variable로 사용
세마포어를 Condition Variable처럼 사용하려면 초기값을 어떻게 설정하고 어떻게 코드를 구성해야 할까? 세마포어 값을 0으로 두면 된다. 또한 이전에 Condition Variable로 Join 함수를 써서 부모 쓰레드가 자식 쓰레드보다 먼저 끝나는 것을 방지했던 것처럼 코드를 아래처럼 두면 된다.
void* child(void* arg) {
sem_post(&s);
return NULL;
}
int main(int argc, char* argv[]) {
sem_init(&s, 0, X);
pthread_t c;
pthread_create(c, NULL.child, NULL);
sem_wait(&s);
return 0;
}
그럼 자식 쓰레드가 생겼을 때 바로 컨텍스트 스위칭이 되는 경우와 sem_wait()이 호출되고 sleep 후 컨텍스트 스위칭 되는 상황을 생각하면 된다. 당연히 전자는 자식 쓰레드가 실행되고 나면 세마포어 값이 1이니 다시 부모 쓰레드로 제어가 넘어왔을 때에는 sem_wait을 불러도 값이 0이라 그냥 통과될 것이다. 아래의 그림처럼 말이다.
후자의 경우에는 wait을 부르는 순간에 값이 -1이니 부모 쓰레드는 sleep 상태로 넘어갈 것이다. 이 때, 자식 쓰레드로 제어가 넘어가는데 post로 세마포어 값을 0으로 바꾸면서 부모 쓰레드를 깨운다. 그러니 이 경우도 부모쓰레드보다 자식 쓰레드가 먼저 종료되는 것을 보장해준다.
세마포어와 생산자 소비자 문제
솔직하게 말해서 이 문제를 세마포어로 보는 것이 세마포어만 쓰는 경우와 뮤텍스 + Condition Variable의 사용의 차이를 가장 많이 알 수 있다고 생각한다. 이전 글에서 뮤텍스 + Condition Variable로 해결했던 문제를 다시 세마포어로 해결해보자.
들어가기 전에 앞서 생각할 건 코드가 당연히 어떻게 구성돼야 할 지는 예상이 갈 것이다. put과 get에 관한 건 특별한 게 없으니 생산자와 소비자 코드만 볼 것이다. 여기서는 각각의 초기화 값과 각각의 세마포어 위치를 어디다가 두어야 할 지 고민해봐야 한다.
우선 뮤텍스로 Lock을 구현하는 것만 생각해보자. 당연히 코드는 다음처럼 구성이 될 것이다. 그리고 mutex의 초기값은 Lock을 구현하기 위한 것이니 1일 것이다.
void* producer(void* arg) {
int i;
for (i = 0; i < loops; i++) {
sem_wait(&mutex);
put(i);
sem_post(&mutex);
}
}
void* consumer(void* arg) {
int i;
for (i = 0; i < loops; i++) {
sem_wait(&mutex);
int tmp = get();
sem_post(&mutex);
}
}
이전에 생산자 소비자 문제에서는 Condition Variable을 두 개 써야함을 얘기했었다. 그 이유는 생산자랑 소비자가 1대1이라면 문제가 없지만 그렇지 않기 때문에 모두가 다 잠들면서 서로 깨우길 기다리는 현상이 일어나기 때문이다. 그러면 똑같이 empty와 full로 세마포어를 만들어야 하는데, 어디에 두어야 하고 초기값은 어떻게 두어야 할까가 막막할 것이다.
일단 딱히 세마포어는 while문으로 그런지 검사할 필요가 없다. 왜냐하면 애초에 count라는 값으로 상태를 확인했던 게 세마포어 값으로 존재하기 때문이다. 값은 full을 기준으로 처음 시작할 때 아무것도 없다고 생각하자. 소비자가 먼저 시작하면 full을 찾을 때 바로 블록해버려야 된다. 왜냐하면 아직 생산자가 생산을 하지 않았는데 가져가 버리려고 하면 문제가 있기 때문이다. 그러면 위치를 생각해봐야 하는데 생산자는 소비자가 부족하다는 조건에 깨고 생산을 하면 소비자를 깨워야한다. 그래서 sem_wait(&empty)가 먼저 들어가고 생산하면 깨워야 하니 sem_post(&full)로 넣어야 한다. 소비자는 그 반대로 설정을 해줘야 한다.
그래서 다음처럼 코드가 나왔다. 아래의 코드는 문제가 있는 코드이다. 왜냐하면 소비자가 먼저 mutex를 잡고 full을 기다리고 생산자로 넘어왔다고 해보자. 그러면 full 값은 -1이다. 생산자는 mutex를 잡는데 이 때 mutex도 -1이 된다. 그러면 둘 다 sleep 상태에 들어가니 더 이상 작동이 되지 않는다. 이런 문제가 발생하기 때문에 반대로 해야 한다.
void* producer(void* arg) {
int i;
for (i = 0; i < loops; i++) {
sem_wait(&mutex);
sem_wait(&empty);
put(i);
sem_post(&full);
sem_post(&mutex);
}
}
void* consumer(void* arg) {
int i;
for (i = 0; i < loops; i++) {
sem_wait(&mutex);
sem_wait(&full);
int tmp = get();
sem_post(&empty);
sem_post(&mutex);
}
}
그래서 수정된 코드는 다음과 같다.
void* producer(void* arg) {
int i;
for (i = 0; i < loops; i++) {
sem_wait(&empty);
sem_wait(&mutex);
put(i);
sem_post(&mutex);
sem_post(&full);
}
}
void* consumer(void* arg) {
int i;
for (i = 0; i < loops; i++) {
sem_wait(&full);
sem_wait(&mutex);
int tmp = get();
sem_post(&mutex);
sem_post(&empty);
}
}
추가적으로 read-write Lock을 세마포어로 하는 법과 철학자의 식사 문제가 있는데, 단순히 Lock만을 이해하기에는 충분하다고 생각하고 다른 DB 쪽에서 Level Lock으로 설명을 하는 게 더 나을 수도 있을 것 가다는 생각이 들었다. 또한 철학자 식사는 데드락 부분에서 다시 이야기하도록 하겠다.
결과적으로 정리를 해보면 세마포어가 어떻게 보면 mutex Lock + condition variables + 내부 확인용 변수의 역할까지 가능하다. 개인적으로 뭐랄까 확실한 구분이 있는 걸 좋아해서 예전에 운영체제 실습 과제에서는 mutex + condition variable로 코드를 짜는 게 좋았다. 본인 맘대로 해라.
[ 참고 자료 ]
운영체제 - 서의성 강의