Node.js: Worker Threads

آخر تحديث: 2026-08-26

1. ما ستتعلمه



2. القصة: تسريع معالجة الصور في «أليس»

أليس مسؤولة عن خدمات معالجة الصور في إحدى شركات SaaS. عندما يقوم المستخدمون بتحميل الصور، يتعين على النظام إنشاء صور مصغرة بثلاثة أحجام مختلفة لكل صورة. كان الحل الأصلي الذي يعمل بخيط واحد يستغرق حوالي 30 ثانية لمعالجة 100 صورة، وخلال ساعات الذروة، كان هناك تراكم كبير في الطلبات.

قررت استخدام خيوط العمل لتوزيع المهام على أربعة خيوط من أجل المعالجة المتوازية: حيث يقوم الخيط الرئيسي بقراءة قائمة الملفات ويمرر معلمات المهمة عبر workerData، بينما تقوم خيوط العمل بإرجاع النتائج عبر parentPort بعد إنشاء الصور المصغرة. وفي النهاية، تم تقليل وقت معالجة 100 صورة إلى حوالي 8 ثوانٍ، وزاد معدل الإنتاجية بنحو أربعة أضعاف.



3. أساسيات فئة العمال

توفر وحدة worker_threads إمكانيات حقيقية للتعدد الخيطي في Node.js. حيث يقوم كل Worker بتنفيذ البرامج النصية في خيط منفصل، وله مثيل محرك V8 خاص به وحلقة أحداث خاصة به.

JAVASCRIPT
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»:

JAVASCRIPT
// 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;
}

▶ مثال: إنشاء عامل أساسي والتواصل معه

JAVASCRIPT
// 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 لاستلام البيانات.

▶ مثال: تبادل الرسائل في الاتجاهين

JAVASCRIPT
// 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 });
  }
});
▶ جرّب الكود
JAVASCRIPT
// 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 });
  }
});

▶ مثال: النقل بدون نسخ للكائنات القابلة للنقل

JAVASCRIPT
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');
▶ جرّب الكود
JAVASCRIPT
// 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» بقراءة هذه البيانات داخليًّا بشكل مباشر، دون الحاجة إلى تبادل الرسائل. وهي مناسبة لتمرير إعدادات التكوين ومسارات الملفات ومعلمات المهام وما إلى ذلك.

▶ مثال: عامل معالجة الصور دفعة واحدة

JAVASCRIPT
// 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);
  });
}
▶ جرّب الكود
JAVASCRIPT
// 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 المترابطة التي يمكن تخصيصها لعمال مختلفين، مما يتيح التواصل المباشر بين الخيوط دون المرور عبر الخيط الرئيسي.

100%
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

▶ مثال: تواصل مباشر بين عاملين

JAVASCRIPT
// 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]);
▶ جرّب الكود
JAVASCRIPT
// 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، فإنه يتيح القراءة والكتابة الآمنتين للبيانات المشتركة، مما يمنع حدوث حالات التنافس.

▶ مثال: عداد مشترك

JAVASCRIPT
// 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}`);
  });
}
▶ جرّب الكود
JAVASCRIPT
// 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) بعدد ثابت من «العمال»؛ فعندما تصل مهمة ما، يتم تخصيصها لـ«عامل» خامل، وبمجرد اكتمال المهمة، يتم استرداد «العامل» وإعادة استخدامه.

▶ مثال: إنشاء مجموعة مؤشرات ترابط بسيطة من الصفر

JAVASCRIPT 📖 للعرض فقط
// 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;
61 سطر من الكود المنطقي (تجاوز الحد 40, للعرض فقط)

▶ مثال: حساب متتابعة فيبوناتشي باستخدام مجموعة مؤشرات الترابط

JAVASCRIPT
// 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();
▶ جرّب الكود
JAVASCRIPT
// 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» لمجموعات الخيوط

BASH
npm install piscina
JAVASCRIPT
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();
JAVASCRIPT
// task.js
module.exports = ({ x, y }) => {
  return x + y;
};


12. مثال شامل: مجموعة مؤشرات الترابط لمعالجة الصور

باستخدام المعرفة التي تم تناولها سابقًا، قم ببناء نظام كامل لمجموعة مؤشرات الترابط الخاصة بمعالجة الصور: حيث يقوم مؤشر الترابط الرئيسي بتوزيع المهام، وتقوم مؤشرات الترابط العاملة بمعالجة الصور المصغرة، ثم يتم جمع النتائج وتجميعها.

