Lock Contention 분석 — Wait·Hold·Convoy·측정 기법
#한 줄 요약
“Contention은 같은 lock을 두 thread 이상이 동시에 쟁탈하는 상황이며, wait time이 throughput을 결정합니다.”
#어떤 문제를 푸는가
멀티스레드 코드에서 lock 자체는 cycle 단위로 가볍습니다. 문제는 contention입니다. 한 thread가 lock을 쥐고 있는 동안 다른 thread들이 대기하면, 그 대기 시간이 곧 throughput 손실로 직결됩니다. CPU를 8개 늘려도 모두 같은 lock 앞에서 줄을 서면 1-core 성능과 다르지 않게 됩니다.
Contention을 줄이려면 먼저 측정이 필요합니다. “느린 것 같다”가 아니라 “어느 lock에서 평균 몇 µs 대기하고 있다”를 알아야 합니다. 그래야 lock granularity 조정, RW-lock 도입, lock-free 전환 같은 다음 결정을 할 수 있습니다.
이 글에서는 contention의 핵심 지표를 정의하고, Linux와 RTOS에서 측정하는 도구를 소개하며, lock convoy와 striping 같은 실전 패턴을 정리합니다.
#핵심 지표
| 지표 | 의미 |
|---|---|
| Hold time | lock을 보유한 시간 |
| Wait time | lock을 얻기까지 대기한 시간 |
| Acquisition rate | 초당 lock 횟수 |
| Contention ratio | wait / (wait + hold) |
이상적: contention < 5%주의: 5-20%심각: > 20% — 재설계가 필요합니다특히 contention ratio가 20%를 넘으면, lock 자체보다 큰 구조적 문제가 있을 가능성이 높습니다. 한 코어가 lock을 푼 순간 모든 다른 코어가 깨어나는 thundering herd 같은 패턴도 같은 증상을 만들어 냅니다.
#perf lock — Linux 측정
sudo perf lock record ./progsudo perf lock report
# 출력 예# Name acquired wait_total(s) wait_avg(s)# spinlock_a 12345 0.234 0.000019# mutex_b 400 1.520 0.003800wait_total이 큰 lock이 bottleneck입니다. 위 예시에서는 mutex_b가 400번만 acquire되지만 누적 대기 시간이 1.52초로 전체의 대부분을 차지하므로, 이 lock을 먼저 분석해야 합니다.
acquired × wait_avg로 정렬하면 시스템 전체의 누적 손실을 한눈에 볼 수 있습니다.
#ftrace lock_events
perf lock이 통계라면 ftrace는 시계열입니다.
echo lock_acquire > /sys/kernel/debug/tracing/set_eventecho lock_release >> /sys/kernel/debug/tracing/set_eventcat /sys/kernel/debug/tracing/trace_pipe각 lock event마다 timestamp가 찍히므로 특정 시점에 어떤 thread가 어느 lock을 잡고 있었는지 재구성할 수 있습니다. Lock convoy나 priority inversion처럼 패턴이 중요한 문제에 효과적입니다.
#FreeRTOS — Lock 통계
configUSE_TRACE_FACILITY=1 옵션을 켜고 Tracealyzer나 SystemView를 연결하면 per-task, per-semaphore 통계를 받을 수 있습니다.
Per-task: - blocked on which semaphore - total blocked time - max wait time
Per-semaphore: - total give count - max queue waitersRTOS에서는 max wait time이 평균보다 중요합니다. Real-time deadline은 worst case로 결정되기 때문입니다.
#Amdahl과 Gunther — Lock의 영향
병렬화의 한계를 보여 주는 Amdahl 식은 lock contention 분석에도 그대로 적용됩니다.
여기서 는 serial fraction(lock으로 보호되는 비율), 은 CPU 수입니다.
Serial fraction이 10%만 되어도 CPU 64개를 줘도 8.8배만 빨라집니다. Gunther의 Universal Scalability Law는 contention과 coherency overhead를 추가로 모델링하므로, 실측 데이터와 더 잘 맞습니다.
#Lock Convoy
Lock이 풀린 직후 깨어난 task들이 같은 순서로 다시 줄을 서는 현상을 lock convoy라고 합니다. 같은 priority의 task들이 fair queueing 정책 아래서 자주 발생합니다.
회피 방법은 다음과 같습니다.
- Lock hold time을 짧게 유지합니다
- 일부 lock에서는 unfair 정책을 허용해 가장 빠른 task가 먼저 잡도록 합니다
- Lock을 더 잘게 분리해 동시 진입 가능성을 늘립니다
Unfair lock은 fairness를 희생하는 대신 cache locality와 throughput을 얻습니다. 같은 thread가 lock을 연속으로 잡으면 cache hit이 그대로 유지되기 때문입니다.
#Lock Granularity
/* Coarse-grained — 하나의 lock으로 전체 보호 */mutex_t global_lock;
mutex_take(&global_lock);do_lots();mutex_give(&global_lock);
/* Fine-grained — 여러 lock으로 분리 */mutex_t lock_a, lock_b, lock_c;
mutex_take(&lock_a);work_a();mutex_give(&lock_a);
mutex_take(&lock_b);work_b();mutex_give(&lock_b);Fine-grained는 contention을 분산하지만 deadlock 위험이 올라갑니다. 두 lock을 잡는 순서가 thread마다 다르면 즉시 데드락이 발생합니다. Lock ordering 규칙을 문서화하고 정적 분석으로 검증하는 것이 안전합니다.
#Striped Lock
Hash table이나 connection pool처럼 키로 접근하는 자료구조에서는 striped lock이 유용합니다.
mutex_t locks[16];
void access(int key) { int idx = key % 16; mutex_take(&locks[idx]); /* access table[key] */ mutex_give(&locks[idx]);}같은 키는 같은 lock으로 직렬화되지만, 다른 키는 16배까지 동시 처리됩니다. Java의 ConcurrentHashMap이 이 방식을 씁니다.
#RW-Lock으로 read 분산
rwlock_t rw;
void reader(void) { rwlock_read_lock(&rw); read_data(); rwlock_read_unlock(&rw);}
void writer(void) { rwlock_write_lock(&rw); write_data(); rwlock_write_unlock(&rw);}읽기가 압도적인 워크로드에서 reader 동시성을 활용할 수 있습니다. 단, write가 30%를 넘으면 RW-lock의 내부 state 관리 비용이 mutex보다 비싸지므로 효과가 줄어듭니다. 자세한 내용은 4-06 편에서 다룹니다.
#Hold Time을 짧게
/* 회피 — lock 안에서 expensive 작업 */mutex_take(&mtx);expensive_compute(); /* 100 ms */update_var();mutex_give(&mtx);
/* Good — 짧은 critical section */expensive_compute();mutex_take(&mtx);update_var();mutex_give(&mtx);가장 효과 큰 최적화는 lock granularity 조정도 striping도 아닌, hold time을 줄이는 것입니다. Critical section을 좁히는 것이 lock 자체를 바꾸는 것보다 항상 우선합니다.
#Latency-Sensitive 코드에서 try-lock
/* ISR 또는 RT task */if (mutex_try_take(&mtx, 0)) { update(); mutex_give(&mtx);} else { log_skipped();}Real-time task가 block되면 deadline을 놓치므로, try-lock으로 우회 경로를 만듭니다. 놓친 update는 다음 cycle에서 처리하거나 deferred queue로 넘깁니다.
#자동차 — Lock Profile 예
Brake ECU loop 1 ms: - measurement: 200 µs - control: 300 µs - actuator: 200 µs - logging: 300 µs ← lock 잡으면 riskASIL-D 시스템에서는 critical section의 worst case가 보장되어야 합니다. Logging처럼 비결정적 길이의 작업은 lock-free queue로 deferred 처리해 control loop를 막지 않도록 설계합니다.
#자주 보는 함정과 안티패턴
⚠️ Lock 안에서 expensive 작업
mutex_take(&db_lock);http_get(url); /* 수 초 가능, 다른 task 모두 정지 */mutex_give(&db_lock);데이터를 미리 fetch한 뒤 lock은 짧게 잡아야 합니다.
⚠️ 측정 없이 추정
“Lock contention이 의심된다”고 추정만 하고 perf lock이나 trace로 확인하지 않으면, 잘못된 lock을 최적화하기 쉽습니다.
⚠️ 모든 read에 lock
mutex_take(&cfg_lock);int v = cfg.value;mutex_give(&cfg_lock);32-bit aligned 정수 read는 atomic합니다. atomic_load나 RCU로 대체하면 contention을 0으로 줄일 수 있습니다.
⚠️ ISR과 task에 다른 lock
ISR: spinlock_take(&sl);Task: mutex_take(&mtx); /* 다른 lock — 보호 안 됨 */ISR과 task 사이는 event group이나 queue로 동기화해야 하며, 같은 mutex를 공유하면 ISR에서 block될 수 없으므로 의미가 없습니다.
#측정 — 실측 결과
Cortex-A72 4-core에서 같은 mutex를 100 thread가 경쟁할 때 측정한 결과입니다.
Hold time Wait avg Wait p99 Contention ratio 100 ns 50 ns 200 ns 33% 1 µs 700 ns 5 µs 41% 10 µs 30 µs 150 µs 75% 100 µs 400 µs 2 ms 80%Hold time이 10 µs를 넘기 시작하면 contention ratio가 70%를 넘어 throughput이 거의 1-core 수준이 됩니다. 측정 데이터로 hold time 1 µs를 목표선으로 잡는 근거가 됩니다.
#정리
- Lock contention의 핵심 지표는 hold time, wait time, contention ratio입니다.
- Linux에서는
perf lock과 ftrace, RTOS에서는 Tracealyzer로 측정합니다. - Amdahl 식으로 serial fraction 10%만 되어도 64-core scaling이 9배 한계입니다.
- Lock convoy는 fair queueing의 부작용이며 unfair 정책이 throughput에는 유리합니다.
- Granularity 조정, striping, RW-lock, lock-free로 contention을 분산할 수 있습니다.
- 가장 효과 큰 최적화는 hold time 자체를 줄이는 것입니다.
다음 편은 Spinlock 성능 — busy-wait가 언제 유리한지 분석합니다.
#관련 항목
Embedded Performance Engineering · 33 of 57
- 1Embedded 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
- 10CPU 파이프라인 분석 — 5-stage·Cortex-M·Cortex-A 비교
- 11Pipeline Stall 분석 — Data·Structural·Control Hazard·Forwarding
- 12Branch Prediction 분석 — Static·2-bit·BTB·BHT·Mispredict 비용
- 13Speculative Execution 분석 — OoO·Reorder Buffer·Register Renaming
- 14CPU Cache 기초 — L1·L2·L3·Set Associative·Replacement Policy
- 15Cache Miss 3C Model 분석 — Compulsory·Capacity·Conflict
- 16Cache Line 최적화 — Alignment·Prefetch·False Sharing 처리
- 17메모리 대역폭 분석 — STREAM·Roofline·Bus Saturation 측정
- 18SIMD·NEON 활용 — 128-bit Vector·Auto-Vectorization·SVE/SVE2
- 19PMU·HPM 하드웨어 카운터 분석 — 정밀 성능 진단
- 20임베디드 Bus Architecture — AHB·AXI·CHI 진화와 5-Channel
- 21Bus Contention 진단 — Arbitration·QoS·Starvation 측정
- 22DMA 성능 최적화 — Burst·Scatter-Gather·Chain·Cache 일관성
- 23DMA vs CPU Copy 성능 비교 — Break-even·Setup Overhead 실측
- 24Interrupt Latency 분석 — 진입·종료·Tail-Chaining·Late Arrival
- 25Interrupt Storm 처리 — NAPI·Rate-Limit·Polling 전환
- 26MMIO 접근 성능 — Cache Policy·Write-Combining·Volatile·Barrier
- 27Peripheral Clock 분석 — PLL·Divider·Gating·DVFS
- 28Power vs Performance 트레이드오프 — DVFS·Race-to-Idle·Big.LITTLE
- 29Thermal Throttling 분석 — Junction Temp·Trip Point·냉각
- 30CXL Interconnect 분석 — AI 시대 메모리 대역폭 확장
- 31Concurrency 기초 — Concurrency vs Parallelism·Race·Memory Model
- 32False Sharing 진단 — Cache Line Ping-Pong·Padding·측정
- 33Lock Contention 분석 — Wait·Hold·Convoy·측정 기법
- 34Spinlock 성능 분석 — Spin-Wait vs Context Switch·Ticket·MCS
- 35Mutex 성능 분석 — Futex·Adaptive·Priority Inheritance
- 36Reader-Writer Lock 성능 — Reader/Writer Priority·RCU·Seqlock
- 37Lock-Free 자료구조 성능 — CAS·ABA·Hazard Pointer·Epoch Reclamation
- 38Memory Ordering 분석 — Acquire·Release·Seq-Cst·ARM Relaxed Model
- 39Cache Coherency 프로토콜 — MESI·MOESI·Snoop·Directory
- 40SMP 성능 분석 — Per-Core·Affinity·Load Balance·Scalability
- 41Linux perf 기초 — stat·record·report 활용
- 42Linux perf 고급 — Raw Event·Tracepoint·perf script
- 43ftrace 활용 — function·function_graph·latency tracer
- 44eBPF·bpftrace 동적 트레이싱 — 커널 무수정 관측
- 45Flamegraph 분석 — On-CPU·Off-CPU·Differential
- 46ARM DS·Lauterbach 분석 — Hardware Trace 전문 도구
- 47Bare-metal 프로파일링 — GPIO·DWT·SysTick·ITM 활용
- 48NVIDIA 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로 떨어지는 이유
- 55CXL.mem 지연·대역폭 실측 — Direct·Switch·Pooled 토폴로지 비교
- 56CXL 성능 프로파일링 도구 — cxl-cli·DAMON·perf-mem 활용
- 57실전 사례 — CXL.mem 추가로 LLM inference KV cache 처리량 회복
관련 글
실전 사례 — 8-core가 4-core를 넘으면 throughput이 떨어지는 이유
8-core 서버에서 thread를 늘릴수록 throughput이 오히려 감소. 단일 global mutex가 cache invalidation 폭주를 일으킨 사례.
Bus Contention 진단 — Arbitration·QoS·Starvation 측정
Round-robin·priority·QoS arbitration. Master 다수 시 starvation. AXI QoS·BUSY counter.
실전 사례 — CXL.mem 추가로 LLM inference KV cache 처리량 회복
70B 모델 KV cache가 HBM 한계를 넘어 throughput이 무너졌을 때, CXL.mem 256 GB pool 추가로 회복한 실전 케이스.
이 글을 참조하는 글 (4)
- 실전 사례 — 8-core가 4-core를 넘으면 throughput이 떨어지는 이유— Embedded Performance Engineering
- Mutex 성능 분석 — Futex·Adaptive·Priority Inheritance— Embedded Performance Engineering
- Spinlock 성능 분석 — Spin-Wait vs Context Switch·Ticket·MCS— Embedded Performance Engineering
- False Sharing 진단 — Cache Line Ping-Pong·Padding·측정— Embedded Performance Engineering