자바스크립트의 SharedArrayBuffer

자바스크립트의 SharedArrayBuffer

이 문서는 자바스크립트의 SharedArrayBuffer에 대해 설명합니다.

SharedArrayBuffer의 기본 개념, 사용 방법, 구체적인 사용 사례, 그리고 보안 관련 사항에 대해 자세히 설명하겠습니다.

YouTube Video

자바스크립트의 SharedArrayBuffer

SharedArrayBuffer는 여러 스레드 간의 메모리를 공유하기 위한 자바스크립트의 강력한 도구입니다. 특히 Web Workers와 결합하면 병렬 처리가 가능하여 계산 집약적인 작업과 실시간 기능이 필요한 애플리케이션에 효과적입니다.

SharedArrayBuffer란 무엇인가?

SharedArrayBuffer는 여러 스레드(주로 Web Workers) 간에 이진 데이터를 공유할 수 있는 자바스크립트의 메모리 버퍼를 제공합니다. 일반 ArrayBuffer는 메인 스레드와 워커 간에 복사가 필요하지만, SharedArrayBuffer는 복사 없이 직접 메모리 공유가 가능하여 성능을 크게 향상시킵니다.

기능

  • 공유 메모리 여러 스레드가 동일한 메모리 공간에서 작업할 수 있게 합니다.
  • 성능 향상 복사를 생략할 수 있기 때문에 대량의 데이터를 처리할 때 오버헤드가 줄어듭니다.
  • 스레드 동기화 Atomics와 함께 사용하여 메모리 접근 시 충돌을 방지하기 위한 동기화를 수행할 수 있습니다.

기본 사용 예시

 1// Create a 16-byte shared memory
 2const sharedBuffer = new SharedArrayBuffer(16);
 3
 4// Treat it as an Int32Array
 5const sharedArray = new Int32Array(sharedBuffer);
 6
 7// Set a value
 8sharedArray[0] = 42;
 9
10console.log(sharedArray[0]);  // 42

이 예제에서는 SharedArrayBuffer를 사용하여 16바이트 메모리 영역을 생성하고 해당 메모리 영역을 Int32Array로 취급합니다. 이 메모리 버퍼는 여러 스레드 간에 공유할 수 있습니다.

Web Workers와 함께 사용하기

SharedArrayBuffer의 진정한 가치는 Web Workers와 결합하여 사용할 때 나타납니다. 다음 코드는 메인 스레드와 워커 간에 공유 메모리를 사용하는 예제입니다.

메인 스레드에서

 1// Create a shared buffer
 2const sharedBuffer = new SharedArrayBuffer(16);
 3const sharedArray = new Int32Array(sharedBuffer);
 4
 5// Create a worker
 6const worker = new Worker('worker.js');
 7
 8// Pass the shared buffer to the worker
 9worker.postMessage(sharedBuffer);
10
11// Modify the memory
12// Output : Main thread: 100
13sharedArray[0] = 100;
14console.log("Main thread: ", sharedArray[0]);

워커 측에서 (worker.js)

 1// worker.js
 2self.onmessage = function(event) {
 3    // Use the received shared buffer
 4    const sharedArray = new Int32Array(event.data);
 5
 6    // Read the contents of the memory
 7    // Output : Worker thread: 100
 8    console.log("Worker thread: ", sharedArray[0]);
 9
10    // Change the value
11    sharedArray[0] = 200;
12};
  • 이 예제에서는 메인 스레드가 공유 버퍼를 생성하고 이를 워커에 전달합니다. 워커는 이 버퍼에 액세스하여 값을 읽고 수정할 수 있습니다. 이 방식으로 데이터를 복사 없이 스레드 간에 공유할 수 있습니다.

양방향 업데이트 확인

SharedArrayBuffer를 사용하면 메인 스레드와 워커 모두 동일한 메모리를 읽고 쓸 수 있어 양방향 업데이트 확인이 가능합니다. 아래는 메인 스레드가 값을 설정하고, 워커가 이 값을 변경한 뒤, 메인 스레드가 업데이트를 확인하는 예시입니다.

메인 스레드에서

 1// Create a shared buffer
 2const sharedBuffer = new SharedArrayBuffer(16);
 3const sharedArray = new Int32Array(sharedBuffer);
 4
 5// Create a worker
 6const worker = new Worker('worker.js');
 7
 8// Pass the shared buffer to the worker
 9worker.postMessage(sharedBuffer);
