JS 엔진은 하나의 콜 스택을 사용하는 싱글 스레드 구조이지만, 실제 런타임에는 브라우저나 Node API를 통해 멀티 스레딩을 이용할 수 있다.
아래는 기본 엔진과 worker_threads
모듈을 사용하는 경우의 프로세스-스레드에 대한 시각화 자료이다. 모듈 기반으로 워커(스레드)들을 생성하고 필요한 작업을 병렬로 처리할 수 있다.
스레드 간에 데이터를 전달하는 방식은 크게 두 가지가 있다.
다른 스레드 간 메세지를 주고 받을 수 있는 .postMessage(data)
const { Worker } = require('worker_threads');
const worker = new Worker(`
const { parentPort } = require('worker_threads');
parentPort.once('message',
message => parentPort.postMessage({ pong: message }));
`, { eval: true });
worker.on('message', message => console.log(message));
worker.postMessage('ping');
$ node --experimental-worker test.js
{ pong: ‘ping’ }
✉️ 메인 스레드인 parentPort worker.postMessage('ping')
✉️ worker parentPort.postMessage({ pong: message })
생성자 인자를 통해 데이터를 전달할 수 있는 { workerData: data }
const { Worker, isMainThread, workerData } = require('node:worker_threads');
if (isMainThread) {
const worker = new Worker(__filename, { workerData: 'Hello, world!' });
} else {
console.log(workerData); // Prints 'Hello, world!'.
}
An arbitrary JavaScript value that contains a clone of the data passed to this thread's Worker constructor. The data is cloned as if using postMessage(), according to the HTML structured clone algorithm.
공식문서에서는 두 방식 모두 a clone of the data를 전달한다고 설명한다. 자바스크립트의 메모리 관리 방식에 그 이유가 있다.
memory-managed language
자바스크립트는 메모리를 엔진이 알아서 관리해주는 언어이다. 프로그래머가 메모리에 직접 접근할 필요가 없다. 변수를 할당하면, 중간에 있는 모자 쓴 엔진이 binary representation으로의 변환과 메모리 할당을 담당한다. 쓰이지 않는 메모리는 garbage collection도 해준다. 메모리 관련된 동작이 추상화되어 사람 입장에서는 편리하다.
manually managed language
반면 C 같은 언어들은 프로그래머가 직접 메모리를 관리한다. malloc
, free
그리고 포인터 사용이 그 예시다. 관리가 까다롭지만 dereference *
reference &
를 사용한 더 정교한 접근이 가능하기도 하다.
그렇다면 자바스크립트에서 멀티 스레드를 사용할 때 변수값이 아닌 변수가 저장된 메모리에 접근하고 싶다면 어떻게 해야할까?
예를 들어 무거운 데이터 여러 세트를 스레드로 나누어 각각 프로세싱하고, 그 결과를 특징에 따라서 메인 스레드의 다양한 변수에 반영하고 싶다면?
엔진이 다 떠먹여주기 때문에 우리는 메모리에 대한 정보가 없고 선언한 변수명을 통해 값을 알 수 있을 뿐이다. postMessage
를 통해 자식 스레드로 보낼 수 있는게 값 뿐이라는 의미이다. 물론 메세지 기반으로 메인 스레드 쪽으로 일부 로드를 넘겨주어도 되지만, 결국 메모리의 값을 수정할 수 있는 건 메인 스레드 뿐이라면 스레드 간 역할이 제대로 분리되지 않을 수 있다.
이럴 때 사용할 수 있는 것이 SharedArrayBuffer
와 Atomics
이다.
ArrayBuffer
바이트로 구성된 배열. 정보를 직접 수정하는 것은 불가능하지만 TypedArray 또는 Dataview 객체를 통해 뷰를 만들어 내용을 읽거나 쓸 수 있다. 이 뷰가 자바스크립트에서도 메모리에 직접 접근할 수 있는 인터페이스가 된다.
const buffer = new ArrayBuffer(4);
const view = new Int8Array(buffer);
view[0] = 2
ArrayBuffer { [Uint8Contents]: <02 00 00 00>, byteLength: 4 }
Int8Array(4) [ 2, 0, 0, 0 ]
찍어보면 이렇게 나온다. buffer
크기는 4byte로 선언했고 Int8===1byte라서 view
는 아래 사진과 같이 네 칸으로 끊긴다. 각각의 인덱스는 하나의 Int8 자료형처럼 사용될 수 있다.
SharedArrayBuffer
공유된 메모리상의 뷰를 생성하는데 사용. ArrayBuffer
와 사용법은 같지만 SharedArrayBuffer
위에 만들어진 뷰는 다른 스레드에서도 사용할 수 있다. 이를 활용해 스레드들이 같은 메모리 공간을 읽고 쓸 수 있게 된다.
위에서 소개했던 두 가지 데이터 전달방식—worker.postMessage(view)
{ workerData: view }
—을 통해 이 뷰를 전달하고, 공유 메모리에 접근할 수 있다.
➕동시성 문제와 Atomics
메모리를 공유하면 여러 스레드가 동시에 해당 메모리를 읽고 쓰는 상황이 생기고 예측과 다른 결과가 나올 수 있다. 접근을 동기적으로 처리하기 위해서 내장객체 Atomics
가 사용된다.
// worker
const buffer = new SharedArrayBuffer(4);
const view = new Int8Array(buffer);
view[0] = 2
const { Worker } = require('worker_threads');
const worker = new Worker(`
const { parentPort } = require('worker_threads');
parentPort.on('message', view => {
Atomics.add(view, 0, 2)
Atomics.store(view, 3, 2)
parentPort.postMessage(0)
});
`, { eval: true });
console.log(view);
worker.postMessage(view);
worker.on('message', () => {
worker.terminate()
console.log(view)
});
Int8Array(4) [ 2, 0, 0, 0 ]
Int8Array(4) [ 4, 0, 0, 2 ]
이렇게 자식 스레드에서 직접 변수값을 변경할 수 있다. Atomics | MDN 에 더 많은 메서드가 정의되어있다.
This is why you don’t want to use SharedArrayBuffers and Atomics in your application code directly. Instead, you should depend on proven libraries by developers who are experienced with multithreading, and who have spent time studying the memory model.
It is still early days for SharedArrayBuffer and Atomics. Those libraries haven’t been created yet. But these new APIs provide the basic foundation to build on top of.
2017년 기준 비교적 새로 생긴 API들이라서 아직 검증된 라이브러리는 부족하다고 한다. npm에서 관련 모듈을 찾아봐도 많지 않고 weekly downloads도 최대 100명대(web-locks
)였다.
웹에서 멀티 스레드를 사용하면 좋은 상황에 대해서는 더 알아봐야겠지만, 그래도 앞으로 얘네 기반으로 JS에서 멀티 스레딩을 더 효율적으로 사용할 수 있는 방법들이 나오지 않을까 🤔
References
nodejs worker_threads
Understanding Worker Threads in Node.js
How to work with worker threads in NodeJS
SharedArrayBuffer | MDN
A cartoon intro to ArrayBuffers and SharedArrayBuffers
설명이 너무 좋아요!