[병행성 #3] 컨디션 변수 본문
쓰레드가 계속 진행하기 전에 어떤 조건이 참인지를 검사해야 하는 경우가 많이 있다.
예를 들어 부모 쓰레드가 작을 시작하기 전에 자식 쓰레드가 작업을 끝냈는지를 검사하기를 원할 수 있다
(이 과정을 보통 join()이라 부른다). 그런 대기문은 어떻게 구현해야 할까?
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 ); // Spin
printf("parent: end\\n");
return 0;
}
- 위 처럼 회전 기반의 방법으로 자식을 기다릴 수 있다.
- 하지만 조건이 참이 될 때까지 회전하며 기다리는 것은 비효율적일 뿐만 아니라 CPU 사이클을 낭비한다.
컨디션 변수
조건이 참이 될 때까지 기다리기 위해 **컨디션 변수(conditional variable)**를 활용할 수 있다.
- 컨디션 변수는 일종의 큐 자료 구조
- 조건이 참이 되기를 기다리며 쓰레드가 대기하는 큐
- 다른 쓰레드가 상태를 변경시켰을 때, 대기 중이던 쓰레드를 조건에 따라 시그널을 보내어 깨운다.
wait()/signal()
pthread_cond_wait(pthread_cond_t *c, pthread_mutex_t *m);
pthread_cond_signal(pthread_cond_t *c);
- wait() 은 락을 해제하고 스스로를 잠재움
- 반드시 wait() 에서 리턴하기 전에 락을 재 획득해야한다. → 깨어나더라도 락을 획득하지 못하면 다시 sleep으로 돌아감
- signal()은 잠자고 있던 쓰레드를 깨우는 것
int done = 0;
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t c = PTHREAD_COND_INITALIZER;
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("praent: begin\\n");
pthread_t p;
Pthread_create(&p, NULL, child, NULL);
thr_join();
printf("parent: end\\n");
return 0;
}
시나리오 1
- 부모 쓰레드가 자식 쓰레드를 생성하고 계속 thr_join()을 호출하고 자식 쓰레드가 끝나기를 기다린다.
- 부모 쓰레드가 락을 획득하고, 자식이 끝났는지 자식이 끝났는지 검사한 후에 자식이 끝나지 않으면 wait()을 호출하여 스스로 잠재운다.
- 자식 쓰레드가 추후에 실행되어 child라는 메시지를 출력하고 thr_exit()을 호출하여 부모 쓰레드를 깨운다.
- done을 1로 설정하고, 부모 쓰레드에 시그널을 보내어 꺠어나도록 한다.
- 부모 쓰레드가 락을 획득한 채로 리턴하여 부모 쓰레드가 실행될 것이며 락을 해제하고 parent: end를 출력한다.
시나리오 2
- 자식 쓰레드가 생성되고 즉시 실행되고 done을 1로 변경하고 자고있는 쓰레드를 꺠우기 위해 시그널을 보낸다.
- 자고있는 쓰레드가 없기 때문에 단순히 리턴한다.
- 부모 쓰레드가 실행하고 thr_join()을 호출하고 done 변수가 1이므로 바로 리턴한다.
질문
1. 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);
}
- 위 처럼 구현할 경우 시나리오 2번에서는 done이 없으므로 부모쓰레드는 바로 wait()을 호출하고 아무도 꺠우지 않을 것이다.
2. 시그널을 주거나 대기할 때 만약 lock을 획득이 필요 없다면?
void thr_exit() {
done = 1;
Pthread_cond_signal(&c);
}
void thr_join() {
if (done == 0)
Pthread_cond_wait(&c, &m);
}
- 만약 부모 쓰레드가 thr_join을 호출하고, done 변수의 값이 0인 것을 확인한 후 잠자려고 한다.
- wait을 호출하기 직전에 인터럽트에 걸린 후, 자식 쓰레드가 실행 되었다면 ?
- 현재 자고있는 쓰레드가 없으므로 부모 쓰레드는 계속 잠잔다.
생산자/소비자 문제
- Dijkstra가 제시한 생산자/소비자(producer/consumer) 문제이다.
- 여러 생산자 쓰레드와 소비자 쓰레드가 있다고 가정
- 생산자는 데이터를 만들어 버퍼에 넣는다.
- 소비자는 버퍼에서 데이터를 꺼내어 사용한다.
- 멀티 쓰레드 웹 서버의 경우 HTTP 요청 작업을 큐에 넣고, 소비즈 쓰레드는 이 큐에서 요청을 꺼내어 처리한다.
- 버퍼는 공유 자원이다. 경쟁 조건의 발생을 방지하기 위해 동기화가 필요하다.
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) {
int i ;
while (1) {
int tep = get();
printf("%d\\n", tmp);
}
}
- 매우 간단한 형태의 생산/소비 루틴이다. (버퍼에 1개만 저장할 수 있다고 설정)
- put()은 버퍼가 비어있다 가정하고 값을 넣은 후 count를 1로 설정하여 가득 찼다고 표시한다.
- get()은 버퍼가 찼는지 확인하고, 값을 꺼낸 후 count를 0으로 설정하여 비었다고 설정한다.
- 이 작업은 생산자 T1 , 소비자 T2 쓰레드에 의해 수행될 것이다.
- T1은 loop 횟수 만큼 put()을 호출, T2는 무한히 get()을 호출
- 이 코드가 제대로 동작하지 않을 것이다 → count와 buffer에 경쟁 조건이 발생하기 떄문이다.
불완전한 해답
- 컨디션 변수와 mutex락으로 해결이 가능할까 ?
cond_t cond;
mutex_t mutex;
void *producer(void *arg) {
int i;
for (i = 0; i < loops; i++) {
Pthread_mutex_lock(&mutex); // p1
if (count == 1) // p2
Pthread_cond_wait(&cond, &mutex); // p3
put(i); // p4
Pthread_cond_signal(&cond); // p5
Pthread_mutex_unlock(&mutex); // p6
}
}
void *consumer(void *arg) {
int i ;
for (i = 0; i < loops; i++) {
Pthread_mutex_lock(&mutex); // c1
if (count == 0) // c2
Pthread_cond_wait(&cond, &mutex); // c3
int tmp = get(); // c4
Pthread_cond_signal(&cond); // c5
Pthread_mutex_unlock(&mutex); // c6
printf("%d\\n", tmp);
}
}
- 생산자는 버퍼가 빌때까지 기다린다. (p1~p3)
- 소비자도 버퍼가 차기를 기다린다. (c1-c3)
- 생산자와 소비자가 각 하나씩인 경우에는 코드가 동작한다.
- Tc1 Tc2 라는 두개의 소비자가 있고 Tp 라는 생산자가 하나가 있다고 하면?
문제점 1 (If 문)

