18. Condition Variables
#컨디션변수 #생산자소비자문제
🔗 Condition Variable
지금까지 락을 통해 병행 프로그램을 작성하는 방법을 배웠다. 하지만 “락”만으로는 병행을 완벽하게 구현할 수 없다. 특히 spin작업을 할때 while문에서 자원과 시간을 낭비하게 된다. 이는 매우 비효율 적이다. 컨디션 변수는 일종의 큐 자료구조로서, 어떤 조건이 될때까지 쓰레드가 대기할 수 있는 큐이다. 컨디션 변수에는 두가지 연산이있다. wait과 signal이다. wait()은 쓰레드가 스스로를 잠재우기 위해 호출하는 것이고, signal()은 자고있던 쓰레드를 깨울 때 호출한다.
pthread_cond_t c;
pthread_cond_wait(pthread_cond_t *c, pthread_mutex_t *m);
pthread_cond_signal(pthread_cond_t *c);
🔷 매우 중요한 이야기
wait()가 호출될때 mutex는 잠겨있다고 가정한다. wait()의 역할은 락을 해제하고 호출한 쓰레드를 재우는 것이다. 어떤 다른 쓰레드가 시그널을 보내어 쓰레드가 깨어나면, wait()에서 리턴하기 전에 락을 재획득 해야한다.
즉, wait()를 리턴할 때 락을 재획득 해야한다는 것이다. 잠에서 깨어나도 락을 획득하지 못하면 다시 sleep상태로 돌아간다. 이는 경쟁 조건의 발생을 방지하기 위함이다.
생산자/소비자 문제
이번에 살펴볼 동기화 문제는 다익스트라가 제시한 생산자/소비자 문제이다. 유한 버퍼 문제로 알려져있고 락이나 컨디션 변수를 대신한 세마포어가 발명된 이유가 이 생산자/소비자 문제 때문이다. 여러개의 생산자 쓰레드와 소비자 쓰레드가 있다. 생산자는 데이터를 버퍼에 넣고, 소비자는 버퍼에서 데이터를 꺼낸다. 이러한 관계는 경쟁조건을 발생시킬 수 있어 이를 방지하기 위한 동기화가 필요하다.
int buffer; //큐의 사이즈가 1
int count = 0; //full인경우1, empty는 0
void put(int value){
assert(count == 0);
count = 1;
buffer = value;
}
int get(){
assert(count == 1);
count = 0;
return buffer;
}
put()과 get()에는 공유변수에 접근하는 임계영역을 갖고있다. 락만 추가하는 것으로 제대로 작동하지 않는다. 컨디션 변수가 더 필요하다. cond라는 컨디션 변수 하나와 그것과 연결된 mutex락을 사용한다.
if문
cond_t cond;
mutex_t mutex;
void *producer(void *arg){
int i;
for(int i=0;i<loops;i++){
pthread_mutex_lock(&mutex);
if(count == 1)
pthread_cond_wait(&cond, &mutex);
put(i);
pthread_mutex_unlock(&mutex);
}
}
void *consumer(void *arg){
int i;
for(int i=0;i<loops;i++){
pthread_mutex_lock(&mutex);
if(count == 0)
pthread_cond_wait(&cond, &mutex);
int tmp = get();
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
printf("%d\n",tmp);
}
}
위의 생산자 소비자에는 문제점이 있다. 바로 if문이다. 여러개의 쓰레드가 작동할때 스케쥴링에 의해 소비자들의 순서가 엉켜버려 예상과 다른 결과가 나올 수 있다. 이를 while문으로 해결해 볼수 있다.
While문
if문을 while로 바꾸고 다시 생각해보자. while에도 문제점이 있다. 컨디션 변수가 하나이기 때문에 시그널 대상이 명확하지 않아 다같이 잠들어 버릴 수 있다.
empty, fill
while을 해결하기 위해 두개의 컨디션 변수를 사용한다. 시스템의 상태가 변경되면 깨워야 하는 쓰레드에게만 시그널을 전달한다.
cond_t empty, fill;
mutex_t mutex;
void *producer(void *arg){
int i;
for(int i=0;i<loops;i++){
pthread_mutex_lock(&mutex);
while(count == 1)
pthread_cond_wait(&empty, &mutex);
put(i);
pthread_mutex_unlock(&mutex);
}
}
void *consumer(void *arg){
int i;
for(int i=0;i<loops;i++){
pthread_mutex_lock(&mutex);
wihle(count == 0)
pthread_cond_wait(&fill, &mutex);
int tmp = get();
pthread_cond_signal(&empty);
pthread_mutex_unlock(&mutex);
printf("%d\n",tmp);
}
}
최종적인 해법
버퍼 공간을 추가하여 대기상태에 들어가기 전 여러 값들이 생산되어 효율성을 높여보자.
int buffer[MAX];
int fill = 0;
int use = 0;
int count = 0;
void put(int value){
buffer[fill] = value;
fill = (fill + 1)%MAX;
count++;
}
int get(){
int tmp = buffer[use];
use = (use + 1)%MAX;
count--;
return tmp;
}
cond_t empty, fill;
mutex_t mutex;
void *producer(void *arg){
int i;
for(int i=0;i<loops;i++){
pthread_mutex_lock(&mutex);
while(count == MAX)
pthread_cond_wait(&empty, &mutex);
put(i);
pthread_mutex_unlock(&mutex);
}
}
void *consumer(void *arg){
int i;
for(int i=0;i<loops;i++){
pthread_mutex_lock(&mutex);
wihle(count == 0)
pthread_cond_wait(&fill, &mutex);
int tmp = get();
pthread_cond_signal(&empty);
pthread_mutex_unlock(&mutex);
printf("%d\n",tmp);
}
}