본문으로 건너뛰기
Embedded Performance Engineering · 35/57

Reader-Writer Lock 성능 — Reader/Writer Priority·RCU·Seqlock

· Hawk · 6분 읽기

#한 줄 요약

“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 W2
Batch 1: R1 R2 R3 동시 진행
Batch 2: W1 exclusive
Batch 3: R4 R5 동시 진행
Batch 4: W2 exclusive

Starvation이 양쪽 모두에서 없습니다. 일부 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-LockRCUSeqlock
Read cost비쌈 (atomic + lock)거의 0retry 가능
Write cost보통비쌈 (grace period)직렬
Read 동시성여러 reader무제한무제한
Memory1 copy2 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 Seqlock
99:1 read 1.2 M 8.5 M 180 M 120 M
90:10 1.1 M 4.2 M 22 M 15 M
70:30 0.9 M 1.5 M 8 M 3 M
50:50 0.7 M 0.6 M 4 M 1 M

RCU와 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. 1Embedded Performance Engineering — 임베디드 성능 엔지니어링 시리즈 소개
  2. 2임베디드 성능 분석 방법론 — Measure → Analyze → Optimize 사이클
  3. 3성능 지표 정의 — Latency·Throughput·Utilization 분석
  4. 4성능 측정의 기본 — Wall-Clock·CPU Cycle·Instruction Count
  5. 5성능 데이터 통계적 분석 — Percentile·Histogram·평균의 함정
  6. 6실시간 성능 분석 — WCET·Jitter·Deadline Miss 측정
  7. 7임베디드 벤치마킹 기초 — 재현성·Warmup·노이즈 제거
  8. 8성능 모델링 — Amdahl·Gustafson·Roofline Model 적용
  9. 9프로파일링 기법 개요 — Sampling vs Instrumentation·PGO·LTO
  10. 10CPU 파이프라인 분석 — 5-stage·Cortex-M·Cortex-A 비교
  11. 11Pipeline Stall 분석 — Data·Structural·Control Hazard·Forwarding
  12. 12Branch Prediction 분석 — Static·2-bit·BTB·BHT·Mispredict 비용
  13. 13Speculative Execution 분석 — OoO·Reorder Buffer·Register Renaming
  14. 14CPU Cache 기초 — L1·L2·L3·Set Associative·Replacement Policy
  15. 15Cache Miss 3C Model 분석 — Compulsory·Capacity·Conflict
  16. 16Cache Line 최적화 — Alignment·Prefetch·False Sharing 처리
  17. 17메모리 대역폭 분석 — STREAM·Roofline·Bus Saturation 측정
  18. 18SIMD·NEON 활용 — 128-bit Vector·Auto-Vectorization·SVE/SVE2
  19. 19PMU·HPM 하드웨어 카운터 분석 — 정밀 성능 진단
  20. 20임베디드 Bus Architecture — AHB·AXI·CHI 진화와 5-Channel
  21. 21Bus Contention 진단 — Arbitration·QoS·Starvation 측정
  22. 22DMA 성능 최적화 — Burst·Scatter-Gather·Chain·Cache 일관성
  23. 23DMA vs CPU Copy 성능 비교 — Break-even·Setup Overhead 실측
  24. 24Interrupt Latency 분석 — 진입·종료·Tail-Chaining·Late Arrival
  25. 25Interrupt Storm 처리 — NAPI·Rate-Limit·Polling 전환
  26. 26MMIO 접근 성능 — Cache Policy·Write-Combining·Volatile·Barrier
  27. 27Peripheral Clock 분석 — PLL·Divider·Gating·DVFS
  28. 28Power vs Performance 트레이드오프 — DVFS·Race-to-Idle·Big.LITTLE
  29. 29Thermal Throttling 분석 — Junction Temp·Trip Point·냉각
  30. 30CXL Interconnect 분석 — AI 시대 메모리 대역폭 확장
  31. 31Concurrency 기초 — Concurrency vs Parallelism·Race·Memory Model
  32. 32False Sharing 진단 — Cache Line Ping-Pong·Padding·측정
  33. 33Lock Contention 분석 — Wait·Hold·Convoy·측정 기법
  34. 34Spinlock 성능 분석 — Spin-Wait vs Context Switch·Ticket·MCS
  35. 35Mutex 성능 분석 — Futex·Adaptive·Priority Inheritance
  36. 36Reader-Writer Lock 성능 — Reader/Writer Priority·RCU·Seqlock
  37. 37Lock-Free 자료구조 성능 — CAS·ABA·Hazard Pointer·Epoch Reclamation
  38. 38Memory Ordering 분석 — Acquire·Release·Seq-Cst·ARM Relaxed Model
  39. 39Cache Coherency 프로토콜 — MESI·MOESI·Snoop·Directory
  40. 40SMP 성능 분석 — Per-Core·Affinity·Load Balance·Scalability
  41. 41Linux perf 기초 — stat·record·report 활용
  42. 42Linux perf 고급 — Raw Event·Tracepoint·perf script
  43. 43ftrace 활용 — function·function_graph·latency tracer
  44. 44eBPF·bpftrace 동적 트레이싱 — 커널 무수정 관측
  45. 45Flamegraph 분석 — On-CPU·Off-CPU·Differential
  46. 46ARM DS·Lauterbach 분석 — Hardware Trace 전문 도구
  47. 47Bare-metal 프로파일링 — GPIO·DWT·SysTick·ITM 활용
  48. 48NVIDIA Nsight Systems — GPU·NPU 포함 시스템 분석
  49. 49모던 프로파일러 비교 — Tracy·Hotspot·uftrace·Coz
  50. 50연속 프로파일링 — Parca·Pixie·Pyroscope·Tetragon
  51. 51실전 사례 — ISR Latency 100µs Deadline Miss 추적
  52. 52실전 사례 — Matrix Multiply가 예상의 10배 느린 이유
  53. 53실전 사례 — 8-core가 4-core를 넘으면 throughput이 떨어지는 이유
  54. 54실전 사례 — 카메라 1080p 60fps가 30fps로 떨어지는 이유
  55. 55CXL.mem 지연·대역폭 실측 — Direct·Switch·Pooled 토폴로지 비교
  56. 56CXL 성능 프로파일링 도구 — cxl-cli·DAMON·perf-mem 활용
  57. 57실전 사례 — CXL.mem 추가로 LLM inference KV cache 처리량 회복