본문 바로가기
Dev/javascript

Atomics?

by 괴발짜응 2025. 3. 11.
반응형

1. Atomics란 무엇인가?

Atomics는 ECMA2017(ES2017 이상)에서 제공되는 정적(Static) 객체로, 다중 스레드 환경에서 고유 메모리에 대해 원자적(atomic)인 연산을 수행할 수 있도록 도와주는 API이다.

 

1.1 왜 필요한가?

1. 멀티스레드 동기화

  • 자바스크립트는 원칙적으로 싱글 스레드(Event Loop) 기반 언어이지만, Web Worker나 Node.js Worker Threads 등을 사용하면 스레드 유사 환경에서 병렬 처리를 할 수 있다.
  • 이때 여러 스레드(Worker)가 공유 메모리(SharedArrayBuffer)에 접근, 수정할 수 있는데, 스레드 간 동기화(레이스 컨디션 방지)가 필요하다.
  • C/C++ 등 전통적인 언어에서 제공되는 원자적 연산(atomic operations)을 자바스크립트에서도 제공하기 위해 Atomics가 등장

2. SharedArrayBuffer

  • Atomics는 공유 버퍼인 SharedArrayBuffer 와 함께 사용된다.
  • SharedArrayBuffer를 TypedArray(Int8Array, Int16Array, Int32Array 등) 형태로 접근할 때, 그 메모리에 대해 원자적 연산을 수행할 수 있다.

3. Lock-free / Wait / Notify 모델

  • Atomics는 단순 Lock-free 알고리즘(ex: Compare-and-swap 등)을 직접 구현하거나, Atomics.wait(), Atomics.notify() 메서드를 통해 뮤텍스와 유사한 흐름을 구현하는 데 사용된다.

2. Atomics 사용 전제: SharedArrayBuffer, COOP/COEP

자바스크립트에서 Atomics를 사용하기 위해서는 TypedArray가 SharedArrayBuffer를 기반으로 생성되어야 한다.

// SharedArrayBuffer 생성 (예: 4byte = Int32 하나 저장 가능)
const sab = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT);
// sab를 기반으로 한 Int32Array 뷰 생성
const sharedArr = new Int32Array(sab);

 

2.1 브라우저 환경에서의 보안 정책 (COOP/COEP)

  • 크롬, 파이어폭스, 사파리 등 대부분의 브라우저들은 Spectre 취약점 등의 이유로, SharedArrayBuffer 사용 시 COOP와 COEP 헤더를 통한 교차 출처 격리를 요구한다.
  • 즉, Cross-Origin-Opener-Policy: same-origin 과 Cross-Origin-Embedder-Policy: require-corp 두 HTTP 헤더가 제대로 설정된 페이지에서만, SharedArrayBuffer를 기본적으로 사용할 수 있다.

 

2.2 Node.js 환경

  • Node.js Worker Threads 환경에서는 브라우저와 달리 HTTP 헤더 정책이 없으므로 바로 SharedArrayBuffer를 사용할 수 있다.
  • Node.js 버전 10.5 이상에서 Worker Threads가 실험적으로 도입되었고, 12+ 버전에서 보다 안정적으로 제공되고 있다.

3. Atomics 메서드 종류

메서드 설명
Atomics.add(typedArray, index, value) typedArray[index]에 value를 더하고, 더하기 연산 수행 전의 값을 반환.
Atomics.sub(typedArray, index, value) typedArray[index]에 value를 빼고, 연산 전의 값을 반환.
Atomics.and(typedArray, index, value) typedArray[index]에 value를 AND 비트 연산, 연산 전의 값을 반환.
Atomics.or(typedArray, index, value) typedArray[index]에 value를 OR 비트 연산, 연산 전의 값을 반환.
Atomics.xor(typedArray, index, value) typedArray[index]에 value를 XOR 비트 연산, 연산 전의 값을 반환.
Atomics.compareExchange(typedArray, index, expectedValue, replacementValue) typedArray[index]가 expectedValue와 같으면 replacementValue로 교체하고, 기존 값을 반환.(Compare-And-Swap)
Atomics.exchange(typedArray, index, value) typedArray[index]를 value로 교체하고, 교체 전의 값을 반환.
Atomics.load(typedArray, index) typedArray[index]의 값을 원자적으로 읽어서 반환.
Atomics.store(typedArray, index, value) typedArray[index]에 value를 원자적으로 저장하고, 저장한 value를 반환
Atomics.wait(typedArray, index, value, timeout) typedArray[index]의 값이 value와 일치할 동안대기(블록). timeout(밀리초) 옵션이 잇면 해당 시간 후 깨어남. (단, Int32Array에서만 가능) 
Atomics.notify(typedArray, index, count) Atomics.wait()로 대기 중인 스레드/워커 중 count개를 깨움. (기본값은 무제한, 즉 대기 중인 스레드 전부 깨움)
Atomics.isLockFree(size) 해당 사이즈(바이트)가 락 프리(lock-free) 연산이 가능한지 여부를 boolean으로 반환. (2,4,8 byte가 lock-free인지 등 하드웨어 종속성에 따라 다름)
주의: Atomics.wait()와 Atomics.notify()는 
브라우저에서는 아직 일부 제한적 지원이며, WebAssembly Threads 활성화 등 조건이 필요할 수 있다.
Node.js에서는 12+ 버전 이후 대부분 지원.

 


