Mutex 성능 분석 — Futex·Adaptive·Priority Inheritance
#한 줄 요약
“Mutex는 block 가능한 lock이며, hold time이 길수록 spinlock보다 유리합니다.”
#어떤 문제를 푸는가
Spinlock은 짧은 critical section에 적합하지만 hold time이 µs 단위를 넘기 시작하면 CPU 낭비가 시스템 전체 throughput을 깎습니다. Mutex는 lock을 얻지 못한 thread를 sleep 시키고 holder가 unlock할 때 wake up 시키므로, 다른 thread가 그 CPU를 쓸 수 있습니다.
문제는 sleep과 wake가 공짜가 아니라는 점입니다. 한 번의 context switch가 1-5 µs, wake up까지 합치면 10 µs 가까이 들기도 합니다. 이 비용을 어떻게 줄이느냐가 modern mutex 구현의 핵심입니다.
이 글에서는 Linux futex의 2-stage 구조, adaptive mutex가 spin과 block을 어떻게 결합하는지, 그리고 priority inheritance가 real-time 시스템에서 어떤 overhead를 만드는지 살펴봅니다.
#Mutex 비용 분해
| 경로 | 항목 | 비용 |
|---|---|---|
| Uncontended (waiter 없음) | atomic CAS | 5-20 cycle |
| 함수 호출 overhead | 10 cycle | |
| 총합 | ~30 cycle ≈ 10-100 ns | |
| Contended (waiter 있음) | waiter park (syscall) | 1-3 µs |
| holder unlock + wake | 1-3 µs | |
| context switch | 1-3 µs | |
| 총합 | ~10 µs |
Uncontended path는 lock-free 수준으로 빠릅니다. 비용은 contention에서만 발생합니다. 따라서 같은 mutex라도 contention rate에 따라 100배 이상 비용이 달라집니다.
#Linux Futex — Fast Userspace Mutex
Linux는 mutex를 futex 위에 구현합니다. Uncontended 시에는 userspace atomic만으로 끝나고, contended 시에만 kernel이 개입합니다.
/* Pseudo-code */void mutex_lock(int *m) { if (atomic_cmpxchg(m, 0, 1) == 0) return; /* fast path */
do { if (*m == 2 || atomic_cmpxchg(m, 1, 2) != 0) futex_wait(m, 2); } while (atomic_cmpxchg(m, 0, 2) != 0);}
void mutex_unlock(int *m) { if (atomic_fetch_sub(m, 1) != 1) { *m = 0; futex_wake(m, 1); }}상태는 세 가지입니다. 0은 free, 1은 locked without waiter, 2는 locked with waiter입니다. Waiter가 없을 때는 syscall이 한 번도 발생하지 않으므로 uncontended path가 매우 빠릅니다.
#Adaptive Mutex — 짧으면 Spin
void adaptive_mutex_lock(mutex_t *m) { int spins = 100; while (spins--) { if (try_lock(m)) return; cpu_relax(); } futex_wait(...);}Linux PTHREAD_MUTEX_ADAPTIVE_NP와 Solaris adaptive mutex가 이 구조입니다. 짧은 contention은 spin으로 해결해 block 비용을 회피하고, 긴 hold time일 때만 sleep으로 전환합니다.
Spin 횟수는 휴리스틱이며 보통 수십에서 수백 cycle 정도입니다. 실측해서 워크로드에 맞게 조정할 수 있습니다.
#Priority Inheritance
Real-time 시스템에서 low priority task가 mutex를 보유한 상태로 medium priority task에 의해 preempt되면, high priority task가 그 mutex를 기다리는 동안 medium이 계속 실행됩니다. 이것이 priority inversion이며, 1997년 Mars Pathfinder를 멈춘 원인으로 유명합니다.
Priority inheritance는 holder의 priority를 임시로 waiter의 priority로 끌어올려 이 문제를 해결합니다.
/* Take with PI */if (lock_held_by_lower_priority_task) { boost_holder_priority(my_priority); if (holder waits on another lock) { propagate further; }}
/* Release */restore_priority();Overhead는 다음과 같습니다.
- Chain inheritance가 발생하면 수십 µs까지 늘어날 수 있습니다
- Wait list 재정렬에 O(log N) 또는 O(N)이 필요합니다
- Holder TCB 접근으로 cache miss가 추가됩니다
ASIL-D 자동차나 항공 시스템에서는 chain depth가 유한임을 보장해야 하며, 보통 3 이하로 제한합니다.
#FreeRTOS Mutex 측정
TickType_t start = xTaskGetTickCount();xSemaphoreTake(mtx, portMAX_DELAY);TickType_t wait = xTaskGetTickCount() - start;Tick 단위 측정은 해상도가 낮으므로, 정확한 분석에는 SEGGER SystemView나 Percepio Tracealyzer를 사용합니다.
Mutex_DB: total_acquired: 12345 total_block_time: 1234 ms max_block_time: 45 µs blocked_count: 2345max_block_time이 RTOS에서는 평균보다 중요합니다. Worst case가 deadline 안에 들어와야 schedulable합니다.
#Robust Mutex
pthread_mutexattr_setrobust(&attr, PTHREAD_MUTEX_ROBUST);Mutex를 보유한 thread가 죽으면 다음 take 시 EOWNERDEAD를 반환해 복구할 기회를 줍니다.
int err = pthread_mutex_lock(&mtx);if (err == EOWNERDEAD) { /* State 복구 */ pthread_mutex_consistent(&mtx);}Safety-critical 시스템이나 multi-process 환경에서 한 process가 crash해도 다른 process가 정상 진행할 수 있게 해 줍니다.
#Recursive Mutex의 비용
recursive_mutex_take(&m); /* count++ — atomic 한 번 */recursive_mutex_take(&m); /* count++ */recursive_mutex_give(&m); /* count-- */recursive_mutex_give(&m); /* count-- → 0 → 실제 unlock */매 take와 give마다 atomic 연산과 owner thread ID 비교가 추가됩니다. Non-recursive보다 살짝 비싸므로 가능하면 일반 mutex를 쓰고, 함수 구조를 재설계해 재귀를 피하는 것이 좋습니다.
#Mutex vs Semaphore — 어느 것을 쓸까
| 항목 | Mutex | Semaphore |
|---|---|---|
| Owner 개념 | 있음 | 없음 |
| Recursive | 가능 | 불가능 |
| Priority Inheritance | 지원 | 없음 |
| ISR에서 give | 금지 | 가능 |
| 의미 | mutual exclusion | counting, signaling |
Mutex는 자원 보호용입니다. 같은 thread가 take와 give를 모두 해야 하며 owner가 명확합니다. Binary semaphore는 ISR이 task에 signal을 보내는 용도로 쓰며, give와 take를 다른 context가 해도 됩니다.
#자동차 ECU — Mutex 패턴
/* Brake task — 1 ms loop, mutex는 timeout으로 짧게 */if (xSemaphoreTake(brake_data_mtx, pdMS_TO_TICKS(1)) == pdTRUE) { read_data(); xSemaphoreGive(brake_data_mtx);} else { use_last_known();}Real-time loop에서는 mutex가 영원히 block되지 않도록 반드시 finite timeout을 명시합니다. Lock 획득 실패 시 fault path가 명확히 정의되어 있어야 하며, 위 예시처럼 last known good value를 쓰는 fallback이 일반적입니다.
#Apple os_unfair_lock
#include <os/lock.h>os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;os_unfair_lock_lock(&lock);critical();os_unfair_lock_unlock(&lock);iOS와 macOS에서 권장하는 lock입니다. 매우 가벼우며 짧은 spin 후 thread를 park 합니다. Priority donation도 자동으로 처리합니다. 기존 OSSpinLock은 priority inversion 위험으로 deprecated 되었습니다.
#자주 보는 함정과 안티패턴
⚠️ ISR 안에서 mutex
ISR: xSemaphoreTake(mtx); /* block 가능 → fault */ISR은 block될 수 없으므로 mutex 대신 binary semaphore의 FromISR API를 사용해야 합니다.
⚠️ 무한 timeout
xSemaphoreTake(mtx, portMAX_DELAY); /* deadlock 시 영원 대기 */Production 코드에서는 항상 finite timeout과 fault 처리 경로를 둡니다.
⚠️ Nested mutex의 순서 불일치
xSemaphoreTake(a);xSemaphoreTake(b); /* 다른 task가 a 대기 + b 보유 시 deadlock */Lock ordering 규칙을 정해 모든 코드가 같은 순서로 잡도록 해야 합니다.
⚠️ Priority inversion 무시한 RT design
Low priority task가 mutex를 잡고 있을 때 medium이 CPU를 점유하면 high priority task가 deadline을 놓칩니다. PI mutex를 쓰고 WCET 분석을 같이 해야 합니다.
#측정 — 실측 결과
Cortex-A53 4-core, glibc futex mutex의 결과입니다.
| 항목 | Latency | 비고 |
|---|---|---|
| Uncontended lock | 30 ns | atomic CAS만 |
| Uncontended unlock | 20 ns | atomic store |
| Contended lock | 8 µs | futex_wait + switch |
| Contended unlock | 3 µs | futex_wake |
| Adaptive spin success | 200 ns | spin 단계에서 성공 |
| PI inherit (depth 1) | 1 µs | priority boost |
| PI inherit (depth 3) | 4 µs | chain propagation |
Uncontended path와 contended path가 100배 이상 차이가 납니다. Contention이 빈번한 lock은 lock-free나 striping으로 분산하는 것이 필요합니다.
#정리
- Mutex는 block 가능한 lock이며 hold time이 길 때 spinlock보다 유리합니다.
- Linux futex는 uncontended path에서 syscall이 0이므로 매우 빠릅니다.
- Adaptive mutex는 짧은 spin 후 block으로 두 방식의 장점을 결합합니다.
- Priority inheritance는 chain overhead가 있으며 자동차 시스템에서는 depth 제한이 필요합니다.
- Mutex는 자원 보호용, semaphore는 signaling용으로 구분합니다.
- Real-time loop에서는 항상 finite timeout과 fault path를 둡니다.
다음 편은 Reader-Writer Lock — read-mostly 워크로드의 최적화 패턴입니다.
#관련 항목
Embedded Performance Engineering · 35 of 57
- 1 Embedded Performance Engineering — 임베디드 성능 엔지니어링 시리즈 소개
- 2 임베디드 성능 분석 방법론 — Measure → Analyze → Optimize 사이클
- 3 성능 지표 정의 — Latency·Throughput·Utilization 분석
- 4 성능 측정의 기본 — Wall-Clock·CPU Cycle·Instruction Count
- 5 성능 데이터 통계적 분석 — Percentile·Histogram·평균의 함정
- 6 실시간 성능 분석 — WCET·Jitter·Deadline Miss 측정
- 7 임베디드 벤치마킹 기초 — 재현성·Warmup·노이즈 제거
- 8 성능 모델링 — Amdahl·Gustafson·Roofline Model 적용
- 9 프로파일링 기법 개요 — Sampling vs Instrumentation·PGO·LTO
- 10 CPU 파이프라인 분석 — 5-stage·Cortex-M·Cortex-A 비교
- 11 Pipeline Stall 분석 — Data·Structural·Control Hazard·Forwarding
- 12 Branch Prediction 분석 — Static·2-bit·BTB·BHT·Mispredict 비용
- 13 Speculative Execution 분석 — OoO·Reorder Buffer·Register Renaming
- 14 CPU Cache 기초 — L1·L2·L3·Set Associative·Replacement Policy
- 15 Cache Miss 3C Model 분석 — Compulsory·Capacity·Conflict
- 16 Cache Line 최적화 — Alignment·Prefetch·False Sharing 처리
- 17 메모리 대역폭 분석 — STREAM·Roofline·Bus Saturation 측정
- 18 SIMD·NEON 활용 — 128-bit Vector·Auto-Vectorization·SVE/SVE2
- 19 PMU·HPM 하드웨어 카운터 분석 — 정밀 성능 진단
- 20 임베디드 Bus Architecture — AHB·AXI·CHI 진화와 5-Channel
- 21 Bus Contention 진단 — Arbitration·QoS·Starvation 측정
- 22 DMA 성능 최적화 — Burst·Scatter-Gather·Chain·Cache 일관성
- 23 DMA vs CPU Copy 성능 비교 — Break-even·Setup Overhead 실측
- 24 Interrupt Latency 분석 — 진입·종료·Tail-Chaining·Late Arrival
- 25 Interrupt Storm 처리 — NAPI·Rate-Limit·Polling 전환
- 26 MMIO 접근 성능 — Cache Policy·Write-Combining·Volatile·Barrier
- 27 Peripheral Clock 분석 — PLL·Divider·Gating·DVFS
- 28 Power vs Performance 트레이드오프 — DVFS·Race-to-Idle·Big.LITTLE
- 29 Thermal Throttling 분석 — Junction Temp·Trip Point·냉각
- 30 CXL Interconnect 분석 — AI 시대 메모리 대역폭 확장
- 31 Concurrency 기초 — Concurrency vs Parallelism·Race·Memory Model
- 32 False Sharing 진단 — Cache Line Ping-Pong·Padding·측정
- 33 Lock Contention 분석 — Wait·Hold·Convoy·측정 기법
- 34 Spinlock 성능 분석 — Spin-Wait vs Context Switch·Ticket·MCS
- 35 Mutex 성능 분석 — Futex·Adaptive·Priority Inheritance
- 36 Reader-Writer Lock 성능 — Reader/Writer Priority·RCU·Seqlock
- 37 Lock-Free 자료구조 성능 — CAS·ABA·Hazard Pointer·Epoch Reclamation
- 38 Memory Ordering 분석 — Acquire·Release·Seq-Cst·ARM Relaxed Model
- 39 Cache Coherency 프로토콜 — MESI·MOESI·Snoop·Directory
- 40 SMP 성능 분석 — Per-Core·Affinity·Load Balance·Scalability
- 41 Linux perf 기초 — stat·record·report 활용
- 42 Linux perf 고급 — Raw Event·Tracepoint·perf script
- 43 ftrace 활용 — function·function_graph·latency tracer
- 44 eBPF·bpftrace 동적 트레이싱 — 커널 무수정 관측
- 45 Flamegraph 분석 — On-CPU·Off-CPU·Differential
- 46 ARM DS·Lauterbach 분석 — Hardware Trace 전문 도구
- 47 Bare-metal 프로파일링 — GPIO·DWT·SysTick·ITM 활용
- 48 NVIDIA Nsight Systems — GPU·NPU 포함 시스템 분석
- 49 모던 프로파일러 비교 — Tracy·Hotspot·uftrace·Coz
- 50 연속 프로파일링 — Parca·Pixie·Pyroscope·Tetragon
- 51 실전 사례 — ISR Latency 100µs Deadline Miss 추적
- 52 실전 사례 — Matrix Multiply가 예상의 10배 느린 이유
- 53 실전 사례 — 8-core가 4-core를 넘으면 throughput이 떨어지는 이유
- 54 실전 사례 — 카메라 1080p 60fps가 30fps로 떨어지는 이유
- 55 CXL.mem 지연·대역폭 실측 — Direct·Switch·Pooled 토폴로지 비교
- 56 CXL 성능 프로파일링 도구 — cxl-cli·DAMON·perf-mem 활용
- 57 실전 사례 — CXL.mem 추가로 LLM inference KV cache 처리량 회복
관련 글
실전 사례 — CXL.mem 추가로 LLM inference KV cache 처리량 회복
70B 모델 KV cache가 HBM 한계를 넘어 throughput이 무너졌을 때, CXL.mem 256 GB pool 추가로 회복한 실전 케이스.
같은 시리즈에서 이어 읽기
CXL 성능 프로파일링 도구 — cxl-cli·DAMON·perf-mem 활용
CXL.mem 환경 성능 도구 — cxl-cli 토폴로지·DAMON page activity·perf-mem로 보는 CXL 트래픽·numastat 통계.
같은 시리즈에서 이어 읽기
CXL.mem 지연·대역폭 실측 — Direct·Switch·Pooled 토폴로지 비교
CXL.mem 토폴로지별 실측 — Direct attach·Single switch·Multi-host pool의 지연·대역폭 비용 측정.
같은 시리즈에서 이어 읽기
이 글을 참조하는 글 (4)
- Reader-Writer Lock 성능 — Reader/Writer Priority·RCU·Seqlock — Embedded Performance Engineering
- Spinlock 성능 분석 — Spin-Wait vs Context Switch·Ticket·MCS — Embedded Performance Engineering
- Lock Contention 분석 — Wait·Hold·Convoy·측정 기법 — Embedded Performance Engineering
- Spinlock vs Mutex 결정 가이드 — Context Switch·Hold Time — Modern Embedded Recipes