QUESTION
pthread_cond_wait는 signal 한 번과 정확히 짝을 이루는가?
condition variable는 event count를 저장하지 않는다. shared predicate가 참이 될 가능성을 waiter에게 알리는 수단이다. waiter는 mutex를 잡은 상태에서 predicate를 while로 검사하고 pthread_cond_wait가 mutex 해제와 sleep 준비를 원자적으로 수행하게 한다.
signal을 먼저 보냈다고 credit이 남는 것이 아니며 wakeup 뒤 다른 thread가 predicate를 소비할 수도 있다. 그래서 if가 아니라 while loop로 다시 확인한다.
STRUCTURE
구조 그림
condition variable는 event를 저장하지 않는다. queue predicate는 mutex 아래에 있고 waiter는 깨어난 뒤 mutex를 다시 얻어 predicate를 검사한다.
CALL PATH
호출 흐름
condition variable 자체 상태보다 mutex가 보호하는 predicate의 값이 기준이다. signal 시점과 작업 항목 수명은 queue 구조가 결정한다.
함수 이름을 외우기 위한 그림이 아니다. 반환값, 파일 디스크립터, 메모리 매핑, 대기 큐 가운데 무엇이 다음 단계로 전달되는지 확인한다.
SOURCE COORDINATES
Linux 6.18.37 LTS 소스 위치
glibc 함수에서 멈추지 않고 syscall 구현과 커널 객체가 만나는 파일까지 내려간다. 링크는 동일한 태그의 원본 파일을 가리킨다.
| 파일 | 함수·구조체 | 여기서 볼 것 |
|---|---|---|
| kernel/futex/waitwake.c | futex_wait(), futex_wake() | pthread condition/mutex contention의 sleep/wakeup 기반 |
| kernel/futex/core.c | get_futex_key() | private/shared futex word를 wait queue key로 변환 |
| kernel/futex/pi.c | futex_lock_pi() | priority inheritance mutex의 kernel 경로 |
COMPLETE PROGRAM
실행 예제 원본
아래 코드는 설명을 위해 중간 줄을 생략한 의사 코드가 아니다. 파일로 빌드해 실행할 수 있는 최소 예제다.
cc -std=c17 -Wall -Wextra -O2 -pthread cond_queue.c -o cond_queue01#include <pthread.h>
02#include <stdio.h>
03
04struct queue {
05 pthread_mutex_t lock;
06 pthread_cond_t ready;
07 int value;
08 int has_value;
09 int stopped;
10};
11
12static void *consumer(void *argument)
13{
14 struct queue *queue = argument;
15 pthread_mutex_lock(&queue->lock);
16 while (!queue->has_value && !queue->stopped)
17 pthread_cond_wait(&queue->ready, &queue->lock);
18 if (queue->has_value) {
19 printf("value=%d\n", queue->value);
20 queue->has_value = 0;
21 }
22 pthread_mutex_unlock(&queue->lock);
23 return NULL;
24}
25
26int main(void)
27{
28 struct queue queue = { PTHREAD_MUTEX_INITIALIZER, PTHREAD_COND_INITIALIZER, 0, 0, 0 };
29 pthread_t thread;
30 pthread_create(&thread, NULL, consumer, &queue);
31 pthread_mutex_lock(&queue.lock);
32 queue.value = 73;
33 queue.has_value = 1;
34 pthread_cond_signal(&queue.ready);
35 pthread_mutex_unlock(&queue.lock);
36 pthread_join(thread, NULL);
37 pthread_cond_destroy(&queue.ready);
38 pthread_mutex_destroy(&queue.lock);
39 return 0;
40}
CODE NOTES
코드 조각별 설명
while (!queue->has_valuespurious wakeup과 다른 consumer의 선점 뒤에도 predicate가 참인지 다시 검사한다. stop predicate도 같은 lock으로 보호한다.
pthread_cond_waitqueue lock을 해제하고 waiter 등록·sleep한 뒤, 반환 전에 mutex를 다시 획득한다.
queue.value = 73payload와 has_value를 같은 mutex critical section에서 갱신해 consumer가 둘을 일관되게 본다.
pthread_cond_signal적어도 한 waiter를 깨울 수 있지만 waiter가 없으면 저장되는 event가 없다. predicate가 이미 true이므로 나중 waiter는 sleep하지 않는다.
pthread_cond_destroyjoin으로 waiter가 모두 사라졌음을 보장한 뒤 synchronization object를 파괴한다.
DETAILS
세부 동작
lost wakeup은 unlock과 wait 사이 창에서 생긴다
predicate를 검사한 뒤 mutex를 직접 unlock하고 별도 sleep primitive를 호출하면 그 사이 producer가 signal을 보내 event를 잃을 수 있다. cond_wait가 이 두 동작을 protocol상 원자적으로 묶는다.
producer도 predicate를 같은 mutex 아래에서 바꿔 waiter의 검사 순서를 직렬화한다.
signal과 broadcast 선택
한 작업을 한 waiter가 소비하면 signal이 맞다. global configuration 변경이나 shutdown처럼 모든 waiter의 predicate가 달라질 수 있으면 broadcast가 필요하다.
broadcast 뒤 thundering herd 비용은 predicate와 queue sharding으로 줄인다.
timeout clock을 명시한다
pthread_cond_timedwait 기본 absolute timeout은 구현에 따라 CLOCK_REALTIME 특성을 가진다. wall clock 조정에 영향을 받지 않으려면 condition attribute에서 CLOCK_MONOTONIC을 설정한다.
timeout 반환 뒤에도 mutex를 보유하고 있으므로 predicate를 마지막으로 확인한 뒤 unlock한다.
OBJECTS
객체와 수명
| 대상 | 언제 생기고 없어지는가 | 확인할 값 |
|---|---|---|
predicate | queue state 변경 때 mutex 아래 갱신되고 consumer가 같은 lock 아래 읽는다 | has_value, stopped, count |
pthread_mutex_t | init부터 모든 사용자 join 뒤 destroy까지 존재한다 | owner, contention, robust/PI attr |
pthread_cond_t | waiter 등록부터 signal/broadcast까지 사용되며 waiter가 없어진 뒤 destroy한다 | clock, waiter sequence |
FAILURE PATH
실패 조건과 오해하기 쉬운 부분
| 겉으로 보이는 현상 | 실제 원인 후보 | 확인 방법 |
|---|---|---|
| 가끔 영원히 대기 | predicate 밖 signal 또는 unlock-wait race | 모든 state access의 mutex 소유 |
| 빈 queue를 소비 | if 사용/spurious wakeup | while predicate loop |
| shutdown hang | 일부 waiter에만 signal | stopped predicate + broadcast |
LAB
직접 확인
- consumer를 여러 개 만들고 signal과 broadcast에서 깨어나는 수와 실제 소비 수를 비교한다.
- while을 if로 바꾼 뒤 불필요한 broadcast를 반복해 predicate 없는 진행을 stress test하고 다시 원복한다.
- CLOCK_MONOTONIC condition attribute와 timedwait를 추가해 wall clock 조정과 독립된 timeout을 구현한다.
./cond_queuestrace -f -e trace=futex,clone,exit ./cond_queuePRIMARY REFERENCES