condition Synchronization
만약 쓰레드 A가 쓰레드 B, C의 작업이 다 끝나고 수행되어야 하는, 의존성이 존재하는 쓰레드라고 했을 때 B와 C의 작업이 끝날때 까지 A를 대기시키고, B와 C의 작업이 끝났다는 조건이 충족되면 A가 실행되어야 할 것이다.
[Thread-1]
int main(int argc, char *argv[]) {
pthread_t c;
printf("parent: begin\n");
pthread_create(&c, NULL, child, NULL);
while (done == 0) { }
printf("parent: end\n");
return 0;
}
[Thread-2]
void * child (void *arg) {
printf("child\n");
done = 1;
return NULL;
}
(Thread 2에서 작업이 끝나고 전역변수 done이 1로 바뀌어야 메인 쓰레드인 Thread 1에서 나머지 작업을 수행한다)
이는 while 문으로 구현하여 CPU를 계속해서 사용해야 하므로 비효율이 발생한다.
따라서 OS 측에서 어떤 조건을 만족하기 전까지 쓰레드를 block상태로 바꿔주고 조건을 만족하면 깨워주는 api를 제공한다.
pthread api
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *c, pthread_mutex_t *m);
int pthread_cond_signal(pthread_cond_t *c);
변수 c는 condition variable로 tread가 만족해야 하는 값이다. 이는 동시에 여러 쓰레드가 접근하는 공유 자원이기도 하기에 mutex를 이용하여 상호배제를 지켜줘야 한다.
세마포어에서 사용한 wait와 signal 함수와 똑같이 작동한다.
pthread의 사용
C는 wait된 쓰레드의 대기큐라고 할 때
wait할 thread에서 thr_join을 실행하므로 메인 함수에서 child 쓰레드가 끝날 때까지 기다리는 코드이다.
pthread_create로 thread를 생성하는 순간 child 함수가 실행되고 바로 밑 줄에서 child가 끝나지 않았다면 메인코드를 대기시킨다.
thr_join이 호출된 시점에서 조건 변수에 접근하기 위해 lock을 걸고 만약 조건 변수가 0일 때(child가 끝나지 않음) wait로 쓰레드를 잠재워 block상태로 대기큐에 집어 넣는다.
child의 모든 작업이 끝나고 조건 변수를 1로 바꾸고 signal을 호출하면 대기큐에 존재하는 메인 쓰레드를 깨우게 된다.
mutex를 사용하지 않는다면?
wait을 호출하기 전에 lock을 걸어주지 않는다면,
스케쥴러가 쓰레드1의 wait이 호출되기 전에 쓰레드2를 실행하게 될 수도 있다. 그렇게 되면 block된 쓰레드가 없는 상태에서 쓰레드2의 signal이 호출되고, 다시 쓰레드1로 돌아와 wait함수로 쓰레드1이 자게 된다면 깨워줄 signal이 없어진 상태이다.
또한 wait을 호출하면서 동시에 unlock 해야 하는데, 그렇게 하지 않으면
다시 깨워줄 쓰레드에서의 접근이 불가능 하기 때문이다.
조건 변수를 사용하지 않는다면?
조건 변수를 사용하지 않는다면 쓰레드2의 작업이 끝나고 join이 실행될 때 조건을 만족했음에도 불구하고 wait을 호출할 수도 있기 때문에 while문으로 조건을 걸어 조건을 만족하지 못할 때만 쓰레드를 재워야 한다.
생산자 소비자 문제
생산자 소비자라는 것은 쓰레드를 생산하고 소비한다는 말이다.
웹 서버에서 멀티쓰레드를 예로 들면 사용자가 어떤 페이지를 이동하고 싶을 때 페이지를 클릭하면 전체 서버에 쓰레드를 하나 생산하는 것이고 서버 컴퓨터는 이 쓰레드를 처리해야 하는 소비자가 되는 것이다.
생산자는 웹 서버에 접속해 있는 동안 계속해서 쓰레드를 생산(put) 할 수 있고 소비자는 쓰레드가 존재하는지 계속 확인하며 소비(get) 할 수 있어야 한다.
이 쓰레드는 버퍼라는 공간에 저장되고 버퍼의 저장공간의 한계가 있기 때문에 생산할 수 있는 쓰레드의 한계치가 정해져 있다. 위 코드는 max값이 1일 때 만약 쓰레드의 갯수가 0일 때만 생산하고 count의 갯수가 1일 때 처리하는 코드이다.
위의 예제와 다르게 웹 서버에서는 여러개의 쓰레드가 추가될 수 있고 때문에 동기화가 필요하다.
void producer() {
while (1) {
pthread_mutex_lock(&m);
if (count == MAX)
pthread_cond_wait(&c, &m);
put(value);
pthread_cond_signal(&c);
pthread_mutex_unlock(&m);
}
}
void consumer() {
while (1) {
pthread_mutex_lock(&m);
if (count == 0)
pthread_cond_wait(&c, &m);
get();
pthread_cond_signal(&c);
pthread_mutex_unlock(&m);
}
}
위에선 count가 max일 때 생산자 thread를 wait하고 count가 0일 때는 소비자 thread를 wait하여 동기화를 시도한다.
하지만 한명의 생산자와 두개 이상의 소비자가 존재했을 때 문제를 발생시키는 코드이다.
만약 첫 번째 소비자가 wait을 만나 자고 있고 한명의 생산자에 의해 count가 1이 되고 그 후 스케쥴러가 첫 번째 소비자를 깨우게 된다면 count 정상적으로 작동한다.
하지만 스케쥴러가 만약 자고 있지 않은 두 번째 소비자를 실행하게 된다면 count가 0이 되고 다시 스케쥴러에 의해 소비자 1이 실행될 때 이미 조건을 지나친 후므로 텅 빈 버퍼에서 get을 호출하여 에러가 발생할 것이다.
이를 방지하기위해 if를 while로 바꿔주면 소비자 1로 돌아와도 다시 조건을 확인하게 되고 count가1 0이므로 다시 잠에 들게 될 것이다.
이 또한 문제점이 발생하는데,,
void producer() {
while (1) {
pthread_mutex_lock(&m);
while (count == MAX)
pthread_cond_wait(&c, &m);
put(value);
pthread_cond_signal(&c);
pthread_mutex_unlock(&m);
}
}
void consumer() {
while (1) {
pthread_mutex_lock(&m);
while (count == 0)
pthread_cond_wait(&c, &m);
get();
pthread_cond_signal(&c);
pthread_mutex_unlock(&m);
}
}
위의 코드에서 wait로 재운 소비자와 생산자가 존재하는 대기큐가 같은 주소를 사용하는 것을 볼 수 있다.
여전히 생산자 1명, 소비자 2명이 있다고 가정했을 때 초기에 count값은 0이고, 소비자가 먼저 실행된다면 두 개의 소비자 모두 while문을 돌며 ccount가 0이므로 잠에들 것이다. 이제 생산자가 실행되고 쓰레드를 생성하여 count가 1이되고 잠에 들 것이다.
다시 소비자 코드가 실행되어 count가 1이므로 get()으로 쓰레드를 처리하고 다시 생산자를 깨우려 signal을 호출하는데 소비자와 생산자가 같은 큐를 사용하기 때문에 생산자가 아니라 잠자고 있는 또다른 소비자를 깨울 수 있는 우려가 있다.
그렇게 실행된 소비자는 count가 0이므로 wait함수가 호출되고 두 명의 소비자와 한명의 생산자 모두 잠자고 있는 현상이 발생한다.
해결법
생산자는 큐가 비어있어야 쓰레드를 생산하므로 empty,
소비자는 큐가 꽉차있어야 쓰레드를 처리하므로 fill 이라는 queue를 각자 가지고 있고
생산자가 소비자를 깨울 때는 fill을, 소비자가 생산자를 깨울 때는 empty를 인자로 넣어주는 것을 볼 수 있다.
'CS > OS' 카테고리의 다른 글
Dead Lock - Resource-Allocation Graph (0) | 2022.05.27 |
---|---|
Semaphore as Condition Variable (0) | 2022.05.26 |
Semaphore with 수도 코드 (0) | 2022.05.25 |
Race condition과 간단한 동기화 (0) | 2022.05.23 |
멀티프로세서 스케쥴링 (0) | 2022.05.02 |