Node.js: Worker Threads
آخر تحديث: 2026-08-26
1. ما ستتعلمه
- استخدم فئة
Workerلإنشاء خيوط العمل وإدارة دورات حياتها - تنفيذ اتصال ثنائي الاتجاه للرسائل بين الخيط الرئيسي (スレッド) والعاملين باستخدام
parentPort - استخدم
workerDataلتمرير بيانات التهيئة إلى «العامل» - استخدم
MessageChannel/MessagePortلإقامة اتصال مباشر بين الخيوط - تنفيذ عمليات الذاكرة المشتركة باستخدام
SharedArrayBufferوAtomics - إنشاء نمط «مجمع الخيوط» لإعادة استخدام العمال وتحسين معدل الإنتاجية
- مقارنة حالات الاستخدام المناسبة لخيوط العمل،
child_processوcluster
2. القصة: تسريع معالجة الصور في «أليس»
أليس مسؤولة عن خدمات معالجة الصور في إحدى شركات SaaS. عندما يقوم المستخدمون بتحميل الصور، يتعين على النظام إنشاء صور مصغرة بثلاثة أحجام مختلفة لكل صورة. كان الحل الأصلي الذي يعمل بخيط واحد يستغرق حوالي 30 ثانية لمعالجة 100 صورة، وخلال ساعات الذروة، كان هناك تراكم كبير في الطلبات.
قررت استخدام خيوط العمل لتوزيع المهام على أربعة خيوط من أجل المعالجة المتوازية: حيث يقوم الخيط الرئيسي بقراءة قائمة الملفات ويمرر معلمات المهمة عبر workerData، بينما تقوم خيوط العمل بإرجاع النتائج عبر parentPort بعد إنشاء الصور المصغرة. وفي النهاية، تم تقليل وقت معالجة 100 صورة إلى حوالي 8 ثوانٍ، وزاد معدل الإنتاجية بنحو أربعة أضعاف.
3. أساسيات فئة العمال
توفر وحدة worker_threads إمكانيات حقيقية للتعدد الخيطي في Node.js. حيث يقوم كل Worker بتنفيذ البرامج النصية في خيط منفصل، وله مثيل محرك V8 خاص به وحلقة أحداث خاصة به.
const { Worker } = require('worker_threads');
const worker = new Worker('./heavy-task.js', {
workerData: { taskId: 42, input: 'hello' }
});
worker.on('message', (result) => {
console.log('Result from worker:', result);
});
worker.on('error', (err) => {
console.error('Worker error:', err);
});
worker.on('exit', (code) => {
if (code !== 0) {
console.error(`Worker stopped with exit code ${code}`);
}
});
داخل البرنامج النصي «Worker»:
// heavy-task.js
const { parentPort, workerData } = require('worker_threads');
const { taskId, input } = workerData;
const result = performHeavyComputation(input);
parentPort.postMessage({ taskId, result });
الدالة performHeavyComputation(data) {
let hash = 0;
for (let i = 0; i < 1e8; i++) {
hash = (hash + data.charCodeAt(i % data.length)) % 65536;
}
return hash;
}
▶ مثال: إنشاء عامل أساسي والتواصل معه
// main.js
const { Worker } = require('worker_threads');
const worker = new Worker(`
const { parentPort } = require('worker_threads');
parentPort.postMessage('Hello from worker!');
`, { eval: true });
worker.on('message', (msg) => {
console.log(msg); // Hello from worker!
});
4. الاتصال ثنائي الاتجاه عبر parentPort
parentPort هي قناة الرسائل بين «العامل» (Worker) والخيط الرئيسي (スレッド). يرسل الخيط الرئيسي الرسائل عبر worker.postMessage()، ويقوم «العامل» بالرد عبر parentPort.postMessage(). ويقوم كلا الطرفين بمراقبة أحداث message لاستلام البيانات.
▶ مثال: تبادل الرسائل في الاتجاهين
// main.js
const { Worker } = require('worker_threads');
const worker = new Worker('./echo-worker.js');
worker.postMessage({ action: 'greet', name: 'Alice' });
worker.on('message', (msg) => {
console.log('Main received:', msg);
if (msg.action === 'greet_reply') {
worker.postMessage({ action: 'task', payload: 100 });
}
});
// echo-worker.js
const { parentPort } = require('worker_threads');
parentPort.on('message', (msg) => {
if (msg.action === 'greet') {
parentPort.postMessage({
action: 'greet_reply',
text: `Hello, ${msg.name}!`
});
} else if (msg.action === 'task') {
const result = msg.payload * 2;
parentPort.postMessage({ action: 'result', القيمة: result });
}
});
▶ مثال: النقل بدون نسخ للكائنات القابلة للنقل
const { Worker } = require('worker_threads');
const buffer = new ArrayBuffer(1024 * 1024); // 1 MB
const worker = new Worker('./process-buffer.js');
worker.postMessage({ buffer }, [buffer]);
console.log('Buffer transferred, main thread no longer owns it');
// process-buffer.js
const { parentPort, workerData } = require('worker_threads');
parentPort.on('message', ({ buffer }) => {
const view = new Uint8Array(buffer);
view[0] = 42;
parentPort.postMessage({ done: true, firstByte: view[0] }, [buffer]);
});
5. بيانات تهيئة workerData
workerData هي بيانات أولية للقراءة فقط يتم تمريرها عبر الخيارات عند إنشاء «Worker». ويقوم «Worker» بقراءة هذه البيانات داخليًّا بشكل مباشر، دون الحاجة إلى تبادل الرسائل. وهي مناسبة لتمرير إعدادات التكوين ومسارات الملفات ومعلمات المهام وما إلى ذلك.
▶ مثال: عامل معالجة الصور دفعة واحدة
// main.js
const { Worker } = require('worker_threads');
const path = require('path');
const imageFiles = [
'photo-001.jpg', 'photo-002.jpg', 'photo-003.jpg',
'photo-004.jpg', 'photo-005.jpg', 'photo-006.jpg'
];
const WORKER_COUNT = 4;
const chunkSize = Math.ceil(imageFiles.length / WORKER_COUNT);
for (let i = 0; i < WORKER_COUNT; i++) {
const chunk = imageFiles.slice(i * chunkSize, (i + 1) * chunkSize);
const worker = new Worker('./image-worker.js', {
workerData: {
workerId: i,
files: chunk,
outputDir: './thumbnails'
}
});
worker.on('message', (result) => {
console.log(`Worker ${i} done:`, result.processed);
});
}
// image-worker.js
const { parentPort, workerData } = require('worker_threads');
const path = require('path');
const { workerId, files, outputDir } = workerData;
async function generateThumbnail(file) {
// Simulate image processing
return new Promise((resolve) => {
setTimeout(() => resolve(`${file} -> thumb`), 100);
});
}
(async () => {
const processed = [];
for (const file of files) {
const result = await generateThumbnail(file);
processed.push(result);
}
parentPort.postMessage({ workerId, processed });
})();
6. MessageChannel و MessagePort
MessageChannel قم بإنشاء زوج من مثيلات MessagePort المترابطة التي يمكن تخصيصها لعمال مختلفين، مما يتيح التواصل المباشر بين الخيوط دون المرور عبر الخيط الرئيسي.
graph LR
Main["Main Thread"] -->|"worker.postMessage()"| W1["Worker 1"]
W1 -->|"parentPort.postMessage()"| Main
Main -->|"worker.postMessage()"| W2["Worker 2"]
W2 -->|"parentPort.postMessage()"| Main
W1 <-->|"MessagePort"| W2
▶ مثال: تواصل مباشر بين عاملين
// main.js
const { Worker, MessageChannel } = require('worker_threads');
const worker1 = new Worker('./chat-worker.js', {
workerData: { id: 1 }
});
const worker2 = new Worker('./chat-worker.js', {
workerData: { id: 2 }
});
const { port1, port2 } = new MessageChannel();
worker1.postMessage({ port: port1 }, [port1]);
worker2.postMessage({ port: port2 }, [port2]);
// chat-worker.js
const { parentPort, workerData } = require('worker_threads');
parentPort.once('message', ({ port }) => {
port.on('message', (msg) => {
console.log(`Worker ${workerData.id} received:`, msg);
});
setInterval(() => {
port.postMessage(`Hello from Worker ${workerData.id}`);
}, 1000);
});
7. SharedArrayBuffer والعمليات الذرية
SharedArrayBuffer يسمح لعدة خيوط بمشاركة نفس كتلة الذاكرة. وعند استخدامه بالاقتران مع العمليات الذرية التي يوفرها Atomics، فإنه يتيح القراءة والكتابة الآمنتين للبيانات المشتركة، مما يمنع حدوث حالات التنافس.
▶ مثال: عداد مشترك
// main.js
const { Worker } = require('worker_threads');
const sharedBuffer = new SharedArrayBuffer(4);
const sharedArray = new Int32Array(sharedBuffer);
const WORKER_COUNT = 4;
const INCREMENTS_PER_WORKER = 100000;
for (let i = 0; i < WORKER_COUNT; i++) {
const worker = new Worker('./counter-worker.js', {
workerData: { sharedBuffer, increments: INCREMENTS_PER_WORKER }
});
worker.on('exit', () => {
const final = Atomics.load(sharedArray, 0);
console.log(`Final counter value: ${final}`);
});
}
// counter-worker.js
const { parentPort, workerData } = require('worker_threads');
const { sharedBuffer, increments } = workerData;
const sharedArray = new Int32Array(sharedBuffer);
for (let i = 0; i < increments; i++) {
Atomics.add(sharedArray, 0, 1);
}
parentPort.postMessage('done');
8. مقارنة بين أساليب التواصل
| طريقة الاتصال | الاتجاه | الميزات | السيناريوهات القابلة للتطبيق |
|---|---|---|---|
parentPort |
الخيط الرئيسي ↔ الخيط العامل | التراسل ثنائي الاتجاه، الاستنساخ المنظم | التواصل العام بشأن المهام |
workerData |
الخيط الرئيسي → العامل | للقراءة فقط؛ يتم تمريره عند الإنشاء | إعدادات/معلمات التهيئة |
MessageChannel |
عامل ↔ عامل | اتصال مباشر بين المنافذ، متجاوزًا الخيط الرئيسي | التعاون بين الخيوط |
SharedArrayBuffer |
مشترك بين جميع الخيوط | بدون نسخ، يتطلب استخدام Atomics | تبادل البيانات بتردد عالٍ |
9. Worker مقابل child_process مقابل cluster
| ميزة | خيوط العمل | child_process | المجموعة |
|---|---|---|---|
| الوحدة | الخيوط | العمليات | العمليات |
| الذاكرة | ذاكرة العملية المشتركة | مساحة الذاكرة المستقلة | مساحة الذاكرة المستقلة |
| الاتصال | الرسائل / الذاكرة المشتركة | تسلسل الاتصال بين العمليات (IPC) | تسلسل الاتصال بين العمليات (IPC) |
| النفقات العامة للشركة الناشئة | معتدلة | عالية | عالية |
| ينطبق على | العمليات الحسابية التي تتطلب استخدامًا مكثفًا لوحدة المعالجة المركزية | البرامج المستقلة/بيئات الاختبار المعزولة | خدمات HTTP متعددة النوى |
| الاستقرار | تعطل العمليات العاملة يؤثر على العملية نفسها | تعطل العمليات الفرعية لا يؤثر على بعضها البعض | تعطل العمليات العاملة لا يؤثر على بعضها البعض |
| الحالة المشتركة | SharedArrayBuffer | غير مدعوم | غير مدعوم |
10. المهام المناسبة وغير المناسبة للتعدد الخيطي
| مناسب للتعدد الخيطي | غير مناسب للتعدد الخيطي |
|---|---|
| معالجة الصور/إنشاء الصور المصغرة | توجيه طلبات HTTP البسيطة |
| حسابات التشفير/التجزئة | عمليات CRUD على قاعدة البيانات |
| الضغط/إلغاء الضغط | عمليات الإدخال/الإخراج للملفات (يكفي أن تكون غير متزامنة) |
| العمليات الحسابية واسعة النطاق | التحويلات البسيطة إلى صيغة JSON |
| إنشاء ملفات PDF/عرضها | المهام الصغيرة قصيرة الأمد |
| تحويل صيغ الصوت والفيديو | إعادة توجيه الرسائل استجابةً للأحداث |
11. نمط تجمع الخيوط
ينطوي إنشاء «عامل» (Worker) على بعض التكاليف الإضافية، وغالبًا ما يؤدي إنشاؤها وإلغاؤها بشكل متكرر إلى إهدار الموارد. ويحتفظ «مجمع الخيوط» (thread pool) بعدد ثابت من «العمال»؛ فعندما تصل مهمة ما، يتم تخصيصها لـ«عامل» خامل، وبمجرد اكتمال المهمة، يتم استرداد «العامل» وإعادة استخدامه.
▶ مثال: إنشاء مجموعة مؤشرات ترابط بسيطة من الصفر
// pool.js
const { Worker } = require('worker_threads');
class Pool {
constructor(workerFile, size) {
this.workerFile = workerFile;
this.size = size;
this.workers = [];
this.queue = [];
for (let i = 0; i < size; i++) {
const worker = new Worker(workerFile);
worker.busy = false;
worker.on('message', (result) => {
const task = worker.currentTask;
worker.busy = false;
worker.currentTask = null;
task.resolve(result);
this._processQueue();
});
worker.on('error', (err) => {
const task = worker.currentTask;
if (task) {
worker.busy = false;
worker.currentTask = null;
task.reject(err);
this._processQueue();
}
});
this.workers.push(worker);
}
}
run(data) {
return new Promise((resolve, reject) => {
const task = { data, resolve, reject };
const idle = this.workers.find((w) => !w.busy);
if (idle) {
this._assign(idle, task);
} else {
this.queue.push(task);
}
});
}
_assign(worker, task) {
worker.busy = true;
worker.currentTask = task;
worker.postMessage(task.data);
}
_processQueue() {
if (this.queue.length === 0) return;
const idle = this.workers.find((w) => !w.busy);
if (!idle) return;
const task = this.queue.shift();
this._assign(idle, task);
}
destroy() {
for (const worker of this.workers) {
worker.terminate();
}
this.workers = [];
this.queue = [];
}
}
module.exports = Pool;
▶ مثال: حساب متتابعة فيبوناتشي باستخدام مجموعة مؤشرات الترابط
// main.js
const Pool = require('./pool.js');
const pool = new Pool('./fib-worker.js', 4);
async function main() {
const tasks = [40, 41, 42, 43, 44, 45, 46, 47];
const promises = tasks.map((n) => pool.run({ n }));
const results = await Promise.all(promises);
for (let i = 0; i < tasks.length; i++) {
console.log(`fib(${tasks[i]}) = ${results[i]}`);
}
pool.destroy();
}
main();
// fib-worker.js
const { parentPort } = require('worker_threads');
parentPort.on('message', ({ n }) => {
const result = fib(n);
parentPort.postMessage(result);
});
function fib(n) {
if (n <= 1) return n;
let a = 0, b = 1;
for (let i = 2; i <= n; i++) {
[a, b] = [b, a + b];
}
return b;
}
▶ مثال: مكتبة «piscina» لمجموعات الخيوط
npm install piscina
const path = require('path');
const Piscina = require('piscina');
const pool = new Piscina({
filename: path.resolve(__dirname, 'task.js'),
maxThreads: 4
});
async function main() {
const results = await Promise.all([
pool.run({ x: 10, y: 20 }),
pool.run({ x: 30, y: 40 }),
pool.run({ x: 50, y: 60 })
]);
console.log(results); // [30, 70, 110]
await pool.destroy();
}
main();
// task.js
module.exports = ({ x, y }) => {
return x + y;
};
12. مثال شامل: مجموعة مؤشرات الترابط لمعالجة الصور
باستخدام المعرفة التي تم تناولها سابقًا، قم ببناء نظام كامل لمجموعة مؤشرات الترابط الخاصة بمعالجة الصور: حيث يقوم مؤشر الترابط الرئيسي بتوزيع المهام، وتقوم مؤشرات الترابط العاملة بمعالجة الصور المصغرة، ثم يتم جمع النتائج وتجميعها.
// image-pool.js
const { Worker } = require('worker_threads');
const path = require('path');
class ImagePool {
constructor(workerCount) {
this.workers = [];
this.queue = [];
this.results = [];
for (let i = 0; i < workerCount; i++) {
const worker = new Worker(path.join(__dirname, 'image-processor.js'));
worker.busy = false;
worker.on('message', (msg) => {
if (msg.type === 'result') {
this.results.push(msg.data);
}
worker.busy = false;
worker.currentResolve();
this._dispatch();
});
worker.on('error', (err) => {
worker.busy = false;
if (worker.currentReject) {
worker.currentReject(err);
}
this._dispatch();
});
this.workers.push(worker);
}
}
process(fileList) {
this.results = [];
const promises = fileList.map((file) => this._enqueue(file));
return Promise.all(promises).then(() => this.results);
}
_enqueue(file) {
return new Promise((resolve, reject) => {
this.queue.push({ file, resolve, reject });
this._dispatch();
});
}
_dispatch() {
while (this.queue.length > 0) {
const idle = this.workers.find((w) => !w.busy);
if (!idle) break;
const task = this.queue.shift();
idle.busy = true;
idle.currentResolve = task.resolve;
idle.currentReject = task.reject;
idle.postMessage({ file: task.file, sizes: [200, 400, 800] });
}
}
destroy() {
this.workers.forEach((w) => w.terminate());
}
}
module.exports = ImagePool;
// image-processor.js
const { parentPort } = require('worker_threads');
parentPort.on('message', async ({ file, sizes }) => {
const results = [];
for (const size of sizes) {
const thumb = await resize(file, size);
results.push(thumb);
}
parentPort.postMessage({
type: 'result',
data: { file, thumbnails: results }
});
});
async function resize(file, maxSize) {
return new Promise((resolve) => {
const duration = Math.random() * 200 + 50;
setTimeout(() => {
resolve(`${file}_${maxSize}px.jpg`);
}, duration);
});
}
// run.js
const ImagePool = require('./image-pool.js');
async function main() {
const pool = new ImagePool(4);
const files = Array.from({ length: 20 }, (_, i) =>
`photo-${String(i + 1).padStart(3, '0')}.jpg`
);
const start = Date.now();
const results = await pool.process(files);
const elapsed = Date.now() - start;
console.log(`Processed ${files.length} images in ${elapsed} ms`);
console.log(`Generated ${results.reduce((sum, r) => sum + r.thumbnails.length, 0)} thumbnails`);
pool.destroy();
}
main();
node run.js
Processed 20 images in 1234 ms
Generated 60 thumbnails
❓ أسئلة شائعة
child_process؟child_process فينشئ عمليات منفصلة ذات ذاكرة معزولة ويتواصل عبر الاتصال بين العمليات (IPC) المتسلسل، وهو ما ينطوي على عبء أعلى ولكنه أكثر أمانًا.postMessage لتمرير الرسائل، أو تمرير القيم عبر workerData أثناء التهيئة، أو استخدام SharedArrayBuffer لمشاركة الذاكرة.child_process، لكنه لا يزال وقتًا لا يمكن تجاهله. نوصي بإعادة استخدام «Workers» في تجمع الخيوط لتجنب تكرار عمليات الإنشاء والإلغاء.fs وhttp داخل «Worker»؟cluster، إلا في العملية الرئيسية.📖 ملخص
- المفاهيم الأساسية وكيفية تطبيقها
- المقال: المفاهيم الأساسية واستخدامات «أليس» لتسريع معالجة الصور
- المفاهيم الأساسية لفئة Worker وكيفية استخدامها
- parentPort: المفاهيم الأساسية واستخدامات الاتصال ثنائي الاتجاه
- المفاهيم الأساسية واستخدام بيانات التهيئة (workerData)
- المفاهيم الأساسية واستخدامات MessageChannel وMessagePort
- المفاهيم الأساسية واستخدامات SharedArrayBuffer و Atomics
- المفاهيم الأساسية واستخدامات مقارنة أساليب التواصل
📝 تمارين
- أكمل جميع أمثلة الأكواد الواردة في هذا الدرس وتأكد من أن كل منها يعمل بشكل صحيح.
- قم بتعديل المثال الشامل وأضف الإضافات الخاصة بك
- راجع الوثائق الرسمية، وحدد واجهة برمجة تطبيقات (API) واحدة أو اثنتين لم يتم تناولهما في هذا الدرس، واكتب كود اختبار لهما.
- التأمل: كيف ستطبق ما تعلمته في هذا الدرس على مشروع في الواقع العملي؟
- حاول أن تجمع بين ما تعلمته في هذا الدرس والمواد التي درستها في الدروس السابقة لإنشاء مشروع صغير.