SharedArrayBuffer와 Atomics — 멀티스레드 공유 메모리
SharedArrayBuffer는 메인 스레드와 Web Worker 간에 메모리를 복사 없이 공유하는 API입니다.Atomics는 공유 메모리에서 발생하는 경쟁 조건(Race Condition)을 방지하는 동기화 도구입니다.
일반 ArrayBuffer vs SharedArrayBuffer
// 일반 ArrayBuffer — 전송 시 복사 또는 소유권 이전
const buffer = new ArrayBuffer(1024);
worker.postMessage(buffer, [buffer]); // Transferable: 소유권 이전
console.log(buffer.byteLength); // 0 — 더 이상 접근 불가
// SharedArrayBuffer — 공유! 복사 없음
const sharedBuffer = new SharedArrayBuffer(1024);
worker.postMessage(sharedBuffer); // 복사 없이 공유
console.log(sharedBuffer.byteLength); // 1024 — 여전히 접근 가능
보안 요구 사항 (COOP/COEP)
Spectre 공격 이후 SharedArrayBuffer 사용에는 보안 헤더가 필요합니다.
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
// 브라우저에서 확인
if (typeof SharedArrayBuffer !== "undefined") {
console.log("SharedArrayBuffer 사용 가능");
}
// self.crossOriginIsolated가 true여야 함
console.log(self.crossOriginIsolated);
기본 공유 메모리 패턴
// 메인 스레드
const sharedBuffer = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 10);
const sharedArray = new Int32Array(sharedBuffer);
// 초기값 설정
sharedArray[0] = 42;
// 워커에 공유
worker.postMessage(sharedBuffer);
// worker.js
self.addEventListener("message", (e) => {
const sharedArray = new Int32Array(e.data);
console.log(sharedArray[0]); // 42 — 메인 스레드의 데이터 직접 접근
sharedArray[0] = 100; // 메인 스레드에서도 변경 반영
});
경쟁 조건 문제
// 두 워커가 동시에 sharedArray[0]++를 실행하면?
// 읽기 → 증가 → 쓰기가 원자적이지 않으므로
// 값이 손실될 수 있음 (Lost Update)
// sharedArray[0] = 0 일 때
// Worker A: 읽기(0) → 증가(1) → 쓰기(1)
// Worker B: 읽기(0) → 증가(1) → 쓰기(1)
// 결과: 1 (기대값 2)
Atomics — 원자적 연산
const sharedArray = new Int32Array(sharedBuffer);
// 원자적 읽기/쓰기
Atomics.store(sharedArray, 0, 42); // 원자적 쓰기
const value = Atomics.load(sharedArray, 0); // 원자적 읽기
// 원자적 증가/감소
Atomics.add(sharedArray, 0, 1); // sharedArray[0] += 1 (원자적)
Atomics.sub(sharedArray, 0, 1); // sharedArray[0] -= 1 (원자적)
// 원자적 비트 연산
Atomics.and(sharedArray, 0, 0xFF);
Atomics.or(sharedArray, 0, 0x80);
Atomics.xor(sharedArray, 0, 0x01);
// 원자적 교환
const old = Atomics.exchange(sharedArray, 0, 99); // 이전 값 반환
// 원자적 비교 후 교환 (CAS)
Atomics.compareExchange(sharedArray, 0, 99, 100);
// sharedArray[0]이 99이면 100으로 교환, 아니면 변경 없음
Atomics.wait / Atomics.notify — 스레드 동기화
// Worker (대기 측)
const sharedArray = new Int32Array(sharedBuffer);
console.log("데이터 대기 중...");
Atomics.wait(sharedArray, 0, 0); // sharedArray[0]이 0인 동안 대기
console.log("깨어남! 값:", Atomics.load(sharedArray, 0));
// 메인 스레드 (알림 측)
Atomics.store(sharedArray, 0, 42); // 값 변경
Atomics.notify(sharedArray, 0, 1); // 대기 중인 워커 1개 깨우기
**주의 **: Atomics.wait은 메인 스레드에서 사용할 수 없습니다 (블로킹 방지).
뮤텍스(Mutex) 구현
class Mutex {
constructor(sharedArray, index = 0) {
this.sharedArray = sharedArray;
this.index = index;
}
lock() {
// 0이면 1로 바꾸고 진행 (잠금 획득)
while (Atomics.compareExchange(this.sharedArray, this.index, 0, 1) !== 0) {
// 이미 잠겨있으면 대기
Atomics.wait(this.sharedArray, this.index, 1);
}
}
unlock() {
Atomics.store(this.sharedArray, this.index, 0);
Atomics.notify(this.sharedArray, this.index, 1);
}
}
// 워커에서 사용
const mutex = new Mutex(new Int32Array(sharedBuffer));
mutex.lock();
// 임계 구역 — 안전하게 공유 데이터 수정
sharedData[1] += 1;
mutex.unlock();
실전 — 병렬 배열 처리
// 메인 스레드
const size = 1_000_000;
const buffer = new SharedArrayBuffer(Float64Array.BYTES_PER_ELEMENT * size);
const array = new Float64Array(buffer);
// 데이터 초기화
for (let i = 0; i < size; i++) array[i] = i;
// 4개 워커에 분할
const workerCount = 4;
const chunkSize = size / workerCount;
for (let i = 0; i < workerCount; i++) {
const worker = new Worker("compute.js");
worker.postMessage({
buffer,
start: i * chunkSize,
end: (i + 1) * chunkSize,
});
}
// compute.js
self.addEventListener("message", (e) => {
const { buffer, start, end } = e.data;
const array = new Float64Array(buffer);
for (let i = start; i < end; i++) {
array[i] = Math.sqrt(array[i]); // 각 워커가 자기 구간만 처리
}
self.postMessage("done");
});
SharedArrayBuffer vs postMessage
| 항목 | SharedArrayBuffer | postMessage |
|---|---|---|
| 데이터 복사 | 없음 (공유) | 있음 (복사) |
| 속도 | 빠름 | 느림 (대용량 시) |
| 동기화 | Atomics 필요 | 불필요 |
| 보안 헤더 | COOP/COEP 필요 | 불필요 |
| 사용 난이도 | 높음 | 낮음 |
**기억하기 **: SharedArrayBuffer는 Web Worker 간 메모리를 복사 없이 공유하며, Atomics는 경쟁 조건을 방지합니다. COOP/COEP 헤더가 필요하고, 동기화 로직이 복잡하므로 대용량 데이터 처리나 고성능이 필요한 경우에만 사용합니다. 대부분의 경우 postMessage + Transferable로 충분합니다.
댓글 로딩 중...