NodeJS 에서 멀티 쓰레딩 사용을 위한 worker_threads 사용 방법 - Whitmem
NodeJS 에서 멀티 쓰레딩 사용을 위한 worker_threads 사용 방법
Web Development
2025-04-16 21:08 게시 89e1565e145a8381bdc2

0
0
28
이 페이지는 외부 공간에 무단 복제할 수 없으며 오직 있는 그대로 게시되며 부정확한 내용을 포함할 수 있습니다. 법률이 허용하는 한 가이드 라인에 맞춰 게시 내용을 인용하거나 출처로 표기할 수 있습니다.
This page is not to be distributed to external services; it is provided as is and may contain inaccuracies.
NodeJS 에서 멀티 쓰레딩 사용을 위한 worker_threads
기본적으로 NodeJS 는 브라우저와 동일하게 싱글 쓰레드에서 수행된다. 결과적으로 NodeJS 및 내장 엔진도 다른 언어 (예를 들자면 C++로 컴파일된)로 작성된 것이고, 그 엔진에서 코드를 처리할 때 단일 쓰레드에서 수행하기 때문에, 결과적으로 JS 에서 아무리 꼼수를 쓰더라도 단일 쓰레드에서만 처리된다.
그러나 다양한 프로그램을 개발해야 하는 상황에서 멀티 쓰레드를 사용 안하기란 어려운데, 보통 브라우저의 경우는 워커라는 것을 제공하고, NodeJS 에서는 내장 모듈로 worker_threads 를 제공한다. 우리는 이를 사용해서 NodeJS 에서 병렬로 계산을 처리하고 사용할 수 있다.
한편 Worker 은 기본적으로 메모리 공간을 공유하지는 않기 때문에, 다른 언어의 멀티 쓰레드와는 달리 별도의 공유 공간 선언 등 작업이 필요한데, 이 부분은 후술할 내용을 참고하길 바란다.
당연하겠지만 상기 코드가 메인 시작점인 app.js에 있다고 가정하자. 상기 for 문이 끝날 때까지 B는 실행되지 아니한다. 이는 메인 app.js 가 메인 단일 쓰레드에서 실행되고 있기 때문에 중간 명령이 끝날 때 까지 다음 작업이 대기 상태에 놓인다. 물론 파일을 읽고 다운로드하는 등 단순 처리를 수행하는 경우에는 얼마가 걸리든 기다리면 되겠지만, 서버처럼 사용자 응답을 24시간 처리하고 수행해야 하는 경우에는 예기치 않은 문제가 발생할 수 있다.
예를 들어 상기 슈도 코드에서, 사용자 응답을 받기 위한 서버를 처리한다고 가정하자. 3000번 포트로 서버를 개방하고, 사용자를 수신받기 위해 while 하기 전에 다운로드를 한다면, 서버는 전역적으로 멈출 것이다. 다운로드가 완료되면 사용자 수신을 시작할 것이고, 사용자 수신을 받는 도중에 다시 쓰레드가 대기 상태에 들어가거나 멈추는 경우 다른 사용자의 접근이 모두 일시적으로 멈출 것이다.
물론 위 상황은 극단적인 예시일 뿐이다. 보통 멀티 쓰레드를 직접 호출해 사용하지 않더라도, 이벤트 기반 작업을 통해 분산 처리하기도 한다. 하지만 근본적으로 컴퓨터 명령어는 시간을 쪼개면 쪼갤 수록 완전한 동시는 없기 때문에, 순차적으로 실행하되 어떻게 해야 서로 간의 영향을 최소화할 수 있는지 고민해야 한다.
아무튼 다시 원론으로 돌아와, 작업을 무한히 수행하는 for문이 있다고 가정하자. 이 for문을 메인에서 호출해서 실행해 본다.
child.js
메인
파일은 달라도 기본적으로 require 을 하면 메인 쓰레드에 가져와 수행하기 때문에 여전히 쓰레드가 멈춘다. 따라서 우리는 NodeJS 모듈에서 제공되는 worker_threads를 호출하여 Worker 을 선언하고, Worker 인스턴스를 생성하면 된다.
이 때 Worker 생성자에서는 인자 하나를 요구하는데, 어떤 js 파일을 워커로 실행할 것인지 경로를 지정 해 주면 된다.
놀랍게도! A, B 가 바로 출력되는 것을 볼 수 있다. 내부적으로는 멀티 쓰레드 공간에서 child.js가 실행 중인 것이다. child.js 에 console.log 로 아웃풋을 뿌려주면, A, B가 출력된 다음 한참 이따가 완료가 출력되는 것을 확인할 수 있다.
child.js
데이터 전달하기
한편 child.js 는 별개의 공간이기 때문에 메모리 정보가 공유되지 않는다. 따라서 작업 자에게 어떤 요청을 보낼 때 인자 정보를 같이 전송해야 한다면, workerData 를 사용하면 된다. workerData 에 넘겨야 할 데이터 정보를 딕셔너리 형태나 맵 형태로 넘기면 된다.
child.js
메인
메인에서 Worker 인스턴스를 생성할 때 두 번째 인자 공간에 딕셔너리로 옵션 사항을 담아줄 수 있는데, 옵션 중에 workerData 라는 키 값을 가진 데이터 객체를 넘겨주면 된다. 여기에 작성하는 데이터는 복사되어 전송되기 때문에 기존 정보를 참조할 수는 없다고 한다. 즉 시리얼라이징 할 수 없는 복잡한 구조의 인스턴스나, 객체 정보는 넘길 수 없는 듯 하다.
위는 workerData 에 {"key":"value"} 정보를 담아 워커를 생성하고, child.js 에서는 다시 workerData 를 받아 이 workerData 를 출력하는 모습을 나타낸 것이다. child.js 에서 처리한 결과를 메인에서 받아보기 위해서는 메인에서 이 worker 에 대해 응답 이벤트를 등록하고, child.js 에서는 이벤트를 post 해야 한다.
child.js
메인
즉 메인 Worker 을 생성하고 난 뒤 on 메서드를 사용해 message 수신 이벤트를 등록한다. Worker 에서 어떤 응답이 생기면 람다 함수를 통해 (e) 파라메터로 결과를 받아볼 수 있다.
child.js 에서 결과를 반환하기 위해서는, worker_threads 내의 parentPort를 통해 이 Worker 을 수행한 부모에게 메시지를 보내야 한다. child.js 에서 parentPort 를 선언한 뒤에 postMessage 메서드를 통해 결과를 반환하면, 메인 Worker을 호출 한 측에서 이벤트를 수신할 수 있다.
예시
아래 예시는 두 인자를 더해서 반환하는 child.js 를 Worker로 실행하는 방법을 나타낸 것이다.
child.js
메인
위 코드는 a 와 b 정보가 담긴 workerData 를 실행하려는 Worker 측에 보내고, 실행 대상인 child.js 는 이 workerData 를 받아 연산을 멀티 쓰레드에서 수행한다. 그리고 그 결과를 부모 경로인 parentPort 를 통해 이벤트로 발송한다. 메인 Worker 호출 측은 이 이벤트 메시지를 수신하여 결과를 출력한다.
메모리 공간 공유
하지만 위 방법은 한계가 있는데, 보통 멀티 쓰레드를 사용할 만큼 최적화를 중시하고, 프로젝트가 거대한 경우 간단한 덧셈 뺄셈과 같은... 유치한 용도의 연산이 아니라, 다양한 메모리 데이터를 결합하고 처리하는 대규모 연산일 경우가 대부분이다.
보통 이런 경우는 대량의 메모리 데이터를 포함하고 있기 때문에, 이 데이터를 복제하여 전송하고, 복제하여 반환하다가는 오버 헤드가 몇 배 증가할 수 있다. 따라서 공유 메모리 공간을 사용하여 오버헤드를 최소화하고 효율적으로 처리할 필요가 있다.
자바 스크립트에서 사용할 수 있는 공유 메모리 클래스로 SharedArrayBuffer 가 존재한다. 기본적인 Int32Array 등을 사용하기 전에 이 공유 배열 버퍼에 공간을 할당하고 이 정보를 workerData 등에 넘기면 참조되는 상태로 넘어가기 때문에, 오버 헤드를 최소화할 수 있다.
물론 메모리 참조 정보를 넘기는 만큼 배열 정보를 직접 다뤄야하고, 사용하기 쉽지는 않다. 바이트 개념을 자세히 알고 있으면 그다지 그렇게 어렵지는 않다.
메모리 생성하기
먼저 공유 배열 버퍼를 생성해야 한다. SharedArrayBuffer 인스턴스를 생성하면 되는데, 이 인스턴스를 생성할 때 메모리 공간 크기도 지정해야 한다.
메모리 공간 크기는 바이트 단위로 기입 해 넣으면 된다. 그런 다음 이 메모리 공간을 조작하기 위해 DataView 객체로 참조한다.
위 데이터 뷰는 공유 메모리 공간인 SharedArrayBuffer을 사용한다. 이 공간은 총 12바이트의 공유 메모리로 할당되어 있으므로 32비트짜리 정수를 3개 넣을 수 있다. setInt32(bytesOffset, IntegerValue) 메서드를 사용하여 0BytesOffset, 4ByteOffset, 8ByteOffset 에 차례로 정수 4, 1, 0을 기입한다.
위 DataView 로 데이터를 조작하면 참조 대상인 원본 SharedArrayBuffer의 메모리가 변경된다. 이 메모리 원본 객체를 workerData 에 담아 전달하면 된다.
이렇게 workerData 에 sharedArrayBuffer 을 전달하면 안의 내용이 복사되지 않고 참조 정보가 전달되어, child.js 에서 해당 참조 객체를 받을 수 있게 된다.
그러면 child.js 측은 workerData 를 통해 buffer 객체를 받고, 이 안에서 다시 DataView를 생성하여 버퍼를 참조한다. Main app.js 에서 04, 14, 2*4 위치에 정수 데이터를 쓴 공유 메모리를 받은 것이기 때문에, 0번 bytesOffset, 4번 bytesOffset의 Int32 를 조회하여 덧셈하고, 이 결과를 8번 bytesOffset에 반영하여 메인에는 성공 여부만 이벤트 메시지로 전송할 수 있다.
기본적으로 Worker 를 호출한 메인 js에서는 child.js 의 작업이 완료됐는지 여부를 자동으로 확인할 수 없기 때문에 child.js 에서 특정 작업이 완료된 경우 부모에 이벤트 메시지를 전송하여 child.js 의 작업 완료 여부를 전달해야 한다. 여기서는 딕셔너리 정보에 성공 여부를 담아서 부모에게 전송하였다.
그러면 부모 측에서는 message 이벤트를 통해 작업이 완료됐다는 메시지를 수신하고, 기존 DataView 객체를 통해 결과 영역 8번 bytesOffset 정보를 가져온다. DataView가 참조하는 SharedArrayBuffer 메모리 공간은 두 js 동일하게 사용했기 때문에 동일한 메모리 주소를 참조하며, 데이터는 공유된다.
child.js
메인
두 js 파일을 정리하면 위와 같다.
즉 결과적으로, Worker 는 메모리 정보가 실시간으로 공유되지는 않기 때문에 workerData를 통해 데이터를 넘길 때 복사됨에 유의한다.
댓글 0개
댓글은 일회용 패스워드가 발급되며 사이트 이용 약관에 동의로 간주됩니다.
확인
Whitmemit 개인 일지 블로그는 개인이 운영하는 정보 공유 공간으로 사용자의 민감한 개인 정보를 직접 요구하거나 요청하지 않습니다. 기본적인 사이트 방문시 처리되는 처리 정보에 대해서는 '사이트 처리 방침'을 참고하십시오. 추가적인 기능의 제공을 위하여 쿠키 정보를 사용하고 있습니다. Whitmemit 에서 처리하는 정보는 식별 용도로 사용되며 기타 글꼴 및 폰트 라이브러리에서 쿠키 정보를 사용할 수 있습니다.
이 자료는 모두 필수 자료로 간주되며, 사이트 이용을 하거나, 탐색하는 경우 동의로 간주합니다.