Node.js - 기본 개념 (2): 멀티스레드
이 포스트는 노드가 멀티 스레드와 관련된 기능에 대해 알아본다.
소스는 assu10/nodejs.git 에 있습니다.
worker_threads
child_process
- 기타 모듈들
4.7. worker_threads
4.7.1 간단한 worker_threads 사용 방식
아래는 노드에서 멀티 스레드 방식으로 작업하는 간단한 예시이다.
worker_threads.js
const { Worker, isMainThread, parentPort } = require('worker_threads');
if (isMainThread) {
// 부모인 경우
const worker = new Worker(__filename);
worker.on('message', message => console.log('from worker', message));
worker.on('exit', () => console.log('worker exit'));
worker.postMessage('ping');
} else {
// 워커인 경우
parentPort.on('message', value => {
console.log('from parent', value);
parentPort.postMessage('pong');
parentPort.close();
});
}
from parent ping
from worker pong
worker exit
기존에 동작하던 싱글 스레드를 메인 스레드 혹은 부모 스레드라고 하고, 생성한 스레드를 워커 스레드라고 한다.
위 코드를 설명하면 아래와 같다.
- 부모에서 워커 생성 후
worker.postMessage
로 워커에게 데이터 전달 - 워커는
parentPort.on('message')
이벤트 리스너로 부모로부터 메시지를 받음 - 워커는 다시
parentPort.postMessage
로 부모에게 메시지를 보냄 - 부모는
worker.on('message')
로 메시지를 받음 (메시지를 한번만 받고 싶으면 on 대신 once 사용) - 워커에서 on 메서드 사용시엔
parentPort.close()
를 통해 직접 워커를 종료해야 함 - 워커가 종료되면 부모의
worker.on('exit')
가 실행됨
4.7.2 workerData 사용
아래는 2개의 워커 스레드에 데이터를 남기는 예시이다. 위에선 worker.postMessage 로 데이터를 전달했는데 아래는 다른 방식으로 데이터를 전달한다.
const {
Worker,
isMainThread,
parentPort,
workerData,
} = require('worker_threads');
if (isMainThread) {
// 부모인 경우
const threads = new Set();
threads.add(
new Worker(__filename, {
workerData: { start: 1 },
}),
);
threads.add(
new Worker(__filename, {
workerData: { start: 2 },
}),
);
for (let worker of threads) {
worker.on('message', message => console.log('from worker', message));
worker.on('exit', () => {
threads.delete(worker);
if (threads.size === 0) {
console.log('job done');
}
});
}
} else {
// 워커인 경우
const data = workerData;
parentPort.postMessage(data.start + 100);
}
from worker 101
from worker 102
job done
new Worker 호출 시 두 번째 인수의 workerData
속성으로 데이터를 보낼 수 있다.
워커는 workerData 로 부모로부터 데이터를 받는다.
워커가 값을 돌려주는 순간 워커가 종료되어 worker.on(‘exit’) 가 실행된다.
4.7.3 복잡한 worker_threads 사용 방식 (소수 갯수 구하기)
아래는 2 ~ 1,000 만 사이의 소수의 갯수를 구하는 작업을 워커 스레드를 사용하지 않은 코드이다.
prime.js
const min = 2;
const max = 10000000;
const primes = [];
function generatePrimes(start, range) {
let isPrime = true;
const end = start + range;
for (let i = start; i < end; i++) {
for (let j = min; j < Math.sqrt(end); j++) {
if (i !== j && i % j === 0) {
isPrime = false;
break;
}
}
if (isPrime) {
primes.push(i);
}
isPrime = true;
}
}
console.time('prime');
generatePrimes(min, max);
console.timeEnd('prime');
console.log(primes.length);
prime: 9.947s
664579
이제 같은 로직을 워커 스레드를 사용하여 사용해보자.
prime-worker.js
const {
Worker,
isMainThread,
parentPort,
workerData,
} = require('worker_threads');
const min = 2;
let primes = [];
function findPrimes(start, range) {
let isPrime = true;
const end = start + range;
for (let i = start; i < end; i++) {
for (let j = min; j < Math.sqrt(end); j++) {
if (i !== j && i % j === 0) {
isPrime = false;
break;
}
}
if (isPrime) {
primes.push(i);
}
isPrime = true;
}
}
if (isMainThread) {
const max = 10000000;
const threadCount = 8;
const threads = new Set();
const range = Math.ceil((max - min) / threadCount);
let start = min;
console.time('prime');
for (let i = 0; i < threadCount - 1; i++) {
const wStart = start;
threads.add(
new Worker(__filename, {
workerData: {
start: wStart,
range,
},
}),
);
start += range;
}
threads.add(
new Worker(__filename, {
workerData: {
start,
range: range + ((max - min + 1) % threadCount),
},
}),
);
for (let worker of threads) {
worker.on('error', err => {
throw err;
});
worker.on('exit', () => {
threads.delete(worker);
if (threads.size === 0) {
console.timeEnd('prime');
console.log(primes.length);
}
});
worker.on('message', msg => {
primes = primes.concat(msg);
});
}
} else {
findPrimes(workerData.start, workerData.range);
parentPort.postMessage(primes);
}
prime: 1.516s
664579
워커 스레드를 사용하지 않을 때보다 훨씬 좋은 성능을 나타내는 것을 확인할 수 있다.
하지만 워커 스레드를 8개 사용한다고 해서 8배 빨라지는 것은 아니다.
스레드를 생성하고 스레드 간 통신 비용이 상당하므로 멀티 스레딩 사용 시엔 이러한 부분도 고려햐여야 한다.
4.8. child_process
child_process
는 다른 프로그램을 실행하고 싶거나, 명령어를 수행하고 싶을 때 사용하는 모듈이다.
child_process
모둘을 통해 다른 언어의 코드를 실행 후 결과값을 받을 수 있다.
child_process
라는 말 그대로 현재 노드 프로세스 외의 새로운 프로세스를 띄워서 명령을 수행하고, 노드 프로세스에 결과를 알려준다.
exec.js
const exec = require('child_process').exec;
const process = exec('ls');
process.stdout.on('data', function (data) {
console.log(data.toString());
});
process.stderr.on('data', function (data) {
console.error(data.toString());
});
3.1-globalA.js
3.1-globalB.js
3.1.1-func.js
결과는 stdout
, stderr
에 붙여준 data
이벤트 리스너에 버퍼 형태로 전달된다.
아래는 파이썬 프로그램을 실행하는 예시이다.
spawn.js
const spawn = require('child_process').spawn;
const process = spawn('python', ['4.8-test.py']);
process.stdout.on('data', function (data) {
console.log(data.toString());
});
process.stderr.on('data', function (data) {
console.error(data.toString());
});
test.py
print('hello~')
hello~
spawn
의 첫 번째 인수는 명령어, 두 번째 인수는 옵션 배열을 넣는다.
exec
는 셸을 실행해서 명령어를 수행하고, spawn
은 새로운 프로세스를 띄우면서 명령어를 실행한다.
spawn
에서도 세 번째 인수로 { shell: true }
를 제공하면 exec 처럼 셸을 실행하여 명령어를 수행한다.
4.9. 기타 모듈들
assert
dns
- 도메인 이름에 대한 IP 주소 획득
net
- HTTP 보다 로우 레벨인 TCP 나 IPC 통신 시 사용
string_decoder
- 버퍼 데이터를 문자열로 변환 시 사용
tls
- TLS 와 SSL 에 관련된 작업 시 사용
tty
- 터미널과 관련된 작업 시 사용
dgram
- UDP 와 관련된 작업 시 사용
v8
- V8 엔진에 직접 접근 시 사용
vm
- 가상 머신에 직접 접근 시 사용
본 포스트는 조현영 저자의 Node.js 교과서 2판을 기반으로 스터디하며 정리한 내용들입니다.