본문으로 바로가기

javascript는 싱글 쓰레드인데 멀티 쓰레딩을 어떻게 하지?

 

HTML5 에는 Web Worker 스펙을 추가 하여 브라우저에서 멀티 쓰레드 구동이 가능하게 되었습니다.

 

사실 그간 웹 외에 일반 앱(데스크탑, 모바일 등등...)에서는 강제적으로 백그라운드 Thread를 사용하여 메인 Thread의 작업을 최대한 지양하도록 하고 있었는데 그것이 이제 웹에서도 가능해진 것이다. 결론은 웹에서도 이제 멀티 쓰레딩을 활용해야 한다~ 이겁니다.

 

javascript 자체는 싱글 쓰레드입니다. 그러나 자바스크립트의 주요한 런타임 환경 중 하나인 브라우저에서 HTML 5의 Web Worker 스펙을 지원하면서 브라우저 내에서 멀티 쓰레드를 활용할 수 있을 따름입니다.

 

자바스크립트의 또 다른 런타임인 Node.js (+Deno)에서는 CPU-intensive한 작업을 thread pool에 넘겨 작업을 하곤 했습니다. Node.js와 브라우저에서 둘 다 멀티 쓰레드를 활용할 수 있으니 사실상 '자바스크립트로 멀티 쓰레딩 할 수 있다!'라고 말할 수 있는거죠. 

 

* Node.js에서 CPU-intensive한 작업을 처리하기 위해 thread pool을 사용하는 것은 아래 포스트에 정리해뒀습니다.

(현재 비공개 ㅎ) 

darrengwon.tistory.com/954

 

일반적인 로직(사람이 직접 작성한 코드)은 모두 메인쓰레드에서 처리됩니다. 네트워크 요청이나 파일시스템 정도만 운영체제에서 멀티쓰레드로 처리해왔습니다. Node.js에서 비동기처리는 libuv의 thread pool이나 OS 커널단에서 처리되는 것 아시죠?darrengwon.tistory.com/953 여기 참고.

 

Row level Node : js의 동작 방식부터 libuv와 event loop까지

글을 시작함에 앞서, 본 포스트는 libuv가 C++으로만 작성되었다는 등의 잘못된 내용 등 일부를 수정하기는 하였으나 다른 분들의 포스트를 참고하였고, 그대로 가져다 쓴 부분도 적지 않습니다.

darrengwon.tistory.com

 

이렇게 런타임에서 멀티 쓰레딩을 사용하고 있다면, 일반적인 코드로도 필요할 때 멀티 쓰레딩을 이용할 수 있지 않을까? 라는 생각이 떠오릅니다. 굳이 파일 시스템과 같은 특정 로직이 아니라 일반적인 로직에서 멀티 쓰레딩의 이점을 누리고 싶다는 거죠.

 

* 종종 오해를 사는데, Worker를 써서 멀티 쓰레딩처럼 사용할 수는 있지만 1개의 Worker 자체는 여전히 싱글 쓰레드입니다. Worker에게 blocking 적인 명령 내리면 그 blocking 된 기간 동안 Worker도 먹통이 되는 건 여전합니다.

 

* Worker에서 DOM 접근은 불가능합니다. 애초에 Worker의 window 전역 객체와 main Thread의 window 전역 객체는 전혀 다른 것입니다. 

MDN을 인용하자면 전용 워커(단일 스크립트에서만 사용하는 워커)의 경우 DedicatedWorkerGlobalScope 객체이고,

공유 워커(여러 스크립트에서 공유하는 워커)의 경우 SharedWorkerGlobalScope 객체입니다.

 

* worker는 window 전역 객체와 전혀 별개의 것이라고 한 것에서 짐작할 수 있듯, worker에서는 접근 가능한 객체가 제한됩니다. window, document, console object, parent object, 등등.

 

* 이미 싱글쓰레드에 비동기만 잘 맞춰줘도 화면이 충분히 부드럽게 도는데 뭘 더 해야해? 그런거 안해도 잘 돌고 있고 우리 서비스가 하드코어한 CPU 연산을 시킬 것도 아닌데 굳이 해야하나

=> 맞는 말이고, 내가 이번에 만들고자 하는 서비스에서 필요해서 Worker를 공부하는 것일 뿐... 현재 대다수 웹은 Worker 없이도 잘 돌아가고 있기는 하다.

 

 

그렇다면 언제 이러한 멀티 쓰레딩을 사용하면 좋은가?

메인 Thread에서 blocking이 될 경도로 아주 오래 걸리는 내용을 Worker에게 맡기고, 연산이 완료되면 나중에 메인 Thread에 받아와 사용하면 되겠죠? 그러면 이런 오래 걸리는 일이 무엇이 있을까?

 