JAVASCRIPT
// 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;
JAVASCRIPT
// 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);
  });
}
JAVASCRIPT
// 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();
BASH
node run.js
TEXT 📖 للعرض فقط
Processed 20 images in 1234 ms
Generated 60 thumbnails


❓ أسئلة شائعة

س ما الفرق بين خيوط العمل وchild_process؟
ج يتم إنشاء خيوط العمل ضمن العملية نفسها ويمكنها مشاركة الذاكرة (SharedArrayBuffer)، مما يؤدي إلى انخفاض عبء الاتصال؛ أما child_process فينشئ عمليات منفصلة ذات ذاكرة معزولة ويتواصل عبر الاتصال بين العمليات (IPC) المتسلسل، وهو ما ينطوي على عبء أعلى ولكنه أكثر أمانًا.
س هل يمكن للعامل الوصول إلى المتغيرات الموجودة في الخيط الرئيسي؟
ج لا. فالعاملون لديهم سياق التنفيذ الخاص بهم ولا يمكنهم الوصول مباشرةً إلى المتغيرات الموجودة في الخيط الرئيسي. يجب عليك استخدام postMessage لتمرير الرسائل، أو تمرير القيم عبر workerData أثناء التهيئة، أو استخدام SharedArrayBuffer لمشاركة الذاكرة.
س متى ينبغي استخدام خيوط العمل؟
ج للمهام التي تتطلب استخدامًا مكثفًا لوحدة المعالجة المركزية (CPU)، مثل معالجة الصور، والتشفير، والضغط وفك الضغط، والحسابات الرياضية واسعة النطاق، وتحويل ترميز الصوت والفيديو. أما المهام التي تتطلب استخدامًا مكثفًا لعمليات الإدخال/الإخراج (I/O)، فلا تتطلب خيوط عمل؛ حيث إن عمليات الإدخال/الإخراج غير المتزامنة في Node.js تتمتع بالفعل بكفاءة كافية.
س هل يُعد SharedArrayBuffer آمنًا؟
ج لا يوفر SharedArrayBuffer أي آليات تزامن بحد ذاته؛ وقد تؤدي عمليات القراءة والكتابة المتزامنة في بيئة متعددة الخيوط إلى حدوث حالات تنافس. يجب عليك استخدام العمليات الذرية (مثل add و load و store و compareExchange) لضمان الذرية، أو استخدام الأقفال لتنسيق الوصول.
س هل ينطوي إنشاء «Worker» على عبء إضافي كبير؟
ج يتطلب إنشاء «Worker» تهيئة مثيل لمحرك V8 وحلقة أحداث، وهو ما يستغرق حوالي 30–50 مللي ثانية — وهو وقت أقل مما يستغرقه child_process، لكنه لا يزال وقتًا لا يمكن تجاهله. نوصي بإعادة استخدام «Workers» في تجمع الخيوط لتجنب تكرار عمليات الإنشاء والإلغاء.
س هل يمكن استخدام وحدات Node.js مثل fs وhttp داخل «Worker»؟
ج نعم. تحتوي خيوط «Worker» على بيئة تشغيل Node.js كاملة وتدعم الغالبية العظمى من الوحدات المدمجة. ومع ذلك، لا يمكن استخدام بعض الوحدات، مثل cluster، إلا في العملية الرئيسية.
س ما الفرق بين «القابلية للنقل» و«الاستنساخ المنظم»؟
ج الاستنساخ المنظم يقوم بنسخ البيانات وهو مناسب لمجموعات البيانات الصغيرة؛ أما «القابلية للنقل» فتنقل الملكية إلى الخيط المستهدف — وهي عملية لا تتطلب نسخًا، لكن الخيط الأصلي يفقد حق الوصول — وهي مناسبة للسيناريوهات التي تتضمن ArrayBuffers كبيرة وما شابهها.

📖 ملخص


📝 تمارين

  1. أكمل جميع أمثلة الأكواد الواردة في هذا الدرس وتأكد من أن كل منها يعمل بشكل صحيح.
  2. قم بتعديل المثال الشامل وأضف الإضافات الخاصة بك
  3. راجع الوثائق الرسمية، وحدد واجهة برمجة تطبيقات (API) واحدة أو اثنتين لم يتم تناولهما في هذا الدرس، واكتب كود اختبار لهما.
  4. التأمل: كيف ستطبق ما تعلمته في هذا الدرس على مشروع في الواقع العملي؟
  5. حاول أن تجمع بين ما تعلمته في هذا الدرس والمواد التي درستها في الدروس السابقة لإنشاء مشروع صغير.
Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%