Node.js ขึ้นชื่อเรื่องความเร็วในการจัดการ I/O-bound Tasks ด้วย Event Loop แบบ Single-threaded แต่เมื่อเจองานหนักที่ใช้ CPU มาก (CPU-intensive Tasks) เช่น การประมวลผลรูปภาพ การ Parse ข้อมูลขนาดใหญ่ หรือการคำนวณ Cryptography Event Loop จะถูก Block ทำให้ Server ไม่ตอบสนอง Worker Threads คือทางออกของปัญหานี้
ปัญหาของ Node.js Single Thread
Node.js ใช้ V8 JavaScript Engine ที่ทำงานบน Thread เดียว ซึ่งดีสำหรับ I/O Operations (อ่านไฟล์ เรียก API Query Database) เพราะ Node.js จะไม่รอ I/O แต่จะไปทำงานอื่นก่อนแล้วกลับมาเมื่อ I/O เสร็จ (Non-blocking I/O)
แต่ถ้ามีงาน CPU-intensive เช่น Loop คำนวณ 1 ล้านรอบ Event Loop จะถูก Block ทั้งหมด ไม่มีใครใช้งาน Server ได้จนกว่าจะคำนวณเสร็จ
// ตัวอย่าง: CPU-intensive task ที่ Block Event Loop
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// ถ้า n = 45 จะใช้เวลาหลายวินาที
// ระหว่างนั้น Server จะไม่ตอบสนองเลย!
app.get('/fibonacci', (req, res) => {
const result = fibonacci(45); // Block Event Loop!
res.json({ result });
});
Worker Threads คืออะไร?
Worker Threads คือ Module ในตัวของ Node.js (worker_threads) ที่ให้คุณสร้าง Thread ใหม่เพื่อรัน JavaScript Code แบบขนาน (Parallel) กับ Main Thread ทำให้งาน CPU-intensive ไม่ Block Event Loop
// main.js — สร้าง Worker
const { Worker } = require('worker_threads');
const worker = new Worker('./worker.js', {
workerData: { n: 45 }
});
worker.on('message', (result) => {
console.log('Fibonacci result:', result);
});
worker.on('error', (err) => {
console.error('Worker error:', err);
});
worker.on('exit', (code) => {
console.log('Worker exited with code:', code);
});
// worker.js — ทำงานใน Thread แยก
const { workerData, parentPort } = require('worker_threads');
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
const result = fibonacci(workerData.n);
parentPort.postMessage(result); // ส่งผลกลับ Main Thread
การส่งข้อมูลระหว่าง Threads
1. postMessage (Structured Clone)
// Main Thread
worker.postMessage({ type: 'calculate', data: [1, 2, 3, 4, 5] });
// Worker Thread
parentPort.on('message', (msg) => {
if (msg.type === 'calculate') {
const sum = msg.data.reduce((a, b) => a + b, 0);
parentPort.postMessage({ type: 'result', sum });
}
});
2. SharedArrayBuffer — แชร์หน่วยความจำ
// Main Thread — สร้าง SharedArrayBuffer
const sharedBuffer = new SharedArrayBuffer(1024); // 1KB
const arr = new Int32Array(sharedBuffer);
arr[0] = 42;
const worker = new Worker('./worker.js', {
workerData: { sharedBuffer }
});
// Worker Thread — อ่าน/เขียน SharedArrayBuffer ได้เลย
const { workerData } = require('worker_threads');
const arr = new Int32Array(workerData.sharedBuffer);
console.log(arr[0]); // 42
arr[1] = 100; // Main Thread จะเห็นค่านี้ด้วย
3. transferList — โอนย้ายข้อมูล (Zero-copy)
// Main Thread — Transfer ArrayBuffer (ไม่ copy, ย้ายเลย)
const buffer = new ArrayBuffer(1024);
const uint8 = new Uint8Array(buffer);
uint8[0] = 255;
worker.postMessage({ buffer }, [buffer]);
// buffer ใน Main Thread จะใช้ไม่ได้อีกต่อไป (transferred)
Worker Pool Pattern
การสร้าง Worker ใหม่ทุกครั้งมี Overhead ทางออกคือสร้าง Pool ของ Workers ที่พร้อมใช้งาน
ใช้ Library: piscina
// npm install piscina
const Piscina = require('piscina');
const pool = new Piscina({
filename: './worker.js',
maxThreads: 4, // จำนวน Worker สูงสุด
minThreads: 2, // จำนวน Worker ขั้นต่ำ
});
// ใช้งาน — Pool จัดการให้อัตโนมัติ
async function processAll() {
const results = await Promise.all([
pool.run({ n: 40 }),
pool.run({ n: 41 }),
pool.run({ n: 42 }),
pool.run({ n: 43 }),
]);
console.log(results);
}
// worker.js สำหรับ piscina
module.exports = function({ n }) {
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
return fibonacci(n);
};
ใช้ Library: workerpool
// npm install workerpool
const workerpool = require('workerpool');
const pool = workerpool.pool('./worker.js', {
maxWorkers: 4,
});
pool.exec('fibonacci', [42])
.then(result => console.log(result))
.catch(err => console.error(err))
.then(() => pool.terminate());
เมื่อไรใช้ Workers vs child_process vs cluster
| Feature | Worker Threads | child_process | cluster |
|---|---|---|---|
| แชร์หน่วยความจำ | ได้ (SharedArrayBuffer) | ไม่ได้ | ไม่ได้ |
| Overhead | ต่ำ | สูง (Process ใหม่) | สูง (Process ใหม่) |
| Use Case | CPU-intensive ใน Node.js | รัน Script/Program ภายนอก | Scale HTTP Server หลาย Core |
| Communication | postMessage + SharedArrayBuffer | IPC (stdin/stdout/message) | IPC (message) |
| Isolation | แชร์ Process (Thread-level) | แยก Process สมบูรณ์ | แยก Process สมบูรณ์ |
| เมื่อไรใช้ | คำนวณหนัก ภายใน Node.js | รัน Python, FFmpeg, etc. | Scale Web Server หลาย CPU |
Use Cases จริง
1. Image Processing
// worker-image.js
const { parentPort, workerData } = require('worker_threads');
const sharp = require('sharp');
async function processImage() {
const { inputPath, outputPath, width, height } = workerData;
await sharp(inputPath)
.resize(width, height)
.jpeg({ quality: 80 })
.toFile(outputPath);
parentPort.postMessage({ status: 'done', outputPath });
}
processImage();
2. Data Parsing (CSV/JSON)
// แบ่ง File ขนาดใหญ่ให้แต่ละ Worker Parse
const { Worker } = require('worker_threads');
function parseChunk(chunk, workerId) {
return new Promise((resolve, reject) => {
const w = new Worker('./parse-worker.js', {
workerData: { chunk, workerId }
});
w.on('message', resolve);
w.on('error', reject);
});
}
// Main: แบ่ง data เป็น chunks แล้วส่งแต่ละ Worker
const chunks = splitIntoChunks(bigData, 4);
const results = await Promise.all(
chunks.map((chunk, i) => parseChunk(chunk, i))
);
const merged = results.flat();
3. Crypto Operations
// worker-hash.js
const { parentPort, workerData } = require('worker_threads');
const crypto = require('crypto');
const hash = crypto.pbkdf2Sync(
workerData.password,
workerData.salt,
100000, // iterations
64,
'sha512'
);
parentPort.postMessage(hash.toString('hex'));
Worker Threads + Express
const express = require('express');
const Piscina = require('piscina');
const app = express();
const pool = new Piscina({
filename: './heavy-computation.js',
maxThreads: 4,
});
app.get('/compute/:n', async (req, res) => {
try {
const result = await pool.run({ n: parseInt(req.params.n) });
res.json({ result });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// Event Loop ยังตอบสนอง Request อื่น ๆ ได้ตามปกติ!
app.get('/health', (req, res) => {
res.json({ status: 'ok' }); // ไม่ถูก Block
});
app.listen(3000);
Benchmarks: Single Thread vs Worker Threads
| Task | Single Thread | 4 Workers | Speedup |
|---|---|---|---|
| Fibonacci(42) x 4 | ~12 sec | ~3.5 sec | 3.4x |
| Image Resize 100 images | ~30 sec | ~8 sec | 3.75x |
| CSV Parse 500MB | ~20 sec | ~6 sec | 3.3x |
| PBKDF2 Hash x 100 | ~15 sec | ~4 sec | 3.75x |
Common Mistakes
- สร้าง Worker ใหม่ทุก Request: Overhead สูงมาก ใช้ Worker Pool แทน
- ส่งข้อมูลใหญ่ผ่าน postMessage: ใช้ SharedArrayBuffer หรือ transferList
- ใช้ Worker สำหรับ I/O Tasks: ไม่จำเป็น Node.js จัดการ I/O ได้ดีอยู่แล้ว
- ไม่จัดการ Error ใน Worker: Worker crash = ไม่มี response ต้อง handle error event
- สร้าง Worker มากเกินจำนวน CPU Core: ไม่ได้เร็วขึ้น อาจช้าลงด้วยซ้ำ
- ลืม Terminate Worker: Memory leak ถ้าไม่ terminate worker เมื่อเสร็จงาน
Alternatives: Bun Workers & Deno Workers
| Runtime | API | ข้อดี |
|---|---|---|
| Node.js | worker_threads module | Mature, Ecosystem ใหญ่, SharedArrayBuffer |
| Bun | Web Workers API (new Worker) | เร็วกว่า Node.js 2-4x, API ง่ายกว่า |
| Deno | Web Workers API | Secure by default, TypeScript built-in |
สรุป
Worker Threads เป็นเครื่องมือสำคัญสำหรับ Node.js Developer ที่ต้องจัดการ CPU-intensive Tasks ช่วยให้ Event Loop ไม่ถูก Block ทำให้ Server ยังคงตอบสนองได้ดี ใช้ Worker Pool Library อย่าง piscina เพื่อจัดการ Workers อย่างมีประสิทธิภาพ และจำไว้ว่า Worker Threads เหมาะสำหรับ CPU-bound Tasks เท่านั้น ไม่ใช่ I/O-bound Tasks