다른 분의 포스팅을 참고하니 아래와 같은 작업할 때 주로 사용한다고 한다.

 

• 매우 많은 문자열의 Encoding/Decoding

• 복잡한 수학 계산(소수prime numbers, 암호화 등)

• 매우 큰 배열의 정렬

• 네트워크를 통한 데이터 처리

• local storage 데이터 처리

• 이미지 처리• 비디오나 오디오 데이터 처리

• Background I/O

• 기타 백그라운드에서 오랜 시간 작업해야 하는 경우

• UI 쓰레드를 방해하지 않고 지속적으로 수행해야 하는 작업 (UI 쓰레드는 99% 메인 쓰레드죠.)

 

사실 위와 같은 경우 외에도 간단한 작업에서도 Worker를 사용해도 된다.

사파리 브라우저에서 지원을 안해서 문제지...

 

 

Worker에서 연산이 끝난 값을 어떻게 main Thread로 가져오는가?

=> 데이터 복사본 생성으로 인한 문제 발생. SharedArrayBuffer를 활용한 해결책과 보안 문제.

 

 

 

https://m.mkexdev.net/52

 

MDN을 그대로 인용해보겠습니다.

 

워커와 메인 스레드 간의 데이터 교환은 메시지 시스템을 사용합니다. 양측 모두 postMessage() 메서드를 사용해 전송하고, onmessage 이벤트 처리기(메시지는 Message data 속성에 들어있습니다)를 사용해 수신합니다. 전송하는 데이터는 복사하며 공유하지 않습니다.

 

결론적으로

 

데이터를 보낼 때는 postMessage() 메서드를

데이터를 받아올 때는 onmessage 이벤트를 리스닝해서 콜백 함수로 처리해주면 되겠습니다.

 

더 중요한 부분은 '전송하는 데이터는 복사하며 공유하지 않습니다' 라는 점입니다.

이로 인한 장점은 같은 데이터에 대한 경합 조건(race condition)이 없어 동기화(synchronize) 문제가 발생하지 않는다는 겁니다. 단점은, 큰 데이터를 주고 받기 위해 복사본을 생성하는 리소스 비용이 많이 들어간다는 겁니다.

 

그런데 많은 양의 데이터를 처리하는데 많은 시간이 걸려서 Worker 를 이용한 멀티 쓰레딩을 사용하는 것인데 이 값을 전송하는 데 드는 비용이 많이 소요된다면, Worker를 이용하는 근본적인 이유를 흔들게 됩니다. Worker를 사용함으로 인해 볼 수 있는 이득이 적다는 것이죠.

 

데이터 복사본 생성으로 인한 문제를 해결하기 위해 공유 메모리 방법이 제안되었으나 보안 문제로 브라우저에서 지원을 했다, 안했다를 반복하면서 동시에 Worker 기술도 잘 안 쓰이게 되었습니다. (훌쩍)

 

어떤 보안 문제가 있었느냐? 네이버 기술 블로그를 참고해보면 확인할 수 있습니다.

 

d2.naver.com/helloworld/7495331

 

메모리 공유에 SharedArrayBuffer 객체가 사용됩니다. SharedArrayBuffer 객체는 에이전트(메일 스레드, worker 등)가 데이터를 공유할 수 있게 해줍니다. 그런데 이 전역 객체의 사용이 CPU 설계 상의 보안 문제로 사용이 정지되었습니다.

 

그러나 CPU의 설계 오류로 인한 보안 취약점인 Meltdown과 Spectre는 2018년 초에 큰 보안 이슈가 됐다. 이 보안 취약점으로 인해 브라우저에서도 웹 콘텐츠에 포함될 수 있는 민감한 사용자 데이터(서로 다른 출처(origin) 간의 데이터 포함)가 노출될 가능성이 있다고 알려졌다.


이에 따라 대부분의 브라우저 제조사는 2018년 1월 5일을 기점으로 관련 패치가 적용되는 시점까지 SharedArrayBuffer 객체의 사용을 임시로 중단했다(이 글을 읽는 시점에 따라 SharedArrayBuffer 객체의 사용이 재개됐을 수도 있다).

 

 

 

Web Worker의 종류

 

Web Worker 는 2가지 타입이 존재한다.

  • Dedicated worker(전용 워커) : 생성한 worker 하나의 thread 가지는 모델. worker를 생성한 곳만 통신함.
  • Shared worker(공유 워커) : 여러 개의 worker 하나의 background process 공유하는 모델. 여러 상대와 통신을 할 수 있고, 이에 따라 port를 이용해야 함.

