336 lines
7.9 KiB
Markdown
336 lines
7.9 KiB
Markdown
---
|
|
id: cs-lockfree-atomic
|
|
title: Lock-free / Atomic — Concurrent 자료구조
|
|
category: Coding
|
|
status: draft
|
|
source_trust_level: B
|
|
verification_status: conceptual
|
|
created_at: 2026-05-09
|
|
updated_at: 2026-05-09
|
|
tags: [cs, concurrency, lockfree, vibe-coding]
|
|
tech_stack: { language: "Various", applicable_to: ["Backend"] }
|
|
applied_in: []
|
|
aliases: [lock-free, atomic, CAS, compare-and-swap, SharedArrayBuffer, Atomics, memory barrier]
|
|
---
|
|
|
|
# Lock-free / Atomic
|
|
|
|
> Lock 없이 동시성. **CAS (compare-and-swap), atomic ops, memory barrier**. 빠름 but 어려움. JS = SharedArrayBuffer + Atomics.
|
|
|
|
## 📖 핵심 개념
|
|
- Atomic: 한 instruction 이 indivisible.
|
|
- CAS: "예상값과 같으면 새 값으로" 한 step.
|
|
- Memory ordering: read/write reorder 제어.
|
|
- Lock-free: thread 가 block 안 됨.
|
|
|
|
## 💻 코드 패턴
|
|
|
|
### JS Atomics (SharedArrayBuffer)
|
|
```ts
|
|
// Setup — Cross-Origin Isolation 필요
|
|
// Headers:
|
|
// Cross-Origin-Opener-Policy: same-origin
|
|
// Cross-Origin-Embedder-Policy: require-corp
|
|
|
|
const sab = new SharedArrayBuffer(1024);
|
|
const view = new Int32Array(sab);
|
|
|
|
// Atomic ops
|
|
Atomics.store(view, 0, 42);
|
|
const v = Atomics.load(view, 0);
|
|
Atomics.add(view, 0, 1); // atomic increment
|
|
Atomics.compareExchange(view, 0, 42, 99); // CAS
|
|
|
|
// Wait / notify (futex-like)
|
|
Atomics.wait(view, 0, 0); // 0 일 때 block
|
|
Atomics.notify(view, 0, 1); // 1 thread 깨움
|
|
```
|
|
|
|
### Worker 간 공유
|
|
```ts
|
|
// main.ts
|
|
const sab = new SharedArrayBuffer(1024);
|
|
const counter = new Int32Array(sab);
|
|
|
|
const worker = new Worker(new URL('./worker.ts', import.meta.url));
|
|
worker.postMessage({ sab });
|
|
|
|
// counter 변경
|
|
Atomics.add(counter, 0, 1);
|
|
|
|
// worker.ts
|
|
self.onmessage = (e) => {
|
|
const { sab } = e.data;
|
|
const counter = new Int32Array(sab);
|
|
|
|
setInterval(() => {
|
|
const v = Atomics.load(counter, 0);
|
|
console.log('counter:', v);
|
|
}, 100);
|
|
};
|
|
```
|
|
|
|
### CAS 패턴 (lock-free counter)
|
|
```ts
|
|
function atomicIncrement(view: Int32Array, idx: number): number {
|
|
while (true) {
|
|
const cur = Atomics.load(view, idx);
|
|
const next = cur + 1;
|
|
if (Atomics.compareExchange(view, idx, cur, next) === cur) {
|
|
return next;
|
|
}
|
|
// 다른 thread 가 변경 — retry
|
|
}
|
|
}
|
|
|
|
// 또는 더 단순 — Atomics.add (atomic 자동)
|
|
const next = Atomics.add(view, idx, 1) + 1;
|
|
```
|
|
|
|
### Lock-free queue (single-producer, single-consumer)
|
|
```ts
|
|
class SPSCQueue<T> {
|
|
private buffer: T[];
|
|
private head = new Int32Array(new SharedArrayBuffer(4));
|
|
private tail = new Int32Array(new SharedArrayBuffer(4));
|
|
private capacity: number;
|
|
|
|
constructor(cap: number) {
|
|
this.capacity = cap;
|
|
this.buffer = new Array(cap);
|
|
}
|
|
|
|
push(item: T): boolean {
|
|
const t = Atomics.load(this.tail, 0);
|
|
const next = (t + 1) % this.capacity;
|
|
if (next === Atomics.load(this.head, 0)) return false; // full
|
|
this.buffer[t] = item;
|
|
Atomics.store(this.tail, 0, next);
|
|
return true;
|
|
}
|
|
|
|
pop(): T | null {
|
|
const h = Atomics.load(this.head, 0);
|
|
if (h === Atomics.load(this.tail, 0)) return null; // empty
|
|
const item = this.buffer[h];
|
|
Atomics.store(this.head, 0, (h + 1) % this.capacity);
|
|
return item;
|
|
}
|
|
}
|
|
```
|
|
|
|
### Mutex (Atomics)
|
|
```ts
|
|
const lockState = new Int32Array(new SharedArrayBuffer(4));
|
|
const UNLOCKED = 0;
|
|
const LOCKED = 1;
|
|
|
|
function lock() {
|
|
while (Atomics.compareExchange(lockState, 0, UNLOCKED, LOCKED) !== UNLOCKED) {
|
|
Atomics.wait(lockState, 0, LOCKED);
|
|
}
|
|
}
|
|
|
|
function unlock() {
|
|
Atomics.store(lockState, 0, UNLOCKED);
|
|
Atomics.notify(lockState, 0, 1);
|
|
}
|
|
|
|
// 사용
|
|
lock();
|
|
try {
|
|
// critical section
|
|
} finally {
|
|
unlock();
|
|
}
|
|
```
|
|
|
|
→ Spinlock + futex.
|
|
|
|
### Memory ordering
|
|
```
|
|
JS Atomics = sequential consistency (가장 강).
|
|
다른 언어 (C++, Rust) = 더 weak ordering 가능 (relaxed, acquire, release).
|
|
|
|
Acquire/release: synchronization point.
|
|
Relaxed: ordering 보장 X — 빠름 but careful.
|
|
```
|
|
|
|
### Rust atomic
|
|
```rust
|
|
use std::sync::atomic::{AtomicI32, Ordering};
|
|
|
|
let counter = AtomicI32::new(0);
|
|
counter.fetch_add(1, Ordering::SeqCst);
|
|
counter.compare_exchange(0, 42, Ordering::SeqCst, Ordering::SeqCst);
|
|
|
|
// 더 weak — 빠름 but careful
|
|
counter.fetch_add(1, Ordering::Relaxed);
|
|
```
|
|
|
|
### Java AtomicInteger
|
|
```java
|
|
AtomicInteger counter = new AtomicInteger(0);
|
|
counter.incrementAndGet();
|
|
counter.compareAndSet(0, 42);
|
|
```
|
|
|
|
### Lock vs Lock-free 결정
|
|
```
|
|
Lock:
|
|
+ 단순
|
|
+ Fairness
|
|
- Contention 시 thread block
|
|
- Priority inversion
|
|
- Deadlock 가능
|
|
|
|
Lock-free:
|
|
+ Thread block 없음
|
|
+ 더 빠름 (low contention)
|
|
+ Deadlock 없음
|
|
- 어려움
|
|
- 더 어려운 디버깅
|
|
- ABA problem
|
|
```
|
|
|
|
### ABA problem
|
|
```
|
|
T1: read A, prepare to CAS A → C
|
|
T2: A → B, B → A
|
|
T1: CAS succeeds (A 가 같다고) — but state 변경됐음
|
|
|
|
해결: version counter (double-CAS) 또는 hazard pointer.
|
|
```
|
|
|
|
```ts
|
|
// Versioned pointer
|
|
const ptr = new Int32Array(new SharedArrayBuffer(8));
|
|
// ptr[0] = value, ptr[1] = version
|
|
|
|
function update(newValue: number) {
|
|
while (true) {
|
|
const oldValue = Atomics.load(ptr, 0);
|
|
const oldVersion = Atomics.load(ptr, 1);
|
|
|
|
if (Atomics.compareExchange(ptr, 1, oldVersion, oldVersion + 1) === oldVersion) {
|
|
Atomics.store(ptr, 0, newValue);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### False sharing
|
|
```
|
|
같은 cache line 의 다른 변수 — 한 쪽 쓰기 = 다른 쪽 cache invalidate.
|
|
→ 큰 latency.
|
|
|
|
해결: padding (cache line 64 byte align).
|
|
```
|
|
|
|
```rust
|
|
#[repr(align(64))]
|
|
struct PaddedCounter {
|
|
value: AtomicI32,
|
|
_pad: [u8; 60],
|
|
}
|
|
```
|
|
|
|
→ JS 는 control 어려움.
|
|
|
|
### Use case
|
|
```
|
|
- Performance counter (atomic increment)
|
|
- Real-time game state share
|
|
- Lock-free queue (audio / video stream)
|
|
- Reference counting
|
|
- Wait-free read mostly data
|
|
```
|
|
|
|
### Anti use case (lock 가 낫음)
|
|
```
|
|
- 복잡 데이터 구조
|
|
- 적은 contention (lock 빠름)
|
|
- 단순 작업 — 가독성 우선
|
|
- Test / debug 어려움
|
|
```
|
|
|
|
### Higher-level patterns
|
|
```
|
|
- Read-Copy-Update (RCU): 읽기 다중, 변경 시 새 copy.
|
|
- Hazard pointer: ABA 안전 free.
|
|
- Crossbeam (Rust): high-quality lock-free.
|
|
- Java java.util.concurrent: thread-safe collections.
|
|
```
|
|
|
|
### JS — lock-free 제한
|
|
```
|
|
JS 는 single-threaded except Worker.
|
|
Worker 간만 SharedArrayBuffer 가능.
|
|
Cross-Origin Isolation 필요.
|
|
|
|
→ 보통 lock-free 의 use case 적음.
|
|
AudioWorklet / WebGPU compute = 강.
|
|
```
|
|
|
|
### TypeScript types
|
|
```ts
|
|
// Strict typing 어려움 — Atomics 가 untyped.
|
|
function atomicLoad(view: Int32Array, idx: number): number {
|
|
return Atomics.load(view, idx);
|
|
}
|
|
```
|
|
|
|
### Test
|
|
```ts
|
|
// 동시성 test 어려움. Stress test:
|
|
const N_WORKERS = 4;
|
|
const N_OPS = 100000;
|
|
const counter = new Int32Array(new SharedArrayBuffer(4));
|
|
|
|
const promises = Array.from({ length: N_WORKERS }, () => {
|
|
return new Promise((resolve) => {
|
|
const w = new Worker(new URL('./incrementer.ts', import.meta.url));
|
|
w.postMessage({ counter, ops: N_OPS });
|
|
w.onmessage = resolve;
|
|
});
|
|
});
|
|
|
|
await Promise.all(promises);
|
|
|
|
const expected = N_WORKERS * N_OPS;
|
|
const actual = Atomics.load(counter, 0);
|
|
expect(actual).toBe(expected);
|
|
```
|
|
|
|
## 🤔 의사결정 기준
|
|
| 상황 | 추천 |
|
|
|---|---|
|
|
| 단순 counter | Atomic |
|
|
| Single producer/consumer | Lock-free SPSC queue |
|
|
| Multi producer | Lock 또는 lock-free MPMC (어려움) |
|
|
| Read-mostly | RCU / version |
|
|
| 복잡 자료구조 | Lock |
|
|
| Real-time audio | Lock-free 필수 |
|
|
| JS web worker | SharedArrayBuffer + Atomics |
|
|
|
|
## ❌ 안티패턴
|
|
- **Lock-free 무 측정**: 실제 슬로우 가능.
|
|
- **ABA 무관심**: subtle bug.
|
|
- **Memory ordering 무지**: data race.
|
|
- **Spinlock 큰 contention**: CPU 폭발.
|
|
- **JS Atomics + COOP/COEP 없음**: undefined.
|
|
- **False sharing 무 padding**: cache thrash.
|
|
- **Race condition test 없음**: 가끔 fail.
|
|
|
|
## 🤖 LLM 활용 힌트
|
|
- 일반 = mutex.
|
|
- Hot counter = atomic.
|
|
- Worker 통신 = SharedArrayBuffer + Atomics.
|
|
- Memory ordering 알기 (대부분 SeqCst OK).
|
|
|
|
## 🔗 관련 문서
|
|
- [[Web_OffMain_WebWorker]]
|
|
- [[CS_Cache_Eviction]]
|
|
- [[CS_Backpressure_Deep]]
|