[EXT-040][실무] 작업 스레드 수와 대기열 제한하기
페이지 정보

본문
[이번 수업]
새 작업마다 스레드를 만들면 준비 비용이 듭니다. 이번에는 미리 만든 작업 스레드를 재사용하는 작업자 풀(worker pool)과 제한된 대기열을 만들고, 처리 한도를 넘은 요청을 거절하는 역압(backpressure)을 익힙니다.
[선수지식]
Worker, parentPort, Promise와 이벤트의 기본 사용법이 필요합니다. 현재 지원 Node.js를 준비하세요.
[학습목표]
1. 고정된 수의 Worker를 여러 계산에 재사용한다.
2. 빈 작업자에게 대기 작업을 순서대로 배정한다.
3. 대기열 한도를 넘으면 호출자에게 실패를 돌려준다.
[핵심개념]
풀은 실행 작업 수를 Worker 수 이하로 묶습니다. 남은 작업은 FIFO(먼저 들어온 것이 먼저 나가는 방식) 대기열에 둡니다. 무한한 대기열은 메모리와 응답 시간을 늘립니다. 한도에 도달하면 새 작업을 거절해 호출자가 부하를 줄이게 합니다. Node.js 공식 문서도 작업마다 Worker를 만들기보다 풀을 권합니다. 운영용 풀에는 실패한 Worker 교체와 제한 시간도 필요합니다.
[따라하기]
아래 내용을 pool.mjs로 저장하세요.
```js
import { Worker, isMainThread, parentPort } from 'node:worker_threads';
if (!isMainThread) {
parentPort.on('message', n => parentPort.postMessage(n * n));
} else {
const queue = [];
const slots = Array.from({ length: 2 }, () => {
const worker = new Worker(new URL(import.meta.url));
const slot = { worker, job: null, dead: false };
worker.on('message', value => {
const job = slot.job;
slot.job = null;
job.resolve(value);
drain();
});
worker.on('error', error => {
if (slot.job) slot.job.reject(error);
slot.job = null;
slot.dead = true;
drain();
});
return slot;
});
function drain() {
for (const slot of slots) {
if (slot.dead || slot.job) continue;
const job = queue.shift();
if (!job) return;
slot.job = job;
slot.worker.postMessage(job.n);
}
}
function submit(n) {
if (queue.length >= 2)
return Promise.reject(new Error('queue full'));
return new Promise((resolve, reject) => {
queue.push({ n, resolve, reject });
drain();
});
}
const results = await Promise.allSettled([2, 3, 4, 5, 6].map(submit));
for (const item of results) {
if (item.status === 'fulfilled') console.log(item.value);
else console.log(item.reason.message);
}
await Promise.all(slots.map(slot => slot.worker.terminate()));
}
```
Windows·macOS·Linux 모두 `node pool.mjs`를 실행합니다. 두 작업은 실행되고 두 작업은 기다리므로 다섯 번째가 거절됩니다.
```text
4
9
16
25
queue full
```
[흔한 실수]
대기열 길이를 제한하지 않거나, message 뒤 slot.job을 비우지 않으면 문제가 생깁니다. 실패한 Worker를 다시 쓰거나 Worker를 무조건 많이 만드는 것도 피하세요.
[보안 주의]
입력의 자료형과 범위를 검사하고 사용자 입력을 코드나 파일 경로로 실행하지 않습니다. Worker 수·대기열 길이·작업 시간을 제한하세요. 과부하 오류에서 내부 상세정보를 숨기고, 본인 소유의 로컬·격리 환경에서 먼저 시험하세요.
[직접 해볼 과제]
Worker 수를 1로 바꿔 결과를 확인하세요. 대기열 한도를 1로 줄여 거절 수를 기록하고, queue full이면 잠시 기다린 뒤 한 번만 다시 시도하도록 확장해 보세요.
[확인문제]
1. 계산마다 Worker를 새로 만드는 방식은 왜 비효율적일까요?
2. Worker 두 개와 대기열 두 칸이 모두 찼을 때 새 작업을 거절하는 이유는 무엇일까요?
3. 작업자 풀을 운영용으로 만들 때 오류 처리 외에 어떤 제한이 더 필요할까요?
[다음 학습]
다음에는 os.availableParallelism()으로 시작 크기를 정하고 처리 시간·대기열 길이·실패 수를 측정합니다.
[공식 참고 자료]
Node.js Worker threads
https://nodejs.org/api/worker_threads.html
Node.js AsyncResource 작업자 풀 예제
https://nodejs.org/api/async_context.html#using-asyncresource-for-a-worker-thread-pool
Node.js Worker 이벤트
https://nodejs.org/api/worker_threads.html#class-worker
새 작업마다 스레드를 만들면 준비 비용이 듭니다. 이번에는 미리 만든 작업 스레드를 재사용하는 작업자 풀(worker pool)과 제한된 대기열을 만들고, 처리 한도를 넘은 요청을 거절하는 역압(backpressure)을 익힙니다.
[선수지식]
Worker, parentPort, Promise와 이벤트의 기본 사용법이 필요합니다. 현재 지원 Node.js를 준비하세요.
[학습목표]
1. 고정된 수의 Worker를 여러 계산에 재사용한다.
2. 빈 작업자에게 대기 작업을 순서대로 배정한다.
3. 대기열 한도를 넘으면 호출자에게 실패를 돌려준다.
[핵심개념]
풀은 실행 작업 수를 Worker 수 이하로 묶습니다. 남은 작업은 FIFO(먼저 들어온 것이 먼저 나가는 방식) 대기열에 둡니다. 무한한 대기열은 메모리와 응답 시간을 늘립니다. 한도에 도달하면 새 작업을 거절해 호출자가 부하를 줄이게 합니다. Node.js 공식 문서도 작업마다 Worker를 만들기보다 풀을 권합니다. 운영용 풀에는 실패한 Worker 교체와 제한 시간도 필요합니다.
[따라하기]
아래 내용을 pool.mjs로 저장하세요.
```js
import { Worker, isMainThread, parentPort } from 'node:worker_threads';
if (!isMainThread) {
parentPort.on('message', n => parentPort.postMessage(n * n));
} else {
const queue = [];
const slots = Array.from({ length: 2 }, () => {
const worker = new Worker(new URL(import.meta.url));
const slot = { worker, job: null, dead: false };
worker.on('message', value => {
const job = slot.job;
slot.job = null;
job.resolve(value);
drain();
});
worker.on('error', error => {
if (slot.job) slot.job.reject(error);
slot.job = null;
slot.dead = true;
drain();
});
return slot;
});
function drain() {
for (const slot of slots) {
if (slot.dead || slot.job) continue;
const job = queue.shift();
if (!job) return;
slot.job = job;
slot.worker.postMessage(job.n);
}
}
function submit(n) {
if (queue.length >= 2)
return Promise.reject(new Error('queue full'));
return new Promise((resolve, reject) => {
queue.push({ n, resolve, reject });
drain();
});
}
const results = await Promise.allSettled([2, 3, 4, 5, 6].map(submit));
for (const item of results) {
if (item.status === 'fulfilled') console.log(item.value);
else console.log(item.reason.message);
}
await Promise.all(slots.map(slot => slot.worker.terminate()));
}
```
Windows·macOS·Linux 모두 `node pool.mjs`를 실행합니다. 두 작업은 실행되고 두 작업은 기다리므로 다섯 번째가 거절됩니다.
```text
4
9
16
25
queue full
```
[흔한 실수]
대기열 길이를 제한하지 않거나, message 뒤 slot.job을 비우지 않으면 문제가 생깁니다. 실패한 Worker를 다시 쓰거나 Worker를 무조건 많이 만드는 것도 피하세요.
[보안 주의]
입력의 자료형과 범위를 검사하고 사용자 입력을 코드나 파일 경로로 실행하지 않습니다. Worker 수·대기열 길이·작업 시간을 제한하세요. 과부하 오류에서 내부 상세정보를 숨기고, 본인 소유의 로컬·격리 환경에서 먼저 시험하세요.
[직접 해볼 과제]
Worker 수를 1로 바꿔 결과를 확인하세요. 대기열 한도를 1로 줄여 거절 수를 기록하고, queue full이면 잠시 기다린 뒤 한 번만 다시 시도하도록 확장해 보세요.
[확인문제]
1. 계산마다 Worker를 새로 만드는 방식은 왜 비효율적일까요?
2. Worker 두 개와 대기열 두 칸이 모두 찼을 때 새 작업을 거절하는 이유는 무엇일까요?
3. 작업자 풀을 운영용으로 만들 때 오류 처리 외에 어떤 제한이 더 필요할까요?
[다음 학습]
다음에는 os.availableParallelism()으로 시작 크기를 정하고 처리 시간·대기열 길이·실패 수를 측정합니다.
[공식 참고 자료]
Node.js Worker threads
https://nodejs.org/api/worker_threads.html
Node.js AsyncResource 작업자 풀 예제
https://nodejs.org/api/async_context.html#using-asyncresource-for-a-worker-thread-pool
Node.js Worker 이벤트
https://nodejs.org/api/worker_threads.html#class-worker
- 이전글[EXT-041][실무] 작업 스레드 수를 측정으로 정하기 26.09.09
- 다음글[EXT-039][실무] 무거운 계산을 다른 실행 흐름으로 나누기 26.09.09
댓글목록
등록된 댓글이 없습니다.