worker의 종류에 따라 사용 방법도 다르다.

  • 전용 워커
    • new Worker... 꼴로 생성합니다.
    • postMessage() method 이용한다.
    • 인자는 type 구별하지 않으며 JSON으로 serialize 되어 처리된다.
    • object 경우 call-by-value 방식으로 전달된다
    • .onmessage() event handler 사용하여 data 수신  처리한다.
  • 공유 워커
    • new SharedWorker... 꼴로 생성합니다.
    • ‘port’ 사용해야만 한다.
    •  번째 사용되는 postMessage() port method 사용해야 


출처: https://brian-doh.tistory.com/6

 

 

이론은 알겠는데 코드로 좀 구현해봐 : 전용 worker 구현

 

우선 브라우저에서 사용하는 것이므로 html에 script 태그를 삽입해서 진행해보도록하겠습니다.

 

1) 브라우저에서 worker를 지원하는지 체크. 모더나이저 참 오랜만에 써보네요.

// window.Worker가 아니라 !!인 이유는, boolean값을 만들기 위함.
if (!!window.Worker) {
  console.log("해당 브라우저는 worker를 지원합니다.");
} else {
  console.log("해당 브라우저는 worker를 지원하지 않습니다.");
}

// Modernizr를 쓰겠다면?
if (Modernizr.webworkers) {
  console.log("해당 브라우저는 worker를 지원합니다.");
} else {
  console.log("해당 브라우저는 worker를 지원하지 않습니다.");
}

 

2) worker를 쓰기 전, 특정 버튼을 누르면 innerText가 hello로 바뀌는 동작을 '즉시'하고 싶은데, 3초 blocking이 되는 코드가 있어 worker에게 이를 위임하고 싶다고 가정합시다.

if (Modernizr.webworkers) {
  console.log("해당 브라우저는 worker를 지원합니다.");

  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
  const button = document.querySelector(".button");

  button.addEventListener("click", async () => {
    await sleep(3000); // 무언가 3초가 걸리는 작업
    button.innerText = "hello"; // 즉시 반영되게 하고 싶은데 3초나 기다려야 함
  });
} else {
  alert("해당 브라우저는 worker를 지원하지 않습니다.");
}

 

 

3)

1. Worker 인스턴스를 생성한다.

2. message 이벤트에서 data를 받아오고, postMessage 메서드로 보내면 됩니다.

3. worker의 셀프 바인딩은 self를 사용합니다. this를 사용해도 되지만 호출 주체가 누구인지에 따라 this가 달라질 수 있으므로 self를 권장합니다.

 

* 크롬에서는 로컬 파일이 워커를 생성하는 것을 금지하기 때문에 안된다고 한다. 필자는 brave 브라우저를 쓰고 있는데 잘 된다.

 

if (Modernizr.webworkers) {
  console.log("해당 브라우저는 worker를 지원합니다.");

  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
  const button = document.querySelector(".button");

  button.addEventListener("click", async () => {
    const worker = new Worker("./worker.js");

    // worker로부터 data를 받기 위함
    worker.addEventListener("message", (event) => {
      console.log(event);
      console.log(event.data); // worker가 보낸 값이 들어 있음
      button.innerText = event.data;
    });

    // worker에게 일을 시킵니다.
    worker.postMessage("워커가 일을 시작합니다.");
    console.log("Message posted to worker");
  });
} else {
  alert("해당 브라우저는 worker를 지원하지 않습니다.");
}
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

self.addEventListener("message", async (event) => {
  console.log('Worker: Posting message back to main script');
  console.log(event.data); // 워커가 일을 시작합니다. 문자열 출력

  sleep(3000); // 3초가 걸리는 일

  const word = "hello";
  self.postMessage(word);
});

 

 

Shared Worker 

모더나이저에서는 SharedWorker 감지를 지원하지 않습니다. 그만큼 비주류 기술...

!!window.SharedWorker 꼴로 감지하시면 되겠습니다.

 

MDN에서 제공하는 예시로 갈음합니다.

 

전용 워커 예시

github.com/mdn/simple-web-worker

 

공유 워커 예시

github.com/mdn/simple-shared-worker

 

 

최신 브라우저의 내부 살펴보기 1~4 정독 추천. 쩐다

 

https://developer.mozilla.org/ko/docs/Web/API/Web_Workers_API

https://www.zerocho.com/category/HTML&DOM/post/5a85672158a199001b42ed9c

https://www.bsidesoft.com/8167

https://m.mkexdev.net/52

 

https://developer.mozilla.org/ko/docs/Web/API/Web_Workers_API

 

Web Workers API - Web API | MDN

웹 워커(Web worker)는 스크립트 연산을 웹 어플리케이션의 주 실행 스레드와 분리된 별도의 백그라운드 스레드에서 실행할 수 있는 기술입니다. 웹 워커를 통해 무거운 작업을 분리된 스레드에서

developer.mozilla.org

 


댓글을 달아 주세요

darren, dev blog
블로그 이미지 DarrenKwonDev 님의 블로그
VISITOR 오늘128 / 전체745,332