라봉이의 개발 블로그

single thread인 Node.js에서 thread 생성하기 1 - worker_thread 모듈 본문

Node.js

single thread인 Node.js에서 thread 생성하기 1 - worker_thread 모듈

Labhong 2019. 2. 16. 18:26
반응형

서론

Node.js는 싱글 스레드 이벤트 루프를 사용한다고 알려져있습니다. 

따라서 Node.js는 싱글 스레드(?)이다. 라고 잘못 아시는 분들도 종종 있구요.


하지만 Node.js는 이벤트 루프가 싱글 스레드에서 동작 한다는 것이지 내부적으로 스레드풀을 두어 I/O 작업에 스레드를 사용할 수 있도록 합니다. 이를 통해 병렬적으로 작업을 진행할 수 있는 것입니다. 


확인을 위해 간단하게 node.js의 http 모듈을 사용해 서버를 띄워보도록 하겠습니다.


간단한 서버 코드


동작시켰을 때 화면


그리고 ps -M 명령어를 사용하여 스레드의 개수를 확인할 수 있습니다. (Mac OS 환경)


총 7개의 내부 스레드가 있는 것을 확인할 수 있다.



위의 사진에서 보다시피 현재 간단한 Node.js 프로세스인데 총 7개의 스레드가 생겼다는 것을 볼 수 있습니다.

이는 node.js가 사용하는 libuv의 모듈은 내부적으로 thread pool을 두어 I/O 작업을 thead pool에 존재하는 thread를 사용해 처리하기 때문에 event loop는 Block 당하지 않고 빠르게 작업을 계속 진행할 수 있는 것입니다.


그러므로 Node.js는 싱글 스레드다 라는 말은 거짓입니다. 하지만 프로그래머가 처리할 수 있는 일반적인 javascript 코드들은 싱글 스레드인 이벤트 루프에서 작업하게 됩니다.

따라서 CPU 작업량이 많은 코드는 다른 자바스크립트 코드를 Block하게 만들어 다른 작업이 중단된 것처럼 보일 수 있습니다. 이는 치명적인 문제입니다.


하지만 슬퍼하긴 이릅니다. Node.js 버전 10.5부터 thread pool에 스레드를 프로그래머가 직접 생성할 수 있게 됐습니다!! 바로 worker_thread라는 모듈을 통해 스레드를 생성할 수 있습니다.

(해당 링크는 Node.js 릴리즈 노트. worker_thread가 추가된 것을 볼 수 있습니다.) 링크


주의할 점은 node를 실행시킬 때 다음 플래그를 삽입해야 한다는 것입니다.

--experimental-worker

이제 Node.js의 스레드를 이용하여 CPU hard한 작업들을 event loop에서 처리할 필요 없이 생성한 스레드에 할당하여 작업할 수 있게 됐습니다. 그러면 event loop는 CPU hard한 작업에서 벗어날 수 있습니다. 간단하게 살펴보죠.


테스트


테스트 내용은 

  • 작업은 반복문을 jobSize만큼 반복하는 것이다.

  • 1번 케이스는 이벤트 루프인 main thread와 새로운 스레드 2개, 총 3개의 스레드가 각각의 jobSize를 처리했을 때이다.

  • 2번 케이스는 main thread가 스레드 2개의 작업을 몰아서 처리하게 되는 경우이다. (case 1의  jobSize 3배)

case1)

index.js 파일

// index.js 파일
const { Worker } = require('worker_threads');
let startTime = process.uptime();           // 프로세스 시작 시간
let jobSize = 100000;
let myWorker1, myWorker2;

myWorker1 = new Worker(__dirname + '/worker.js');        // 스레드를 생성해 파일 절대경로를 통해 가리킨 js파일을 작업
myWorker2 = new Worker(__dirname + '/worker.js');

doSomething();  // event loop가 처리해야 할 CPU 하드한 작업

let endTime = process.uptime();
console.log("main thread time: " + (endTime - startTime));  // 스레드 생성 시간 + doSomething 처리하는 데 걸린 시간.

function doSomething() {
	let data;
	for (let i = 0; i < jobSize; i++) {      // CPU Hard
		data += i;
	}
}


worker.js 파일

//worker.js 파일
let startTime = process.uptime();           // 스레드 생성 시간.
const Worker = require('worker_threads');
let jobSize = 100000;

doSomething();      // 스레드가 처리해야 할 CPU 하드한 작업

let endTime = process.uptime();
console.log(Worker.threadId + " thread time: " + (endTime - startTime));

function doSomething() {
	let data;
	for (let i = 0; i < jobSize; i++) {      // CPU Hard
		data += i;
	}
}


결과


이번엔 스레드를 사용하지 않고 작업하게 되는 경우를 살펴봅시다.


case2) 

index.js 파일 (스레드 삭제 및 작업량을 3배로 함)

// index.js 파일
const { Worker } = require('worker_threads');
let startTime = process.uptime();           // 프로세스 시작 시간
let jobSize = 300000;                       // 이전 작업량의 3배
let myWorker1, myWorker2;

// myWorker1 = new Worker(__dirname + '/worker.js');
// myWorker2 = new Worker(__dirname + '/worker.js');

doSomething();  // event loop가 처리해야 할 CPU 하드한 작업

let endTime = process.uptime();
console.log("main thread time: " + (endTime - startTime));  // 스레드 생성 시간 + doSomething 처리하는 데 걸린 시간.

function doSomething() {
	let data;
	for (let i = 0; i < jobSize; i++) {      // CPU Hard
		data += i;
	}
}


결과


이게 어찌 된 일이죠?? 스레드를 생성했을 때 오히려 작업 속도가 더 떨어지는군요.

예상으로는 스레드를 생성하는 작업 자체가 부하가 크다는 것으로 생각해 볼 수 있습니다.

따라서 이번엔 작업량을 매우 크게 해보았습니다.


case1)

스레드를 사용했을 때 결과 (jobSize = 1000000000)


case2)

스레드를 사용하지 않고 메인 스레드에 몰아줬을 때 결과


이번엔 확실히 스레드를 사용했을 때 main thread에서 작업 속도가 3배 빠른 것을 볼 수 있었습니다.


결론

이번 테스트로 결과를 내보자면 다음과 같습니다.


1. Node.js에도 스레드를 생성할 수 있는 방법이 생겼다.

2. CPU hard한 작업이 아니면 스레드 생성때문에 오히려 처리 시간이 늘어날 수 있다.


마무리

사실 worker_thread 모듈은 더 많은 기능이 존재합니다. 예를 들어 각 thread는 메세지를 통해 데이터를 교환할 수 있습니다. 이번 포스트는 간단하게 Node.js도 스레드를 만들 수 있다라는 주제에 맞춰 작성했기 때문에 다음 작업은 worker_thread 모듈을 더 연구하여 다음 포스트에 전달하도록 하겠습니다.

반응형
Comments