← System Programming DUJINLABS.COM

Thread / Synchronization · Linux userspace / kernel ABI

mutex와 condition variable

predicate를 mutex로 보호하고 cond_wait의 unlock-and-sleep, spurious wakeup, lost wakeup을 bounded queue 코드로 설명합니다.

Series
28 / 37
Build
cc -std=c17 -Wall -Wextra -O2 -pthread cond_queue.c -o cond_queue
Run
./cond_queue
Kernel
Linux 6.18.37 LTS

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로 다시 확인한다.

구조 그림

그림 1. predicate, mutex owner, condition waiter의 배치
shared queue
value=73has_value=1stopped=0
mutex
owner: producerhandoffowner: consumer
condition waiters
consumer Aconsumer Btimeout waiter
wake 후
mutex reacquirewhile predicate한 consumer가 value 소비

condition variable는 event를 저장하지 않는다. queue predicate는 mutex 아래에 있고 waiter는 깨어난 뒤 mutex를 다시 얻어 predicate를 검사한다.

호출 흐름

그림 2. 사용자 코드에서 관찰 가능한 결과까지
lock mutex predicate 보호
while false 조건 재검사
cond_wait unlock + sleep
producer update mutex 안에서 변경
signal/relock 깨어나 재검사

condition variable 자체 상태보다 mutex가 보호하는 predicate의 값이 기준이다. signal 시점과 작업 항목 수명은 queue 구조가 결정한다.

그림 3. 커널 내부에서 지나가는 주요 지점
pthread mutex user atomic fast path
futex_wait contended sleep
producer store predicate 변경
futex_wake waiter 깨움
mutex acquire memory visibility

함수 이름을 외우기 위한 그림이 아니다. 반환값, 파일 디스크립터, 메모리 매핑, 대기 큐 가운데 무엇이 다음 단계로 전달되는지 확인한다.

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 경로

실행 예제 원본

아래 코드는 설명을 위해 중간 줄을 생략한 의사 코드가 아니다. 파일로 빌드해 실행할 수 있는 최소 예제다.

빌드cc -std=c17 -Wall -Wextra -O2 -pthread cond_queue.c -o cond_queue
01#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}

코드 조각별 설명

실제 코드 16행while (!queue->has_value

spurious wakeup과 다른 consumer의 선점 뒤에도 predicate가 참인지 다시 검사한다. stop predicate도 같은 lock으로 보호한다.

실제 코드 17행pthread_cond_wait

queue lock을 해제하고 waiter 등록·sleep한 뒤, 반환 전에 mutex를 다시 획득한다.

실제 코드 32행queue.value = 73

payload와 has_value를 같은 mutex critical section에서 갱신해 consumer가 둘을 일관되게 본다.

실제 코드 34행pthread_cond_signal

적어도 한 waiter를 깨울 수 있지만 waiter가 없으면 저장되는 event가 없다. predicate가 이미 true이므로 나중 waiter는 sleep하지 않는다.

실제 코드 37행pthread_cond_destroy

join으로 waiter가 모두 사라졌음을 보장한 뒤 synchronization object를 파괴한다.

세부 동작

01

lost wakeup은 unlock과 wait 사이 창에서 생긴다

predicate를 검사한 뒤 mutex를 직접 unlock하고 별도 sleep primitive를 호출하면 그 사이 producer가 signal을 보내 event를 잃을 수 있다. cond_wait가 이 두 동작을 protocol상 원자적으로 묶는다.

producer도 predicate를 같은 mutex 아래에서 바꿔 waiter의 검사 순서를 직렬화한다.

02

signal과 broadcast 선택

한 작업을 한 waiter가 소비하면 signal이 맞다. global configuration 변경이나 shutdown처럼 모든 waiter의 predicate가 달라질 수 있으면 broadcast가 필요하다.

broadcast 뒤 thundering herd 비용은 predicate와 queue sharding으로 줄인다.

03

timeout clock을 명시한다

pthread_cond_timedwait 기본 absolute timeout은 구현에 따라 CLOCK_REALTIME 특성을 가진다. wall clock 조정에 영향을 받지 않으려면 condition attribute에서 CLOCK_MONOTONIC을 설정한다.

timeout 반환 뒤에도 mutex를 보유하고 있으므로 predicate를 마지막으로 확인한 뒤 unlock한다.

객체와 수명

대상언제 생기고 없어지는가확인할 값
predicatequeue state 변경 때 mutex 아래 갱신되고 consumer가 같은 lock 아래 읽는다has_value, stopped, count
pthread_mutex_tinit부터 모든 사용자 join 뒤 destroy까지 존재한다owner, contention, robust/PI attr
pthread_cond_twaiter 등록부터 signal/broadcast까지 사용되며 waiter가 없어진 뒤 destroy한다clock, waiter sequence

실패 조건과 오해하기 쉬운 부분

겉으로 보이는 현상실제 원인 후보확인 방법
가끔 영원히 대기predicate 밖 signal 또는 unlock-wait race모든 state access의 mutex 소유
빈 queue를 소비if 사용/spurious wakeupwhile predicate loop
shutdown hang일부 waiter에만 signalstopped predicate + broadcast

직접 확인

  1. consumer를 여러 개 만들고 signal과 broadcast에서 깨어나는 수와 실제 소비 수를 비교한다.
  2. while을 if로 바꾼 뒤 불필요한 broadcast를 반복해 predicate 없는 진행을 stress test하고 다시 원복한다.
  3. CLOCK_MONOTONIC condition attribute와 timedwait를 추가해 wall clock 조정과 독립된 timeout을 구현한다.
실행./cond_queue
추적strace -f -e trace=futex,clone,exit ./cond_queue

원문