4.  Atomics 사용 예시

4.1 단순한 원자적 덧셈

// 1) SharedArrayBuffer 생성
const sharedBuffer = new SharedArrayBuffer(4); // 4바이트
const sharedArr = new Int32Array(sharedBuffer);

// 2) 메인 스레드에서 sharedArr[0] 초기값 0
sharedArr[0] = 0;

// 3) 다른 Worker에서 sharedArr[0]에 1을 더하고, 이전 값을 반환
const oldValue = Atomics.add(sharedArr, 0, 1);
console.log(oldVvalue); // 0 (연산 전 값)
console.log(sharedArr[0]); // 1 (연산 후 값)

여기서 Atomics.add(sharedArr, 0, 1)는 

1. sharedArr[0]를 읽어서 oldValue에 담은 뒤,

2. sharedArr[0]에 1을 더하는 작업을 원자적으로 수행한다.

 

4.2 Compare-And-Swap (CAS) 패턴

Compare-And-Swap은 락 없이 동시성을 제어할 수 있는 핵심 패턴 중 하나로, Atomics.compareExchange()를 이용해 구현할 수 있다.

function atomicIncrementIfZero(typedArr, idx) {
  // typedArr[idx]가 0이면 1로 바꾸고, 연산 전 값을 oldVal에 담음
  const oldVal = Atomics.compareExchange(typedArr, idx, 0, 1);
  return oldVal;
}

const sharedBuffer = new SharedArrayBuffer(4);
const sharedArr = new Int32Array(sharedBuffer);
sharedArr[0] = 0;

// sharedArr[0]가 0이면 1로 바꾼다.
const oldVal = atomicIncrementIfZero(sharedArr, 0);
console.log(oldVal);  // 0 (교체 전 값)
console.log(sharedArr[0]); // 1 (교체 후 값)

만약 sharedArr[0]가 이미 1이었다면 compareExchange는 oldVal로 1을 반환하고, 실제 값은 변화가 없다.

 

4.3 스레드 간 "대기/깨우기"(wait/notify)

Atomics.wait() / Atomics.notify()를 통해, 전통적인 POSIX 스레드의 pthread_cond_wait / pthread_cond_signal 같은 기능을 흉내낼 수 있다.

  • Atomics.wait(typedArr, index, value[, timeout])
    • typedArr[index]의 현재 값이 value와 같은 동안 블록 상태(대기)에 들어간다.
    • 대기는 다른 스레드에서 Atomics.notify()를 호출하거나, typedArr[index]가 바뀌어 더 이상 value가 아니게 되거나, timeout이 만료될 때까지 이어진다.
  • Atomics.notify(typedArr, index[, count])
    • Atomics.wait()로 typedArr[index]에 대해 대기 중인 스레드들 중 count개를 깨운다.(기본값: 무제한)
// ---- main.js ----
const sab = new SharedArrayBuffer(Int32Array.BYPTES_PER_ELEMENT);
const sharedInt = new Int32Array(sab);
sharedInt[0] = 0;

const worker = new Worker('worker.js');
worker.postMessage(sab);