10
11// Set initial value
12// Output : Main thread initial: 100
13sharedArray[0] = 100;
14console.log("Main thread initial:", sharedArray[0]);
15
16// Listen for worker confirmation
17worker.onmessage = () => {
18    // Output : Main thread after worker update: 200
19    console.log("Main thread after worker update:", sharedArray[0]);
20};

워커 측에서 (worker.js)

 1// worker.js
 2self.onmessage = function(event) {
 3    const sharedArray = new Int32Array(event.data);
 4
 5    // Read initial value
 6    // Output : Worker thread received: 100
 7    console.log("Worker thread received:", sharedArray[0]);
 8
 9    // Update the value
10    sharedArray[0] = 200;
11
12    // Notify main thread
13    self.postMessage("Value updated");
14};
  • 이 예시에서 메인 스레드는 먼저 값 100을 쓰고, 워커가 이를 읽은 후 200으로 다시 씁니다. 그 후 워커가 메인 스레드에 알리고, 메인 스레드는 공유 메모리를 다시 읽어 업데이트를 확인합니다. 이처럼 알림을 결합함으로써 양방향 업데이트를 확인할 수 있습니다.

Atomics를 사용한 동기화

공유 메모리를 사용할 때는 데이터 경합 조건과 불일치에 주의해야 합니다. 다중 스레드가 동일한 메모리에 동시에 접근하면 충돌이 발생할 수 있습니다. 이를 방지하기 위해 JavaScript는 동기화를 위해 Atomics 객체를 사용합니다.

예를 들어, 다중 스레드에서 안전하게 카운터를 증가시키기 위해 Atomics를 사용하여 충돌을 방지할 수 있습니다.

1const sharedBuffer = new SharedArrayBuffer(16);
2const sharedArray = new Int32Array(sharedBuffer);
3
4// Increment the counter
5Atomics.add(sharedArray, 0, 1);
6
7console.log(Atomics.load(sharedArray, 0));  // 1

Atomics.add는 특정 인덱스의 값을 원자적으로 증가시키고 새 값을 반환합니다. 이 작업은 다른 스레드와의 충돌이 없음을 보장합니다. Atomics.load도 공유 메모리에서 값을 안전하게 읽는 데 사용됩니다.

Atomics.waitAtomics.notify를 사용한 대기 및 알림

SharedArrayBuffer를 사용할 때, 워커가 특정 조건이 충족될 때까지 대기하고, 다른 워커가 그 조건을 충족시키면 대기 중인 워커에게 알릴 필요가 있습니다. 이러한 경우 Atomics.waitAtomics.notify가 유용합니다.

Atomics.wait는 공유 메모리의 특정 인덱스 값이 변경될 때까지 스레드를 블록하고, Atomics.notify는 대기 중인 스레드에게 진행할 수 있음을 알립니다. 이를 통해 여러 워커 간에 안전한 대기 및 알림이 가능합니다. 하지만 Atomics.wait는 메인 스레드에서는 사용할 수 없고, 워커 내부에서만 사용 가능합니다.

 1// Create a shared buffer (1 Int32 slot is enough for signaling)
 2const sharedBuffer = new SharedArrayBuffer(4);
 3const sharedArray = new Int32Array(sharedBuffer);
 4
 5// Create workers with names
 6const waiter = new Worker('worker.js', { name: 'waiter' });
 7const notifier = new Worker('worker.js', { name: 'notifier' });
 8
 9// Pass the shared buffer to both
10waiter.postMessage(sharedBuffer);
11notifier.postMessage(sharedBuffer);
12
13// Listen for messages
14waiter.onmessage = (event) => {
15    console.log(`[Main] Message from waiter:`, event.data);
16};
17notifier.onmessage = (event) => {
18    console.log(`[Main] Message from notifier:`, event.data);
19};
  • 메인 스레드에서는 공유 메모리로 SharedArrayBuffer를 생성하고, 이를 하나의 요소만 가지는 Int32Array로 변환합니다. 이 하나의 정수 슬롯은 워커들 간의 동기화를 위한 신호로 사용됩니다. 다음으로 두 개의 워커를 생성하고, 각각 name 속성을 사용하여 역할을 부여합니다: waiter(대기 역할)와 notifier(알림 역할). 마지막으로 공유 버퍼를 두 워커에 모두 전달하고, 각 워커로부터 전송된 메시지를 받을 수 있도록 onmessage 핸들러를 설정합니다.

