-
조건 변수, 생산자/소비자 문제운영체제 2024. 4. 18. 21:51
락은 상호배제를 위해서
조건변수는 동기화를 위해서
void *child(void *arg) { printf("child\n"); return NULL; } int main(int argc, char *argv[]) { printf("parent: begin\n"); pthread_t c; pthread_create(&c, NULL, child, NULL); printf("parent: end\n"); return 0; }
부모 스레드와 자식 스레드 중 무엇이 먼저 끝날지 알 수 없음.
volatile int done = 0; void *child(void *arg) { printf("child\n"); done = 1; return NULL; } int main(int argc, char *argv[]) { printf("parent: begin\n"); pthread_t c; pthread_create(&c, NULL, child, NULL); while(done == 0) ; printf("parent: end\n"); return 0; }
1. 변수로 비지 웨이팅하는 방법
소프트웨어적으로 해결하는 방식, 자식이 하나면 작동은 함
문제: 스핀이라 낭비가 심하고(비효율적), 자식이 여러개면 부정확
int done = 0; pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t c = PTHREAD_COND_INITIALIZER; void thr_exit() { pthread_mutex_lock(&m); done = 1; pthread_cond_signal(&c); pthread_mutex_unlock(&m); } void *child(void *arg) { printf("child\n"); thr_exit(); return NULL; } void thr_join() { pthread_mutex_lock(&m); while( done == 0 ) pthread_cond_wait(&c, &m); pthread_mutex_unlock(&m); } int main(int argc, char *argv[]) { printf("parent: begin\n"); pthread_t p; pthread_create(&p, NULL, child, NULL); thr_join(); printf("parent: end\n"); return 0; }
조건 변수의 예시..
중요한 점: wait 앞 뒤로 락, 언락은 명시적으로 해줘야 하고, while 문으로 변수(done)를 확인해줘야 함
문제 상황 1) state variable (done) 안씀
void thr_exit() { pthread_mutex_lock(&m); pthread_cond_signal(&c); pthread_mutex_unlock(&m); } void thr_join() { pthread_mutex_lock(&m); pthread_cond_wait(&c, &m); pthread_mutex_unlock(&m); }
경우 1: 부모가 먼저 실행되는 경우
부모는 기다리고, 자식이 끝난 다음에 다시 깨어나서 실행된다.
경우 2: 자식이 먼저 수행되는 경우
자식이 시그널을 보냈는데 부모는 애초에 wait에 들어가지 않았고,
자식이 끝난 다음에야 wait에 들어가서 깨어나지 않게 되는 경우가 발생
문제 상황 2)
void thr_exit() { done = 1; pthread_cond_signal(&c); } void thr_join() { if( done == 0 ) pthread_cond_wait(&c); }
1. 일단 락이 없어서 상호배제가 안됨
2. while문이 아니면 waiting 중이던 스레드가 다시 깨어났을 때 done이 조건에 맞는지 재확인이 안됨
생산자, 소비자 문제
생산자는 만들어서 버퍼에 넣어주고 소비자는 버퍼(크기 1)에서 꺼내는 상황을 가정
int buffer; int count = 0; void put(int value) { assert(count == 0); count = 1; buffer = value; } int get() { assert(count == 1); count = 0; return buffer; } void *producer(void *arg) { int i; int loops = (int)arg; for(i = 0; i < loops; i++) { put(i); } } void *consumer(void *arg) { while(1) { int tmp = get(); printf("%d\n", tmp); } }
쉐어링을 고려하지 않은 코드
cond_t cond; mutex_t mutex; void *producer(void *arg) { int i; for( i = 0; i < loops; i++ ) { pthread_mutex_lock(&lock); if(count == 1) pthread_cond_wait(&cond, &mutex); put(i); pthread_cond_signal(&cond); pthread_mutex_unlock(&mutex); } } void *consumer(void *arg) { int i; for( 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 문을 지나 대기하고 있던 소비자가 다른 소비자에 의해 깨워지면
count에 대한 검사가 없이 실행됨 -> 버퍼가 비어있는데 가져가려고 할 가능성이 존재함
=> while 문으로 검사를 해줘야 함
cond_t cond; mutex_t mutex; void *producer(void *arg) { int i; for( i = 0; i < loops; i++ ) { pthread_mutex_lock(&mutex); while( count == 1 ) pthread_cond_wait(&cond, &mutex); put(i); pthread_cond_signal(&cond); pthread_mutex_unlock(&mutex); } } void *consumer(void *arg) { int i; for( i = 0; i < loops; i++ ) { pthread_mutex_lock(&mutex); while( count == 0 ) pthread_cond_wait(&cond, &mutex); int tmp = get(); pthread_cond_signal(&cond); pthread_mutex_unlock(&mutex); printf("%d\n", tmp); } }
이래도 문제가 있음
생산자가 하나, 소비자가 여럿이라 할 때,
생산자가 슬립 중인데 소비자가 소비하고 다른 소비자를 깨우면 생산자에 시그널을 보낼 수가 없어진다.
cond_t empty, fill; mutex_t mutex; void *producer(void *arg) { int i; for( i = 0; i < loops; i++ ) { pthread_mutex_lock(&mutex); while( count == 1 ) pthread_cond_wait(&empty, &mutex); put(i); pthread_cond_signal(&fill); pthread_mutex_unlock(&mutex); } } void *consumer(void *arg) { int i; for( i = 0; i < loops; i++ ) { pthread_mutex_lock(&mutex); while( count == 0 ) pthread_cond_wait(&fill, &mutex); int tmp = get(); pthread_cond_signal(&empty); pthread_mutex_unlock(&mutex); printf("%d\n", tmp); } }
조건 변수를 2개 두고, while 문에서 wait하도록 하여 해결된다.
int buffer[MAX]; int fill_ptr = 0; int use_ptr = 0; int count = 0; void put(int value) { buffer[fill_ptr] = value; fill_ptr = (fill_ptr + 1) % MAX; count++; } int get() { int tmp = buffer[use_ptr]; use_ptr = (use_ptr + 1) % MAX; count--; return tmp; }
cond_t empty, fill; mutex_t mutex; void *producer(void *arg) { int i; for( i = 0; i < loops; i++ ) { pthread_mutex_lock(&mutex); while( count == max ) pthread_cond_wait(&empty, &mutex); put(i); pthread_cond_signal(&fill); pthread_mutex_unlock(&mutex); } } void *consumer(void *arg) { int i; for( i = 0; i < loops; i++ ) { pthread_mutex_lock(&mutex); while( count == 0 ) pthread_cond_wait(&fill, &mutex); int tmp = get(); pthread_cond_signal(&empty); pthread_mutex_unlock(&mutex); printf("%d\n", tmp); } }
버퍼의 크기가 늘어난 일반적인 버전
pthread_cond_broadcast()
pthread_cond_signal()은 기다리고 있던 스레드 하나만 깨움
pthread_cond_broadcast()는 조건 변수 해당하는 스레드 다 깨움
왜 이것이 필요한가
메모리가 충분하지 않은 상황(꽉 찼다고 가정)
T1: 100바이트 필요
T2: 50바이트 필요
free로 인해 발생한 시그널 -> 여유 공간이 50바이트 생겼다고 가정
pthread_cond_signal()을 쓴다고 할 때,
경우1: T2가 깨는 경우 -> T2 실행
경우2: T1이 깨는 경우
-> 여유공간이 부족해서 T1 슬립, T2는 깬 적이 없으니 실행 불가
=> pthread_cond_broadcast()로 일단 다 깨우면 T2가 실행이 가능
'운영체제' 카테고리의 다른 글
데드락 (0) 2024.05.06 Semaphore (세마포) (0) 2024.04.18 Lock-based Concurrent Data Structure (0) 2024.04.18 Lock의 구현 (0) 2024.04.11 멀티 스레드 / 경쟁 상태, 공유자원, 임계영역, 상호배제 (0) 2024.04.10