//메인 스레드에서 2초 후 값 변경 및 notify
setTimeout(() => {
  // sharedInt[0]를 1로 바꿔서, wait 중인 워커를 깨울 수 있도록 
  Atomics.store(sharedInt, 0, 1);
  Atomics.notify(sharedInt, 0);
}, 2000);
// ---- worker.js ----
onmessage = function(e) {
  const sab = e.data;
  const sharedInt = new Int32Array(sab);
  
  console.log('Worker: wait 시작');
  //현재 sharedInt[0]가 0이면, wait로 대기
  const res = Atomics.wait(sharedInt, 0, 0);
  // notify 되거나 값이 달라지면 여기로 복귀
  console.log('Worker: wait 종료. 결과:', res); // 'ok', 'time-out', 또는 'not-equa'
  
  // 이후 로직 수행...
};

위 코드는 worker.js 내부에서 sharedInt[0] 값이 0인 동안 대기한다. 메인 스레드세서 2초 뒤에 sharedInt[0] = 1로 바꾸고, Atomics.notify(sharedInt[0], 0)로 대기 중인 워커를 깨우면 Atomics.wait 호출이 해제된다.

참고: Atomics.wait()는 Int32Array 에 대해서만 동작하며, 브라우저별 지원 여부가 다르다. (Chrome/Firefox 최신 버전에서 대부분 가능하지만, Safari는 아직 제한적이다).

 

5. 성능과 주의사항

1. 원자적 연산은 일반 연산보다 비용이 크다.

  • 하드웨어 차원에서 캐시 라인 동기화가 일어나므로 일반 메모리 접근보다 느리다.
  • 너무 빈번한 원자적 연산은 성능 병목을 일으킬 수 있다.

2. 브라우저 메인 스레드에서의 사용은 신중할 것.

  • 브라우저에서 메인 스레드는 UI, 이벤트 루프를 처리한다.
  • 만약, 메인 스레드가 Atomics.wait()를 호출해 블록되면, UI가 멈추는 문제가 발생한다.
  • 실제로 브라우저가 메인 스레드에서 wait()를 금지하는 등 제한을 두는 케이스가 많다.(최신 표준에 따라 메인 스레드에서는 Atomics.wait()가 동작하지 않도록 지정).
  • 따라서 Worker에서만 사용하는 것이 권장된다.

3. 락(Spinlock) vs. wait/notify

  • Atomics.compareExchange 등을 이용해 스핀락(Spinlock)을 구현할 수 있지만, 해당 스레드는 '바쁘게 도는' 상태에서 CPU 소모가 크다.
  • Atomics.wait/notify를 사용하면, 스레드가 대기 상태로 들어가 CPU를 소모하지 않게 된다.
  • 다만, 모든 브라우저에서 완전히 지원되는 것은 아니므로. 프로젝트 호환성을 고려해야 한다. 

4. COOP/COEP 설정 필수 (브라우저)

  • 앞서 언급했듯이, 브라우저에서 SharedArrayBuffer를 사용하려면 보안 정책(교차 출처 격리)이 필요하다.

6. 요약

1. Atomics는 자바스크립트에서 멀티스레드 환경의 동기화를 위해 원자적 연산을 제공하는 정적 객체이다.

2. SharedArrayBuffer + TypedArray 조합에서만 동작하며, 브라우저에서는 보안 정책(COOP/COEP) 설정이 필요하다.

3. 핵심 메서드: add, sub, adn, or, xor, compareExchange, exchange, load, store, wait, notify 등이 있다.

4. wait / notify 를 통해 효율적인 스레드 블로킹/깨우기가 가능하나, 호환성과 메인 스레드 사용 여부를 주의해야 한다.

5. 일반 연산보다 비용이 더 크고, 잘못 사용하면(특히 스핀락 구현 시) CPU 사용률이 크게 증가할 수 있으므로, 필요한 경우에만 사용해야 한다.

 

결국, Atomics는 자바스크립트가 고성능, 병렬 처리를 일부 지원할 수 있도록 만들어진 중요한 API이지만, 대부분의 웹 애플리케이션은 Worker간 메시징(메시지 채널)으로 충분히 해결되는 경우가 많다. 성능 최적화나, 실제로 공유 메모리를 직접 제어해야 하는 고급 시나리오에서 Atomics를 적용하면 된다. 


반응형

'Dev > javascript' 카테고리의 다른 글

JavaScript에서 Proxy란  (0) 2025.04.09
Express 5.0 및 5.1의 주요 변경 사항  (0) 2025.04.03
Spinlock - 스핀락  (0) 2025.02.28
SharedArrayBuffer  (1) 2025.02.28
CommonJS, AMD, UMD, ESM  (1) 2025.02.24