워커 측에서 (worker.js)

 1// worker.js
 2onmessage = (event) => {
 3    const sharedArray = new Int32Array(event.data);
 4
 5    if (self.name === 'waiter') {
 6        postMessage('Waiter is waiting...');
 7        // Wait until notifier signals index 0
 8        Atomics.wait(sharedArray, 0, 0);
 9        postMessage('Waiter was notified!');
10    }
11
12    if (self.name === 'notifier') {
13        postMessage('Notifier is preparing...');
14        setTimeout(() => {
15            // Notify waiter after 2 seconds
16            Atomics.store(sharedArray, 0, 1);
17            Atomics.notify(sharedArray, 0, 1);
18            postMessage('Notifier has sent the signal!');
19        }, 2000);
20    }
21};
22// Output
23// [Main] Message from waiter: Waiter is waiting...
24// [Main] Message from notifier: Notifier is preparing...
25// [Main] Message from notifier: Notifier has sent the signal!
26// [Main] Message from waiter: Waiter was notified!
  • 이 예제에서 waiter 워커는 인덱스 0의 값이 0인 동안 Atomics.wait를 사용해 대기 상태를 유지합니다. 반면, notifier 워커가 Atomics.store로 값을 123으로 변경하고 Atomics.notify를 호출하면, waiter 워커가 다시 시작되어 변경된 값을 얻을 수 있습니다. 이렇게 하면 스레드 간의 효율적이고 안전한 대기 및 알림을 구현할 수 있습니다.

SharedArrayBuffer의 사용 사례

SharedArrayBuffer는 특히 다음과 같은 사용 사례에 유용합니다:.

  • 실시간 처리 오디오, 비디오 처리나 게임 엔진처럼 낮은 지연 시간과 즉각적인 데이터 공유가 필요한 애플리케이션에 적합합니다.
  • 병렬 컴퓨팅 여러 스레드로 대량의 데이터를 동시에 처리할 때, SharedArrayBuffer를 사용하면 메모리 복사를 피하고 성능을 높일 수 있습니다.
  • 머신러닝 데이터 전처리나 추론과 같은 작업을 병렬화하여 효율적인 연산이 가능합니다.

보안 고려 사항

SharedArrayBuffer는 강력한 기능이지만 보안 위험도 수반합니다. 특히 Spectre와 같은 부채널 공격에 대한 우려로 인해 지원이 일시적으로 중단되었습니다. 이 취약점을 완화하기 위해 브라우저는 다음과 같은 조치를 구현했습니다:.

  • 사이트 격리 SharedArrayBuffer의 사용을 허용하는 사이트는 다른 사이트와 완전히 격리된 프로세스에서 실행됩니다.
  • 교차 출처 리소스 정책 SharedArrayBuffer를 사용하려면 Cross-Origin-Opener-PolicyCross-Origin-Embedder-Policy 헤더를 올바르게 설정해야 합니다.

예를 들어, 다음과 같은 헤더를 설정하면 SharedArrayBuffer 사용이 가능해집니다:.

1Cross-Origin-Opener-Policy: same-origin
2Cross-Origin-Embedder-Policy: require-corp

이는 외부 리소스가 현재 콘텐츠를 방해하지 못하도록 하여 보안을 강화합니다.

요약

SharedArrayBuffer는 다중 스레드 간 메모리를 공유하기 위한 매우 강력한 도구입니다. 이는 성능을 향상시키는 데 필수적인 기술이며, 특히 실시간 처리 및 병렬 컴퓨팅 분야에서 그 효과가 뚜렷하게 나타납니다. 그러나 보안 위험도 수반되므로 올바른 설정과 동기화가 중요합니다.

SharedArrayBuffer를 활용하면 더욱 고급스럽고 고성능의 웹 애플리케이션을 구축할 수 있습니다.

위의 기사를 보면서 Visual Studio Code를 사용해 우리 유튜브 채널에서 함께 따라할 수 있습니다. 유튜브 채널도 확인해 주세요.

YouTube Video