- Tc1이 먼저 실행한다. 락을 획득하고 버퍼를 가져올 수 있는지 검사하고, 비어있음을 확인하고 확인한 후 대기하며 락을 해제한다.
- Tp가 실행된다. 락을 획득하고 버퍼가 비어있으므로 버퍼를 채우고 시그널을 보낸다.
- 대기중인 첫째 소비자는 꺠어나 준비 큐로 이동한다. Tc1은 이제 실행할 수 있는 상태지만 아직 실행하지 않는다.
- Tp를 실행하고, 버퍼가 차 있으므로 대기 상태로 존재한다.
- 여기서 Tc2가 끼어들어서 실행된다면 ? 다음 Tc1은 .. 데이터가 없어요
- 문제 원인은 Tc1이 꺠어나서 실행되기 까지 사이에 버퍼의 상태가 변경이 되었다.
- 깨어난 쓰레드는 실제 실행되는 시점에도 그 상태가 유지된다는 보장이 없다. (Mesa semantic)
이 문제는 if 문 대신 while문을 사용하면 해결할 수 있다.
소비자 Tc1 이 깨어나서 즉시 count 상태를 재확인한다.
만약 이 시점에 버퍼가 비어 있다면, 소비자는 대기 상태로 돌아간다
cond_t cond;
mutex_t mutex;
void *producer(void *arg) {
int i;
for (i = 0; i < loops; i++) {
Pthread_mutex_lock(&mutex); // p1
while (count == 1) // p2
Pthread_cond_wait(&cond, &mutex); // p3
put(i); // p4
Pthread_cond_signal(&cond); // p5
Pthread_mutex_unlock(&mutex); // p6
}
}
void *consumer(void *arg) {
int i ;
for (i = 0; i < loops; i++) {
Pthread_mutex_lock(&mutex); // c1
while (count == 0) // c2
Pthread_cond_wait(&cond, &mutex); // c3
int tmp = get(); // c4
Pthread_cond_signal(&cond); // c5
Pthread_mutex_unlock(&mutex); // c6
printf("%d\n", tmp);
}
}
문제점 2 (컨디션 변수가 하나)
- Tc1과 Tc2가 먼저 실행 한 후 소비한 후에 둘다 대기 상태에 있다면 ?
- Tc1는 이제 어떤 쓰레드를 깨울 것인가?
- 만약 Tc2를 깨운다면 버퍼가 비어있는 것을 확인한 후에 다시 대기상태로 들어간다. 그러면 세 개의 쓰레드가 모두 대기상태이다..
- signal 을 보내야 하지만, 대상이 명확해야한다. (소비자 ↔ 생산자)
단일 버퍼 생산자/소비자 해법
- 두개의 컨디션 변수를 사용해야만 쓰레드에게 시그널을 제대로 전달한다.
- empty 일때 fill에 대한 시그널을 보내고, fill 에 대해서 대기하고 empty에 대해서 시그널을 발생시켜
- 소비자가 다른 소비자를 꺠울 수 없도록, 생산자가 생산자를 꺠울 수 없도록 한다.
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);
}
}
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;
}
출처 : 운영체제 : 아주 쉬운 세 가지 이야기
'Fundamentals > OS' 카테고리의 다른 글
| [병행성 #4] 세마포어 (0) | 2025.07.07 |
|---|---|
| [병행성 #2] 락(Lock) (5) | 2025.07.05 |
| [병행성 #1] 개요, 쓰레드 vs 프로세스 (0) | 2025.07.04 |
| [메모리 가상화 #6] 페이지 스왑, 페이지 폴트 (1) | 2025.07.02 |
| [메모리 가상화 #5] 페이징: TLB (1) | 2025.07.01 |
Comments