worker_threads va cluster — CPU intensive vazifalar
Muammodan boshlaylik
Node.js bitta thread da ishlaydi. Bu ko'p hollarda yaxshi — asinxron I/O tufayli bitta thread minglab so'rovni boshqara oladi. Lekin ikki holat bor:
1. CPU intensive vazifa — katta massivni saralash, kriptografiya, rasm qayta ishlash, video konvertatsiya. Bu hisoblash tugamaguncha thread bloklanadi, server boshqa so'rovlarga javob bera olmaydi.
2. Ko'p yadroli protsessor — zamonaviy serverlar 8, 16, 32 yadroli. Node.js standart holatda faqat bitta yadroni ishlatadi. Qolgan 7 ta yoki 15 ta yoki 31 ta — bo'sh turadi.
Bu ikki muammoni hal qilish uchun worker_threads va cluster bor.
worker_threads — bir jarayon, ko'p thread
worker_threads — bitta Node.js jarayoni ichida bir nechta thread yaratadi. Threadlar xotirani ulashadi — bu child_process.fork dan farqi.
Hayotiy o'xshatma: siz ofisda ishlayapsiz. Og'ir hisob-kitob kelib qoldi. Siz uni hamkasbingizga topshirasiz — ikkalingiz bir xona (bir jarayon) da ishlaysiz, bir xil hujjatlarga (xotira) kirishingiz mumkin. Hamkasbingiz hisoblayotganda siz boshqa ishni qilasiz.
// main.js
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');
if (isMainThread) {
// Asosiy thread
const worker = new Worker(__filename, {
workerData: { n: 1_000_000_000 }
});
worker.on('message', (result) => {
console.log('Natija:', result);
});
worker.on('error', (err) => {
console.error('Worker xatosi:', err.message);
});
worker.on('exit', (code) => {
if (code !== 0) console.error(`Worker ${code} kodi bilan tugadi`);
});
console.log('Worker ishlamoqda, men boshqa ishni qilaman...');
} else {
// Worker thread
const { n } = workerData;
let sum = 0;
for (let i = 0; i < n; i++) sum += i;
parentPort.postMessage(sum);
}
isMainThread — hozir asosiy thread da ishlayapmizmi yoki worker da? Bir xil faylni ikki maqsadda ishlatish mumkin.
Worker Pool — bir nechta worker
Har so'rov uchun yangi worker yaratish — sekin. Yaxshisi, oldindan bir nechta worker yaratib, ularni qayta ishlatish. Bu — Worker Pool pattern.
const { Worker } = require('worker_threads');
const os = require('os');
class WorkerPool {
constructor(workerFile, size = os.cpus().length) {
this.workers = [];
this.queue = [];
for (let i = 0; i < size; i++) {
this._addWorker(workerFile);
}
}
_addWorker(file) {
const worker = new Worker(file);
worker.busy = false;
worker.on('message', (result) => {
worker.busy = false;
worker.currentResolve(result);
this._processQueue();
});
this.workers.push(worker);
}
run(data) {
return new Promise((resolve) => {
const freeWorker = this.workers.find(w => !w.busy);
if (freeWorker) {
freeWorker.busy = true;
freeWorker.currentResolve = resolve;
freeWorker.postMessage(data);
} else {
this.queue.push({ data, resolve });
}
});
}
_processQueue() {
if (this.queue.length === 0) return;
const { data, resolve } = this.queue.shift();
this.run(data).then(resolve);
}
}
// Ishlatish:
const pool = new WorkerPool('./heavy-worker.js');
// Bir vaqtda bir nechta og'ir hisoblash:
const results = await Promise.all([
pool.run({ n: 1e9 }),
pool.run({ n: 2e9 }),
pool.run({ n: 3e9 }),
]);
cluster — bir nechta jarayon, bir port
cluster — bitta portda bir nechta Node.js jarayonini ishlatish imkonini beradi. Har bir jarayon — alohida, mustaqil. Xotirani ulashmaydi.
Hayotiy o'xshatma: katta restoran. Bitta oshpaz (bitta jarayon) bilan 1000 ta mijozga xizmat qilish qiyin. Yechim — 8 ta oshpaz (8 ta jarayon), hammasi bir xil menyudan (bir xil kod) ishlaydi. Mijozlar esa navbat bilan taqsimlanadi.
const cluster = require('cluster');
const http = require('http');
const os = require('os');
if (cluster.isPrimary) {
// Asosiy jarayon — workerlarni boshqaradi
const cpuCount = os.cpus().length;
console.log(`${cpuCount} ta worker yaratilmoqda`);
for (let i = 0; i < cpuCount; i++) {
cluster.fork();
}
// Worker o'chib qolsa — yangisini yaratish
cluster.on('exit', (worker, code) => {
console.log(`Worker ${worker.process.pid} to'xtadi (kod: ${code})`);
console.log('Yangi worker yaratilmoqda...');
cluster.fork();
});
} else {
// Worker jarayon — haqiqiy server
http.createServer((req, res) => {
res.end(`Jarayon ${process.pid} javob berdi`);
}).listen(3000);
console.log(`Worker ${process.pid} ishga tushdi`);
}
Ishga tushirganda:
8 ta worker yaratilmoqda
Worker 1234 ishga tushdi
Worker 1235 ishga tushdi
...
Worker 1241 ishga tushdi
Endi http://localhost:3000 ga so'rov yuborsangiz — 8 ta jarayondan biri javob beradi. Agar biri o'chib qolsa — asosiy jarayon yangisini yaratadi.
worker_threads vs cluster — qachon qaysi?
Bu ikki texnologiya turli muammolarni hal qiladi:
worker_threads — bitta so'rov ichidagi og'ir hisoblash uchun. Masalan, foydalanuvchi rasm yukladi — siz uni qayta ishlashingiz kerak. Bu bitta so'rov, lekin og'ir ish. Worker thread bu ishni asosiy threadni bloklamasdan bajaradi.
cluster — ko'p so'rovlarni parallel qabul qilish uchun. Masalan, serveringizga sekundiga 10,000 so'rov keladi. Bitta jarayon yetmaydi — 8 ta jarayon bilan 8 baravar ko'p so'rov qabul qilinadi.
worker_threads:
Bitta so'rov → og'ir hisoblash → worker thread ga topshir
Asosiy thread boshqa so'rovlarga javob beradi
cluster:
Ko'p so'rov → 8 ta jarayon → har biri bir qismini oladi
Jami throughput 8 baravar oshadi
Amaliy holat — Express + cluster
const cluster = require('cluster');
const os = require('os');
if (cluster.isPrimary) {
os.cpus().forEach(() => cluster.fork());
cluster.on('exit', (worker) => {
console.error(`Worker ${worker.process.pid} o'chdi, qayta ishga tushirilmoqda`);
cluster.fork();
});
} else {
// Har bir worker o'z Express serverini ishga tushiradi
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.json({ pid: process.pid, message: 'Salom!' });
});
app.listen(3000, () => {
console.log(`Worker ${process.pid} tayyor`);
});
}
Tez-tez uchraydigan xatolar
worker_threads da xotirani ulashish xavfi:
// SharedArrayBuffer — ikki thread bir xil xotiraga yozsa:
const shared = new SharedArrayBuffer(4);
const arr = new Int32Array(shared);
// Ikki thread bir vaqtda yozsa — race condition
// Atomics ishlatiladi:
Atomics.add(arr, 0, 1); // xavfsiz
cluster da holat saqlash:
// Xato — har worker alohida xotira:
const sessions = {}; // bitta worker da saqlangan session
// boshqa worker ko'rmaydi!
// To'g'ri — Redis yoki boshqa tashqi saqlash:
const redis = require('redis');
// session ni Redis da saqlash
Cluster da workerlar xotirani ulashmaydi — session, cache kabi narsalar tashqi saqlashda (Redis) bo'lishi kerak.
Xulosa
worker_threads — bitta jarayon ichida ko'p thread. Og'ir hisoblashni asosiy threadni bloklamasdan bajarish uchun. Xotirani ulashadi.
cluster — bir portda ko'p jarayon. Ko'p yadroli protsessordan to'liq foydalanish, ko'p so'rovlarni parallel qabul qilish uchun. Xotirani ulashmaydi.
Production da ko'pincha ikkalasi birga ishlatiladi: cluster bilan ko'p jarayon, har jarayon ichida worker_threads bilan og'ir hisoblash.
Keyingi dars
23-dars: Memory Leak — nima, qanday topiladi