Reader-Writer Lock 성능 — Reader/Writer Priority·RCU·Seqlock
#한 줄 요약
“RW-Lock은 read 동시 N, write 1을 허용하며 read-mostly 워크로드에 적합합니다.”
#어떤 문제를 푸는가
라우팅 테이블, 설정 캐시, sensor 데이터처럼 읽기가 압도적인 자료구조에 일반 mutex를 쓰면 모든 read가 직렬화됩니다. Read는 서로 충돌하지 않으므로 동시에 진행해도 안전한데, mutex는 그것을 막아 버립니다.
RW-lock은 read를 동시 허용하고 write만 exclusive로 처리합니다. 99% read인 워크로드에서는 throughput이 mutex보다 한 자릿수 빨라집니다. 하지만 50
가까워지면 RW-lock의 내부 state 관리 비용이 mutex와 비슷해지거나 더 느려집니다.이 글에서는 RW-lock의 기본 구조와 reader/writer priority 정책, 그리고 더 극단적인 read-mostly 워크로드를 위한 RCU와 seqlock까지 다룹니다.
#기본 구조
typedef struct { atomic_int readers; atomic_int writers_waiting; spinlock_t state_lock; cond_var_t no_readers, no_writers;} rwlock_t;
void read_lock(rwlock_t *rw) { spin_lock(&rw->state_lock); while (rw->writers_waiting > 0) cond_wait(&rw->no_writers, &rw->state_lock); rw->readers++; spin_unlock(&rw->state_lock);}
void read_unlock(rwlock_t *rw) { spin_lock(&rw->state_lock); if (--rw->readers == 0) cond_signal(&rw->no_readers); spin_unlock(&rw->state_lock);}
void write_lock(rwlock_t *rw) { spin_lock(&rw->state_lock); rw->writers_waiting++; while (rw->readers > 0) cond_wait(&rw->no_readers, &rw->state_lock);}매 read 진입과 종료마다 state_lock과 atomic counter 갱신이 발생합니다. 이 비용이 RW-lock의 본질적 overhead이며, mutex 한 번의 atomic CAS보다 비쌉니다.
#Reader Preference
Reader가 도착하면 다른 reader가 있을 때 즉시 함께 진입합니다. Writer가 대기 중이라도 우선되지 않습니다.
문제는 writer starvation입니다. 연속해서 reader가 도착하면 writer는 영원히 대기할 수 있습니다. Web cache나 DNS 같은 read-heavy 시스템에서는 write가 드물어 큰 문제가 안 되지만, sensor data처럼 write가 주기적으로 도착하는 시스템에서는 deadline을 놓칠 수 있습니다.
Linux의 기본 rwlock_t가 unfair reader-preferring입니다.
#Writer Preference
Writer가 대기 중일 때는 새 reader도 대기합니다. Writer가 처리된 뒤에야 reader가 진행됩니다.
이 정책은 writer starvation을 방지하지만, read가 압도적이면 reader starvation이 발생할 수 있습니다. POSIX PTHREAD_RWLOCK_PREFER_WRITER_NP로 선택할 수 있으며, database나 sensor write가 중요한 시스템에 적합합니다.
#Fair RW-Lock (FIFO)
도착 순서대로 queue에 넣되, 같은 batch의 reader는 동시 처리합니다.
Queue: R1 R2 R3 W1 R4 R5 W2Batch 1: R1 R2 R3 동시 진행Batch 2: W1 exclusiveBatch 3: R4 R5 동시 진행Batch 4: W2 exclusiveStarvation이 양쪽 모두에서 없습니다. 일부 platform의 pthread_rwlock_init 기본 동작입니다.
#성능 비교 — Mutex vs RW-Lock
Workload: 99% read, 1% write Mutex: 모든 read 직렬화 → throughput 저하 RW-Lock: read 동시 처리 → throughput 향상
Workload: 50% read, 50% write RW-Lock: state_lock contention 큼 → mutex와 비슷 또는 느림
Workload: 90% read, 10% write RW-Lock: 약 1.5-3x mutex 대비RW-lock의 효과는 read 비율이 결정합니다. 90% 이상이 read일 때만 의미가 있으며, 70-80% 정도라면 측정 없이 가정하지 말아야 합니다.
#RCU — Lock 없는 Read
RCU(Read-Copy-Update)는 reader가 lock을 전혀 잡지 않고 데이터를 읽는 mechanism입니다.
/* Reader */rcu_read_lock(); /* preempt disable, 거의 0 비용 */struct data *d = rcu_dereference(global_data);use(d);rcu_read_unlock();
/* Writer */struct data *new_d = create();struct data *old = rcu_assign_pointer(global_data, new_d);synchronize_rcu();free(old);Writer는 새 copy를 만들고 atomic pointer swap으로 교체합니다. Old version은 모든 기존 reader가 완료될 때까지 살려 두었다가 free합니다. 이 대기 구간을 grace period라고 합니다.
Reader는 atomic read 하나만 실행하므로 throughput이 수백만 read/sec를 넘습니다. Linux kernel의 routing table, dentry cache 등이 RCU 위에 구현되어 있습니다.
#RCU의 사용 조건
- Read가 압도적이고 write는 1% 이하입니다
- Pointer-based 자료구조여야 atomic swap이 가능합니다
- Old version을 잠시 유지해도 시스템 동작에 문제가 없어야 합니다
Hash table, linked list, radix tree처럼 atomic pointer로 노드를 갱신할 수 있는 구조에 적합합니다.
#Seqlock — Read는 Retry, Write는 직렬
typedef struct { atomic_int seq; /* 짝수 = idle, 홀수 = writing */ spinlock_t writelock;} seqlock_t;
void seqlock_write(seqlock_t *sl) { spin_lock(&sl->writelock); atomic_store(&sl->seq, atomic_load(&sl->seq) + 1); /* modify */ atomic_store(&sl->seq, atomic_load(&sl->seq) + 1); spin_unlock(&sl->writelock);}
uint32_t seqlock_read(seqlock_t *sl, T *out) { uint32_t seq; do { seq = atomic_load(&sl->seq); if (seq & 1) continue; /* writer 진행 중 */ *out = data; } while (atomic_load(&sl->seq) != seq); return seq;}Reader는 lock을 잡지 않고 sequence number 두 번 비교로 일관성을 확인합니다. Writer가 끼어들었으면 retry합니다. Writer는 spinlock으로 직렬화합니다.
Linux의 jiffies(시스템 시간) 읽기에 쓰이며, GPS 좌표나 sensor 측정값처럼 작은 데이터의 read-mostly 시나리오에 적합합니다.
#RCU·Seqlock·RW-Lock 비교
| 항목 | RW-Lock | RCU | Seqlock |
|---|---|---|---|
| Read cost | 비쌈 (atomic + lock) | 거의 0 | retry 가능 |
| Write cost | 보통 | 비쌈 (grace period) | 직렬 |
| Read 동시성 | 여러 reader | 무제한 | 무제한 |
| Memory | 1 copy | 2 copy 잠시 | 1 copy |
| 사용처 | 균형 워크로드 | read 압도적 | 작은 데이터 read-mostly |
선택 기준은 read 비율, write 빈도, 그리고 자료구조의 형태입니다. Pointer-based이고 write가 매우 드물면 RCU, 작은 value type이면 seqlock, 그 외에는 RW-lock이나 mutex입니다.
#자동차 — Double Buffer
자동차 ECU에서는 lock 자체의 결정성을 보장하기 어렵기 때문에 double buffer가 자주 쓰입니다.
struct { atomic_int active; sensor_data buf[2];} sensor;
void writer(void) { int next = !sensor.active; sensor.buf[next] = read_sensor(); sensor.active = next;}
void reader(void) { int idx = sensor.active; use(sensor.buf[idx]);}Writer는 inactive buffer를 채우고 atomic swap으로 교체합니다. Reader는 active index를 읽어 그 buffer를 사용합니다. Race가 없으며 lock도 없습니다. 결정성이 critical한 ASIL 시스템에서 표준 패턴입니다.
#자주 보는 함정과 안티패턴
⚠️ Read 적은 워크로드에 RW-Lock
/* read 50%, write 50% */rwlock_read_lock(&rw); /* state_lock contention 큼 */이런 비율에서는 mutex가 더 빠를 수 있습니다. 측정 없이 RW-lock을 쓰지 말아야 합니다.
⚠️ Read 중 write 시도
read_lock(&rw);modify_data(); /* undefined */read_unlock(&rw);Read lock은 데이터 수정을 허용하지 않습니다. 명시적인 upgrade API가 있는 구현에서만 가능합니다.
⚠️ RCU read 안에서 free 호출
rcu_read_lock();struct data *d = rcu_dereference(global);free(d); /* 다른 reader도 d 사용 중 */rcu_read_unlock();Free는 반드시 synchronize_rcu() 또는 call_rcu() 콜백 안에서 합니다.
⚠️ Seqlock에 pointer 데이터
Seqlock은 value 데이터에만 안전합니다. Pointer를 저장하면 retry 사이에 원본이 변경되어 잘못된 메모리를 dereference할 수 있습니다.
#측정 — 실측 결과
Cortex-A72 4-core, 4 thread에서 read
비율을 바꿔 가며 측정한 throughput(ops/sec)입니다. Mutex RW-Lock RCU Seqlock99:1 read 1.2 M 8.5 M 180 M 120 M90:10 1.1 M 4.2 M 22 M 15 M70:30 0.9 M 1.5 M 8 M 3 M50:50 0.7 M 0.6 M 4 M 1 MRCU와 seqlock은 read-heavy에서 압도적입니다. RW-lock의 효과는 90
이상에서만 분명하며, 50 오히려 mutex보다 느려집니다.#정리
- RW-lock은 read 동시 N, write exclusive로 read-mostly 워크로드에 적합합니다.
- Reader preference는 writer starvation, writer preference는 reader starvation 위험이 있습니다.
- RCU는 read가 거의 무료이며 write가 비쌉니다. Pointer 기반 자료구조에 적합합니다.
- Seqlock은 read가 retry loop이며 작은 value 데이터에 적합합니다.
- 자동차 시스템에서는 double buffer가 lock보다 결정성에서 우수합니다.
- Read 비율 90% 이상에서만 RW-lock의 효과가 분명합니다.
다음 편은 Lock-Free 기초 — CAS와 ABA 문제를 살펴봅니다.
#관련 항목
Embedded Performance Engineering · 36 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 처리량 회복
관련 글
실전 사례 — 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의 지연·대역폭 비용 측정.