자바스크립트에서 멀티쓰레드를 사용할 땐, 쓰레드 간 메세지 패싱 패턴을 주로 사용합니다.
보통 간단한 문자열을 이용해 특정 함수를 호출하는 방식으로 구현하지만,
애플리케이션의 규모가 커지면 좀 더 좋은 방법이 필요하게 됩니다.
이를 위한 패턴들을 공부해 봅니다.
The RPC Pattern
worker.postMessage('square_sum|num:4');
worker.postMessage('fibonacci|num:33');
worker.onmessage = (result) => {
// Which result belongs to which message?
// '3524578'
// 4.1462643
};
이를 위한JSON-RPC 표준이 있습니다.
이 표준은 요청 및 응답 개체의 JSON 표현을 notification 객체:
호출되는 메서드 및 해당 메서드의 인수, 응답의 결과, 요청과 응답을 연결하는 메커니즘을 정의하는 객체로 정의합니다.
오류 값과 요청 일괄 처리도 지원합니다.
이 예에서는 요청 및 응답만 사용합니다.
예제에서 두 개의 함수 호출을 취하면 해당 요청 및 응답의 JSON-RPC 버전은
아래와 같을 수 있습니다.
// worker.postMessage
{"jsonrpc": "2.0", "method": "square_sum", "params": [4], "id": 1}
{"jsonrpc": "2.0", "method": "fibonacci", "params": [33], "id": 2}
// worker.onmessage
{"jsonrpc": "2.0", "result": "3524578", "id": 2}
{"jsonrpc": "2.0", "result": 4.1462643, "id": 1}
The Command Dispatcher Pattern
const commands = { // 지원되는 모든 명령의 정의.
square_sum(max) {
let sum = 0;
for (let i = 0; i < max; i++) sum += Math.sqrt(i);
return sum;
},
fibonacci(limit) {
let prev = 1n, next = 0n, swap;
while (limit) {
swap = prev; prev = prev + next;
next = swap; limit--;
}
return String(next);
}
};
function dispatch(method, args) {
if (commands.hasOwnProperty(method)) { // 명령이 있는지 확인합니다.
return commands[method](...args); // 인수를 확산하여 메서드를 호출합니다.
}
throw new TypeError(`Command ${method} not defined!`);
}
commands.hasOwnProperty()를 사용하는 이유는,
. __proto__ 와 같은 명령이 아닌 속성이 호출되는 것을 원하지 않기 때문입니다.
method in commands, commands[method] 보다 훨씬 안전합니다.
응용
RpcWorker 클래스에서 다음을 수행합니다.
- 작업자 스레드 인스턴스화
- 요청 Json 객체 생성
- 메세지 ID를 이용해 요청과 응답 매핑
https://github.com/MultithreadedJSBook/code-samples/blob/main/ch2-patterns/rpc-worker.js
class RpcWorker {
constructor(path) {
this.next_command_id = 0;
this.in_flight_commands = new Map();
this.worker = new Worker(path);
this.worker.onmessage = this.onMessageHandler.bind(this);
}
// THIS LINE SHOULD NOT APPEAR IN PRINT
onMessageHandler(msg) {
const { result, error, id } = msg.data;
const { resolve, reject } = this.in_flight_commands.get(id);
this.in_flight_commands.delete(id);
if (error) reject(error);
else resolve(result);
}
// THIS LINE SHOULD NOT APPEAR IN PRINT
exec(method, ...args) {
const id = ++this.next_command_id;
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
this.in_flight_commands.set(id, { resolve, reject });
this.worker.postMessage({ method, params: args, id });
return promise;
}
}
worker.js 클래스에서 실제 작업자 쓰레드를 구현합니다.
해당 작업자 쓰레드가 처리할 수 있는 커맨드와
커맨드에 응답하는 방법을 정의합니다.
https://github.com/MultithreadedJSBook/code-samples/blob/main/ch2-patterns/worker.js
const sleep = (ms) => new Promise((res) => setTimeout(res, ms)); // <1>
function asyncOnMessageWrap(fn) { // <2>
return async function(msg) {
postMessage(await fn(msg.data));
}
}
const commands = {
async square_sum(max) {
await sleep(Math.random() * 100); // <3>
let sum = 0; for (let i = 0; i < max; i++) sum += Math.sqrt(i);
return sum;
},
async fibonacci(limit) {
await sleep(Math.random() * 100);
let prev = 1n, next = 0n, swap;
while (limit) { swap = prev; prev = prev + next; next = swap; limit--; }
return String(next); // <4>
},
async bad() {
await sleep(Math.random() * 10);
throw new Error('oh no');
}
};
self.onmessage = asyncOnMessageWrap(async (rpc) => { // <5>
const { method, params, id } = rpc;
if (commands.hasOwnProperty(method)) {
try {
const result = await commands[method](...params);
return { id, result }; // <6>
} catch (err) {
return { id, error: { code: -32000, message: err.message }};
}
} else {
return { // <7>
id, error: {
code: -32601,
message: `method ${method} not found`
}
};
}
});
이와 같은 rpc와 command dispatcher 패턴은,
json을 인코딩 수단으로 사용하는
네이티브 - 웹간 브릿지 통신에도 응용할 수 있습니다.
참고
Multithreaded JavaScript
'FrontEnd' 카테고리의 다른 글
[Vue3] 2023년, Vue3은 어떻게 달라질 것인가? (0) | 2023.01.29 |
---|---|
리액트 이벤트 리스너는 어떻게 등록되고 처리되는가 (0) | 2023.01.28 |
typescript(타입스크립트)의 satisfies 연산자 제대로 알아보기 (0) | 2023.01.26 |
리덕스는 왜 단일 스토어를 사용하는가? (0) | 2023.01.26 |
Javascript 멀티쓰레딩 : 언제 멀티쓰레딩을 사용해야 할까 (0) | 2023.01.24 |