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
- 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의 지연·대역폭 비용 측정.
같은 시리즈에서 이어 읽기