QUESTION
atomic store를 썼다는 사실만으로 다른 데이터도 안전하게 보이는가?
atomic operation은 해당 atomic object의 찢어진 read/write와 data race를 막지만 주변 non-atomic data의 순서를 자동으로 정하지 않는다. producer가 payload를 쓴 뒤 release store로 ready를 publish하고 consumer가 acquire load로 ready를 관찰해야 이전 payload write가 보인다.
futex는 userspace atomic word 값이 예상과 같을 때만 kernel wait queue에서 잠들게 한다. uncontended lock은 syscall 없이 끝나고, contention 때만 wait/wake로 들어간다.
STRUCTURE
구조 그림
Producer CPU
- payload stores
- release store ready=1
- futex wake
Shared cache line
- payload[4]
- atomic ready
- modification order
Consumer CPU
- acquire load
- payload reads
- CAS/spin fast path
Kernel futex bucket
- key=(mm,address)
- waiter list
- timeout/signal/wake
정확성은 atomic state가 담당하고 kernel은 contention 때 잠드는 장소만 제공한다. FUTEX_WAIT는 잠들기 직전 값을 다시 검사한다.
CALL PATH
호출 흐름
memory ordering과 thread scheduling을 분리한다. acquire/release는 값의 가시성 순서를 만들고 futex는 기다리는 동안 CPU를 양보한다.
함수 이름을 외우기 위한 그림이 아니다. 반환값, 파일 디스크립터, 메모리 매핑, 대기 큐 가운데 무엇이 다음 단계로 전달되는지 확인한다.
SOURCE COORDINATES
Linux 6.18.37 LTS 소스 위치
glibc 함수에서 멈추지 않고 syscall 구현과 커널 객체가 만나는 파일까지 내려간다. 링크는 동일한 태그의 원본 파일을 가리킨다.
| 파일 | 함수·구조체 | 여기서 볼 것 |
|---|---|---|
| kernel/futex/waitwake.c | futex_wait_setup(), futex_wait_queue() | expected value 재검사와 lost wakeup 방지 |
| kernel/futex/waitwake.c | futex_wake() | 같은 futex key waiter를 runnable로 전환 |
| kernel/futex/core.c | get_futex_key() | private virtual address와 shared backing을 key로 변환 |
COMPLETE PROGRAM
실행 예제 원본
아래 코드는 설명을 위해 중간 줄을 생략한 의사 코드가 아니다. 파일로 빌드해 실행할 수 있는 최소 예제다.
cc -std=c17 -Wall -Wextra -O2 -pthread atomic_publish.c -o atomic_publish01#include <pthread.h>
02#include <stdatomic.h>
03#include <stdio.h>
04
05struct message {
06 int payload[4];
07 atomic_int ready;
08};
09
10static void *producer(void *argument)
11{
12 struct message *message = argument;
13 message->payload[0] = 10;
14 message->payload[1] = 20;
15 message->payload[2] = 30;
16 message->payload[3] = 40;
17 atomic_store_explicit(&message->ready, 1, memory_order_release);
18 return NULL;
19}
20
21int main(void)
22{
23 struct message message = { .payload = {0}, .ready = ATOMIC_VAR_INIT(0) };
24 pthread_t thread;
25 pthread_create(&thread, NULL, producer, &message);
26 while (atomic_load_explicit(&message.ready, memory_order_acquire) == 0)
27 ;
28 printf("%d %d %d %d\n", message.payload[0], message.payload[1],
29 message.payload[2], message.payload[3]);
30 pthread_join(thread, NULL);
31 return 0;
32}
CODE NOTES
코드 조각별 설명
int payload[4]payload는 non-atomic이지만 producer만 publish 전에 쓰고 consumer는 acquire 뒤 읽어 happens-before가 성립하므로 data race가 없다.
atomic_int readypublish flag만 atomic object로 둔다. lock-free 여부는 implementation과 type/alignment에 따라 확인한다.
memory_order_release앞선 payload store가 ready=1 이후로 재배치되지 않게 하고 acquire reader에 공개한다.
memory_order_acquire1을 읽은 뒤 payload load가 ready load 앞으로 이동하지 않게 하며 같은 value를 publish한 release와 synchronize한다.
== 0)예제는 busy wait라 짧은 대기 외에는 비효율적이다. atomic_wait/C++ 또는 futex/condition variable로 sleep을 결합한다.
DETAILS
세부 동작
relaxed도 atomic이지만 publish는 아니다
memory_order_relaxed load/store는 ready 자체의 modification order는 유지하지만 payload 접근과 순서를 만들지 않는다. counter 통계처럼 다른 data와 불변 조건이 없을 때 적합하다.
x86에서 우연히 동작하는 테스트를 C memory model의 portable 보장으로 착각하지 않는다.
compare-exchange에는 성공·실패 order가 있다
CAS 성공은 lock ownership publish/acquire를 만들 수 있지만 실패 path는 write를 하지 않으므로 release order를 사용할 수 없다. expected 값이 갱신되는 semantics도 loop 설계에 포함한다.
ABA, object lifetime, reclamation은 atomic pointer CAS 하나로 해결되지 않는다.
futex wait는 조건을 한 번 더 확인한다
userspace가 lock이 잠긴 것을 본 뒤 syscall에 들어가기 전 unlock/wake가 발생할 수 있다. FUTEX_WAIT는 kernel에서 현재 word가 expected와 같은지 다시 검사하고 다르면 EAGAIN으로 잠들지 않는다.
정확성은 userspace atomic state machine이 담당하고 futex는 blocking 최적화다.
OBJECTS
객체와 수명
| 대상 | 언제 생기고 없어지는가 | 확인할 값 |
|---|---|---|
atomic object | 공유 구조체 수명 동안 존재하고 모든 concurrent 접근이 memory model을 지켜야 한다 | modification order, alignment |
payload | release 전 producer가 쓰고 matching acquire 뒤 consumer가 읽는다 | ownership, happens-before |
futex waiter | contention 시 syscall에서 잠들고 wake/timeout/signal에서 제거된다 | key, expected value, priority |
FAILURE PATH
실패 조건과 오해하기 쉬운 부분
| 겉으로 보이는 현상 | 실제 원인 후보 | 확인 방법 |
|---|---|---|
| 다른 architecture에서 값이 깨짐 | relaxed publish 또는 data race | TSan과 memory-order review |
| CPU 100% | 무제한 spin loop | wait duration과 futex/condvar 전환 |
| missed wakeup | state 변경과 futex expected protocol 불일치 | CAS/state diagram과 syscall 반환 |
LAB
직접 확인
- release/acquire를 relaxed로 바꾼 variant를 ARM 장비와 ThreadSanitizer 관점에서 분석하되 잘못된 코드를 배포하지 않는다.
- spin 횟수 뒤 condition variable로 전환하는 adaptive wait를 구현해 latency와 CPU 사용을 비교한다.
- raw futex WAIT/WAKE wrapper를 만들고 expected value가 이미 바뀌었을 때 EAGAIN을 확인한다.
./atomic_publishstrace -f -e trace=futex,clone ./atomic_publishPRIMARY REFERENCES