스핀락(Spinlock)이란?
- 스핀락은 락(잠금) 대상이 해제될 때까지 스레드가 계속해서(바쁘게) 확인하는(스핀 도는)' 방식으로 동작하는 락 구현이다.
- 보통 멀티스레드 환경에서 하나의 스레드가 잠금을 획득하려고 할 때, 잠금을 사용 중인 다른 스레드가 잠금을 해제할 때까지 짧은 반복문으로 대기(바쁜 대기, busy-wait)하는 구조다.
- 일반적인 뮤텍스(mutex)는 OS 커널이 스레를 '수면(sleep)' 상태로 만들어 CPU를 양보하지만, 스핀락은 스레드를 그대로 계속 돌면서 잠금 여부를 확인한다.
스핀락의 장단점
장점
- 락을 기다리는 시간이 매우 짧다면(ns ~ μs), 스레드를 커널에 의뢰해 '수면/깨우기(sleep/wakeup)' 과정을 거치는 것보다 빠를 수 있다.
- 커널에 의존하기보다 사용자 레벨에서 락을 제어하므로 컨텍스트 스위칭 비용이 적을 때 이점이 있다.
단점
- 바쁘게 도는 만큼 CPU 자원을 계속 사용한다(= 효율이 떨어짐).
- 락 경쟁이 심하면 CPU 부담이 커지고, 다른 작업이 지연될 수 있다.
- 장시간 잠금이 필요한 상황에서는 오히려 커널 락(뮤텍스) 같은 방식을 쓰는 편이 낫다.
자바스크립트에서 스핀락은 왜 특별한가?
자바스크립트(브라우저 메인 스레드)는 전통적으로 싱글 스레드 + 이벤트 루프 모델을 사용하기 때문에 "바쁘게 도는" 코드를 작성하면 이벤트 루프가 막혀서 다른 이벤트를 처리할 수 없게 된다. 즈거, 스피란의 의미가 거의 없어진다.
다만, Web Worker(또는 Node.js Worker Threads) 와 SharedArrayBuffer + Atomics를 사용하면, 멀티스레드와 유사한 구조에서 공유 메모리에 접근이 가능하다. 이때, Worker 간 자원을 잠금할 필요가 있을 수 있는데, 그런 상황에서 이론적으로 스핀락을 구현할 수 있다.
주의: 브라우저 환경에서 SharedArrayBuffer를 사용하려면, COOP/COEP 등의 보안 헤더가 설정되어 있어야 한다. 보통크롬, 파이어폭스, 사파리 등에서는 SharedArrayBuffer를 기본적으로 차단하기 때문이다.
1. 간단한 자바스크립트 스핀락 구현
1.1 SharedArrayBuffer와 Atomics를 이용한 예시
SharedArrayBuffer 생성 및 공유 배열 뷰 생성
// lockArray는 길이 1짜리 Int32Array
// 0 -> unlocked, 1 -> locked
const sharedBuffer = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT);
const lockArray = new Int32Array(sharedBuffer);
lockArray[0] = 0; // 초기 상태 unlocked
스핀락 함수 구현
function spliLock(lockArray) {
// Atomics.compareExchange(배열, 인덱스, 예상값, 바꿀 값)
// lockArray[0]이 0이면(=unlocked이면) 1로 바꾸고, 이전 값을 반환(0)
// lockArray[0]이 이미 1이면(=locked), compareExchange는 1을 반환
while (Atomics.compareExchange(lockArray, 0, 0, 1) !== 0) {
// 잠금이 풀릴 때까지 바쁘게 대기(spin)
// 아주 짧은 대기나, Atomics.wait 등을 활용할 수도 있음
}
}
function spinUnlock(lockArray) {
// 락 해제
Atomics.store(lockArray, 0, 0);
}
Worker간 사용 예시
예시로 메인 스레드와 하나의 워커가 있다고 가정. 워커에 sharedBuffer 를 전달하고, 내부에서 spinLock(lockArray) -> 공유 데이터 수정 -> spinUnlock(lockArray) 과 같이 사용할 수 있다.
// main.js
const sharedBuffer = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT);
const sharedView = new Int32Array(sharedBuffer);
const worker = new Worker('worker.js');
worker.postMessage({ sharedBuffer });
// 메인에서 사용 예
spinLock(lockArray);
// 공유 데이터 갱신
doSomething();
spinUnlock(lockArray);
// worker.js
onmessage = (e) => {
const { sharedBuffer } = e.data;
const lockArray = new Int32Array(sharedBuffer);
// worker에서도 동일하게 사용
spinLock(lockArray);
// 공유 데이터 갱신
doSomething();
spinUplock(lockArray);
};
function spinLock(lockArray) {
while (Atomics.compareExchange(lockArray, 0, 0, 1) !== 0) {}
}
function spinUnlock(lockArray) {
Atomics.store(lockArray, 0, 0);
}
1.2 Atomics.wait/notify로 개선하기
단순 무한 푸르(while)로 스핀을 돌리면 CPU가 계속 소비된다. Atomics.wait() (Node.js, 브라우저에서 WebAssembly Threads가 활성화된 환경 등 지원 필요)와 Atomics.notify()를 이용하면, OS 차원의 block/wakeup과 비슷한 구조로 구현이 가능하다. 이 방식 전통적인 "뮤텍스 + 조건 변수" 구조에 가깝다.
function lock(lockArray) {
while (true) {
// 아직 잠겨있지 않다면(0) -> 1로 만들어 락 획득 시도
if (Atomics.compareExchange(lockArray, 0, 0, 1) === 0) {
// 잠금 성공
return;
}
// 잠금에 실패하면 현재 스레드는 대기
Atomics.wait(lockArray, 0, 1);
}
}
function unlock(lockArray) {
// 1 -> 0 (잠금 해제)
Atomics.store(lockArray, 0, 0);
// 잠금 대기 중인 다른 스레드를 깨움
Atomics.notify(lockArray, 0, 1);
}
이렇게 하면 무조건 busy-wait 하지 않고, 잠금이 걸려있는 동안에는 Atomics.wait()를 통해 일시 정지 상태가 되었다가, 잠금 해제 시점에 Atomics.notify() 호출로 깨울 수 있다(단, 모든 브라우저에서 완벽히 지원되는 것은 아니므로 호환성을 확인야 한다).
스핀락 vs. 다른 동기화 방안
- 자바스크립트에서 머티스레드를 다루는 경우, 스핀락보다는 일반적으로 Atomics.wait/notify나 MessageChaneel, postMessage 등을 통한 Worker 간 통신으로 문제를 해결하는 편이 많다.
- 브라우저는 전통적인 POSIX 스레드 환경과 다르고, 메인 스레드에서는 특히 스핀락을 사용하면 UI나 이벤트 루프가 완전히 멈춘다.
- Node.je Worker Threads 환경 역시, Node.js 자체에서 제공하는 MessagePort나 다른 비동기 I/O 구조를 사용하는 편이 더 낫다.
- 스핀락은 매우 짧은 시간( 몇 마이크로초 이하) 동안 잠금이 필요한 극히 제한된 상황에서만 성능상의 이점이 있을 수 있다.(그 밖에는 OS 스케쥴링/뮤텍나 Atomics.wait/notify 쪽이 CPU 낭비를 줄이는 데 유리)
정리
- 스핀락은 여러 스레드가 동시에 하나의 공유 자원에 접근할 때, 락이 해제될 때까지 바쁘게 돌면서 확인하는 방식이다.
- 자바스크립트는 원래 싱글 스레드이므로, 전통적 의미의 스핀락이 메인 스레드에서 쓰이기는 어렵다(이벤트 루프를 차단하기 때문).
- SharedArraybuffer + Atomics (그리고 Worker Threads)를 이용하면 멀티스레드 유사 환경에서 스핀락을 구현할 수 있다.
- 그러나 CPU 점유가 매우 커질 수 있으므로, 실제로는 Atomics.wait/notify 또는 메시지 기반의 동기화 기법을 더 권장한다.
결론
자바스크립트에서 스핀락은 특정 멀티스레드 상황(Worker + SharedArrayBuffer)에서만 의미가 있으며, 대부분의 웹 애플리케이션에서는 부적합하거나 다른 동기화 방식이 더 효율적이다.
終
'Dev > javascript' 카테고리의 다른 글
Express 5.0 및 5.1의 주요 변경 사항 (0) | 2025.04.03 |
---|---|
Atomics? (1) | 2025.03.11 |
SharedArrayBuffer (1) | 2025.02.28 |
CommonJS, AMD, UMD, ESM (1) | 2025.02.24 |
ECMAScript 2024 : ES2024 - 주요 추가 기능 (0) | 2025.02.22 |