본문 바로가기

FrontEnd

멀티쓰레드 자바스크립트 : 메세지 패싱 추상화 패턴

반응형

자바스크립트에서 멀티쓰레드를 사용할 땐, 쓰레드 간 메세지 패싱 패턴을 주로 사용합니다.

보통 간단한 문자열을 이용해 특정 함수를 호출하는 방식으로 구현하지만,

애플리케이션의 규모가 커지면 좀 더 좋은 방법이 필요하게 됩니다.

이를 위한 패턴들을 공부해 봅니다.

javascript

The RPC Pattern

RPC(원격 프로시저 호출) 패턴은 함수의 표현과 해당 함수의 인수를 직렬화한 뒤, 원격 타겟으로 전달하여 실행되도록 하는 방법입니다.
ex) square_sum|num:1000000
 
궁극적으로 squareNum(1000000)과 같은 함수 호출로 변환될 수 있습니다.
 
메인 스레드가 웹 작업자에게 한 번에 하나의 메시지만 보내는 경우 웹 작업자에서 메시지가 반환되면
해당 메시지에 대한 응답임을 쉽게 알 수 있습니다.
하지만 웹 작업자에게 동시에 여러 메시지를 보내는 경우 응답과 메세지를 매핑하는 것이 쉽지 않을 수 있습니다.
웹 작업자에게 두 개의 메시지를 보내고 두 개의 응답을 받는 애플리케이션을 상상해 보세요.
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}
이 경우 이제 응답 메시지와 해당 요청 간에 명확한 상관 관계가 있습니다.
JSON-RPC는 특히 네트워크를 통해 메시지를 보내기 위해 메시지를 직렬화할 때 JSON을 인코딩으로 사용하기 위한 것입니다.
실제로 이러한 jsonrpc 필드는 메시지가 준수하는 JSON-RPC 버전을 정의하며
이는 네트워크 설정에서 매우 중요합니다.
 
웹 작업자는 JSON 호환 개체를 전달할 수 있는 구조화된 복제 알고리즘(Structured_clone_algorithm)을 사용하기 때문에
앱은 JSON 직렬화 및 역직렬화 비용을 지불하지 않고 객체를 직접 전달할 수 있습니다.
 jsonrpc 필드는 통신 채널의 양쪽 끝을 더 엄격하게 제어할 수 있는 브라우저에선 중요하지 않을 수 있습니다.
 
객체의 id 필드를 이용하여 요청과 응답 객체를 연관지을 수 있습니다.

The Command Dispatcher Pattern

RPC 패턴은 프로토콜을 정의하는 데 유용하지만
해당 메세지를 수신하는 측에서 실행할 코드 위치를 결정하는 메커니즘을 제공하지는 않습니다.
커맨드 디스패처 패턴은 직렬화된 커맨드를 취하고 적절한 함수를 찾은 다음 실행하고
선택적으로 인수를 전달하는 방법을 제공합니다.
 
이 패턴은 구현하기가 매우 간단합니다.
리덕스랑 비슷합니다.
 
첫째, 코드를 실행해야 하는 메서드나 명령에 대한 관련 정보를 포함하는 두 개의 변수가 있다고 가정할 수 있습니다.
첫 번째 변수는 메서드라고 하며 문자열입니다.
두 번째 변수는 args라고 하며 메서드에 전달할 값의 배열입니다.
응용 프로그램의 RPC 계층에서 가져온 것으로 가정합니다.
또 다른 중요한 개념은 정의된 명령만 실행해야 한다는 것입니다.
호출자가 존재하지 않는 메서드를 호출하려는 경우 웹 작업자를 충돌시키지 않고 호출자에게 반환할 수 있는 오류가 정상적으로 생성되어야 합니다.
그리고 인수를 배열로 메서드에 전달할 수 있지만 인수 배열을 일반 함수 인수로 스프레드 할 수 있으면
훨씬 더 좋은 인터페이스가 됩니다.
 
애플리케이션에서 사용할 수 있는 커맨드 디스패처(명령 발송자)의 구현 예입니다.
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

 

Multithreaded JavaScript

Traditionally, JavaScript has been a single-threaded language. Nearly all online forum posts, books, online documentation, and libraries refer to the language as single threaded. Thanks to recent advancements in the … - Selection from Multithreaded JavaS

www.oreilly.com

 

반응형