Skip to content

تتبع السياق غير المتزامن

[مستقر: 2 - مستقر]

مستقر: 2 استقرار: 2 - مستقر

رمز المصدر: lib/async_hooks.js

مقدمة

تُستخدم هذه الفئات لربط الحالة ونشرها في جميع عمليات الاستدعاء الراجعة وسلاسل الوعود. تتيح تخزين البيانات طوال عمر طلب ويب أو أي مدة غير متزامنة أخرى. وهي مشابهة لتخزين محلي للخيوط في لغات أخرى.

تُعد فئتا AsyncLocalStorage و AsyncResource جزءًا من وحدة node:async_hooks:

js
import { AsyncLocalStorage, AsyncResource } from 'node:async_hooks'
js
const { AsyncLocalStorage, AsyncResource } = require('node:async_hooks')

الفئة: AsyncLocalStorage

[السجل]

الإصدارالتغييرات
v16.4.0أصبح AsyncLocalStorage الآن مستقرًا. كان تجريبيًا سابقًا.
v13.10.0، v12.17.0تمت الإضافة في: v13.10.0، v12.17.0

تقوم هذه الفئة بإنشاء مخازن تبقى متماسكة عبر العمليات غير المتزامنة.

بينما يمكنك إنشاء تنفيذك الخاص أعلى وحدة node:async_hooks، يجب تفضيل AsyncLocalStorage لأنه تنفيذ عالي الأداء وآمن للذاكرة ينطوي على تحسينات كبيرة يصعب تنفيذها.

يوضح المثال التالي استخدام AsyncLocalStorage لإنشاء مسجل بسيط يعين معرفات لطلبات HTTP الواردة ويتضمنها في الرسائل المسجلة ضمن كل طلب.

js
import http from 'node:http'
import { AsyncLocalStorage } from 'node:async_hooks'

const asyncLocalStorage = new AsyncLocalStorage()

function logWithId(msg) {
  const id = asyncLocalStorage.getStore()
  console.log(`${id !== undefined ? id : '-'}:`, msg)
}

let idSeq = 0
http
  .createServer((req, res) => {
    asyncLocalStorage.run(idSeq++, () => {
      logWithId('start')
      // تخيل أي سلسلة من العمليات غير المتزامنة هنا
      setImmediate(() => {
        logWithId('finish')
        res.end()
      })
    })
  })
  .listen(8080)

http.get('http://localhost:8080')
http.get('http://localhost:8080')
// يطبع:
//   0: start
//   1: start
//   0: finish
//   1: finish
js
const http = require('node:http')
const { AsyncLocalStorage } = require('node:async_hooks')

const asyncLocalStorage = new AsyncLocalStorage()

function logWithId(msg) {
  const id = asyncLocalStorage.getStore()
  console.log(`${id !== undefined ? id : '-'}:`, msg)
}

let idSeq = 0
http
  .createServer((req, res) => {
    asyncLocalStorage.run(idSeq++, () => {
      logWithId('start')
      // تخيل أي سلسلة من العمليات غير المتزامنة هنا
      setImmediate(() => {
        logWithId('finish')
        res.end()
      })
    })
  })
  .listen(8080)

http.get('http://localhost:8080')
http.get('http://localhost:8080')
// يطبع:
//   0: start
//   1: start
//   0: finish
//   1: finish

تحافظ كل مثيل من AsyncLocalStorage على سياق تخزين مستقل. يمكن أن توجد أمثلة متعددة بأمان في وقت واحد دون خطر التدخل في بيانات بعضها البعض.

new AsyncLocalStorage()

[السجل]

الإصدارالتغييرات
v19.7.0، v18.16.0تمت إزالة خيار onPropagate التجريبي.
v19.2.0، v18.13.0إضافة خيار onPropagate.
v13.10.0، v12.17.0تمت الإضافة في: v13.10.0، v12.17.0

يُنشئ مثيلًا جديدًا من AsyncLocalStorage. لا يُوفر التخزين إلا ضمن مُكالمة run() أو بعد مُكالمة enterWith().

طريقة ثابتة: AsyncLocalStorage.bind(fn)

تمت الإضافة في: v19.8.0، v18.16.0

[مستقر: 1 - تجريبي]

مستقر: 1 الثبات: 1 - تجريبي

  • fn <Function> الدالة التي سيتم ربطها بسياق التنفيذ الحالي.
  • الإرجاع: <Function> دالة جديدة تستدعي fn ضمن سياق التنفيذ المُلتقط.

يربط الدالة المعطاة بسياق التنفيذ الحالي.

طريقة ثابتة: AsyncLocalStorage.snapshot()

تمت الإضافة في: v19.8.0، v18.16.0

[مستقر: 1 - تجريبي]

مستقر: 1 الثبات: 1 - تجريبي

  • الإرجاع: <Function> دالة جديدة ذات توقيع (fn: (...args) : R, ...args) : R.

يلتقط سياق التنفيذ الحالي ويعيد دالة تقبل دالة كوسيطة. عندما يتم استدعاء الدالة المُرجعة، فإنها تستدعي الدالة المُمررة إليها ضمن السياق المُلتقط.

js
const asyncLocalStorage = new AsyncLocalStorage()
const runInAsyncScope = asyncLocalStorage.run(123, () => AsyncLocalStorage.snapshot())
const result = asyncLocalStorage.run(321, () => runInAsyncScope(() => asyncLocalStorage.getStore()))
console.log(result) // يُرجع 123

يمكن أن يحل AsyncLocalStorage.snapshot() محل استخدام AsyncResource لأغراض تتبع سياق غير متزامن بسيط، على سبيل المثال:

js
class Foo {
  #runInAsyncScope = AsyncLocalStorage.snapshot()

  get() {
    return this.#runInAsyncScope(() => asyncLocalStorage.getStore())
  }
}

const foo = asyncLocalStorage.run(123, () => new Foo())
console.log(asyncLocalStorage.run(321, () => foo.get())) // يُرجع 123

asyncLocalStorage.disable()

مضاف في: v13.10.0، v12.17.0

[مستقر: 1 - تجريبي]

مستقر: 1 استقرار: 1 - تجريبي

يعطل مثيل AsyncLocalStorage. ستعيد جميع المكالمات اللاحقة إلى asyncLocalStorage.getStore() قيمة undefined حتى يتم استدعاء asyncLocalStorage.run() أو asyncLocalStorage.enterWith() مرة أخرى.

عند استدعاء asyncLocalStorage.disable(), سيتم الخروج من جميع السياقات الحالية المرتبطة بالمثيل.

يُطلب استدعاء asyncLocalStorage.disable() قبل أن يتمكن asyncLocalStorage من التجميع بواسطة جامع القمامة. لا ينطبق هذا على المخازن التي يوفرها asyncLocalStorage, حيث يتم تجميع هذه الكائنات بواسطة جامع القمامة مع الموارد غير المتزامنة المقابلة.

استخدم هذه الطريقة عندما لا يكون asyncLocalStorage قيد الاستخدام بعد الآن في العملية الحالية.

asyncLocalStorage.getStore()

مضاف في: v13.10.0، v12.17.0

يعيد المخزن الحالي. إذا تم استدعاء هذه الدالة خارج سياق غير متزامن تم تهيئته باستدعاء asyncLocalStorage.run() أو asyncLocalStorage.enterWith(), فإنها تُعيد undefined.

asyncLocalStorage.enterWith(store)

مضاف في: v13.11.0، v12.17.0

[مستقر: 1 - تجريبي]

مستقر: 1 استقرار: 1 - تجريبي

ينتقل إلى السياق لبقية التنفيذ المتزامن الحالي ثم يُبقي المخزن عبر أي مكالمات غير متزامنة لاحقة.

مثال:

js
const store = { id: 1 }
// يستبدل المخزن السابق بكائن المخزن المعطى
asyncLocalStorage.enterWith(store)
asyncLocalStorage.getStore() // يُعيد كائن المخزن
someAsyncOperation(() => {
  asyncLocalStorage.getStore() // يُعيد نفس الكائن
})

سيستمر هذا الانتقال طوال التنفيذ المتزامن بأكمله. هذا يعني أنه إذا تم الدخول إلى السياق، على سبيل المثال، ضمن مُعالِج أحداث، فإن مُعالِجي الأحداث اللاحقين سيعملون أيضًا ضمن هذا السياق ما لم يتم ربطهم تحديدًا بسياق آخر باستخدام AsyncResource. لهذا السبب، يجب أن يُفضل run() على enterWith() ما لم تكن هناك أسباب قوية لاستخدام الطريقة الأخيرة.

js
const store = { id: 1 }

emitter.on('my-event', () => {
  asyncLocalStorage.enterWith(store)
})
emitter.on('my-event', () => {
  asyncLocalStorage.getStore() // يُعيد نفس الكائن
})

asyncLocalStorage.getStore() // يُعيد undefined
emitter.emit('my-event')
asyncLocalStorage.getStore() // يُعيد نفس الكائن

asyncLocalStorage.run(store, callback[, ...args])

أضيف في: v13.10.0، v12.17.0

يقوم بتشغيل دالة بشكل متزامن داخل سياق ويعيد قيمة إرجاعها. لا يمكن الوصول إلى المخزن خارج دالة الاستدعاء. يمكن الوصول إلى المخزن من قبل أي عمليات غير متزامنة تم إنشاؤها داخل دالة الاستدعاء.

يتم تمرير الوسائط الاختيارية args إلى دالة الاستدعاء.

إذا قامت دالة الاستدعاء بإلقاء خطأ، فسيتم إلقاء الخطأ بواسطة run() أيضًا. لا يتأثر تتبع المكدس بهذه المكالمة ويتم الخروج من السياق.

مثال:

js
const store = { id: 2 }
try {
  asyncLocalStorage.run(store, () => {
    asyncLocalStorage.getStore() // يعيد كائن المخزن
    setTimeout(() => {
      asyncLocalStorage.getStore() // يعيد كائن المخزن
    }, 200)
    throw new Error()
  })
} catch (e) {
  asyncLocalStorage.getStore() // يعيد undefined
  // سيتم التقاط الخطأ هنا
}

asyncLocalStorage.exit(callback[, ...args])

أضيف في: v13.10.0، v12.17.0

[مستقر: 1 - تجريبي]

مستقر: 1 الثبات: 1 - تجريبي

يقوم بتشغيل دالة بشكل متزامن خارج سياق ويعيد قيمة إرجاعها. لا يمكن الوصول إلى المخزن داخل دالة الاستدعاء أو العمليات غير المتزامنة التي تم إنشاؤها داخل دالة الاستدعاء. أي مكالمة getStore() تتم داخل دالة الاستدعاء ستعيد دائمًا undefined.

يتم تمرير الوسائط الاختيارية args إلى دالة الاستدعاء.

إذا قامت دالة الاستدعاء بإلقاء خطأ، فسيتم إلقاء الخطأ بواسطة exit() أيضًا. لا يتأثر تتبع المكدس بهذه المكالمة ويتم إعادة الدخول إلى السياق.

مثال:

js
// ضمن مكالمة إلى run
try {
  asyncLocalStorage.getStore() // يعيد كائن المخزن أو القيمة
  asyncLocalStorage.exit(() => {
    asyncLocalStorage.getStore() // يعيد undefined
    throw new Error()
  })
} catch (e) {
  asyncLocalStorage.getStore() // يعيد نفس الكائن أو القيمة
  // سيتم التقاط الخطأ هنا
}

الاستخدام مع async/await

إذا كان داخل دالة async، يجب تشغيل await مرة واحدة فقط داخل سياق ما، فيجب استخدام النمط التالي:

js
async function fn() {
  await asyncLocalStorage.run(new Map(), () => {
    asyncLocalStorage.getStore().set('key', value)
    return foo() // سيتم انتظار قيمة الإرجاع من foo
  })
}

في هذا المثال، المخزن متوفر فقط داخل دالة الارتجاع والدوال التي تدعوها foo. خارج run، فإن استدعاء getStore سيرجع undefined.

استكشاف الأخطاء وإصلاحها: فقدان السياق

في معظم الحالات، يعمل AsyncLocalStorage بدون مشاكل. في حالات نادرة، يضيع المخزن الحالي في إحدى العمليات غير المتزامنة.

إذا كان كودك قائمًا على الارتجاع، يكفي تحويله إلى وعود باستخدام util.promisify() حتى يبدأ العمل مع الوعود الأصلية.

إذا كنت بحاجة إلى استخدام واجهة برمجة تطبيقات قائمة على الارتجاع أو افترض كودك تنفيذًا قابلًا للتنفيذ مخصصًا، فاستخدم فئة AsyncResource لربط العملية غير المتزامنة بسياق التنفيذ الصحيح. ابحث عن دعوة الدالة المسؤولة عن فقدان السياق عن طريق تسجيل محتوى asyncLocalStorage.getStore() بعد الدعوات التي تشك في أنها مسؤولة عن الخسارة. عندما يسجل الكود undefined، فإن آخر دالة ارتجاع تم استدعاؤها هي المسؤولة على الأرجح عن فقدان السياق.

الفئة: AsyncResource

[السجل]

الإصدارالتغييرات
v16.4.0أصبح AsyncResource الآن مستقرًا. كان تجريبيًا سابقًا.

تم تصميم فئة AsyncResource لتوسيعها بواسطة موارد المُدمِج غير المتزامنة. باستخدام هذا، يمكن للمستخدمين بسهولة تشغيل أحداث دورة حياة مواردهم الخاصة.

سيتم تشغيل خطاف init عند إنشاء AsyncResource.

فيما يلي نظرة عامة على واجهة برمجة تطبيقات AsyncResource.

js
import { AsyncResource, executionAsyncId } from 'node:async_hooks'

// AsyncResource() يُقصد بها أن تُوسّع. إنشاء
// AsyncResource() جديد يُشغّل أيضًا init. إذا تم حذف triggerAsyncId، فسيتم استخدام
// async_hook.executionAsyncId().
const asyncResource = new AsyncResource(type, { triggerAsyncId: executionAsyncId(), requireManualDestroy: false })

// تشغيل دالة في سياق تنفيذ المورد. سيفعل هذا
// * إنشاء سياق المورد
// * تشغيل AsyncHooks قبل عمليات الارتجاع
// * استدعاء الدالة المُقدّمة `fn` مع الوسائط المُقدّمة
// * تشغيل AsyncHooks بعد عمليات الارتجاع
// * استعادة سياق التنفيذ الأصلي
asyncResource.runInAsyncScope(fn, thisArg, ...args)

// استدعاء عمليات ارتجاع AsyncHooks destroy.
asyncResource.emitDestroy()

// إرجاع المعرف الفريد المُعيّن لمثيل AsyncResource.
asyncResource.asyncId()

// إرجاع معرف المُشغّل لمثيل AsyncResource.
asyncResource.triggerAsyncId()
js
const { AsyncResource, executionAsyncId } = require('node:async_hooks')

// AsyncResource() يُقصد بها أن تُوسّع. إنشاء
// AsyncResource() جديد يُشغّل أيضًا init. إذا تم حذف triggerAsyncId، فسيتم استخدام
// async_hook.executionAsyncId().
const asyncResource = new AsyncResource(type, { triggerAsyncId: executionAsyncId(), requireManualDestroy: false })

// تشغيل دالة في سياق تنفيذ المورد. سيفعل هذا
// * إنشاء سياق المورد
// * تشغيل AsyncHooks قبل عمليات الارتجاع
// * استدعاء الدالة المُقدّمة `fn` مع الوسائط المُقدّمة
// * تشغيل AsyncHooks بعد عمليات الارتجاع
// * استعادة سياق التنفيذ الأصلي
asyncResource.runInAsyncScope(fn, thisArg, ...args)

// استدعاء عمليات ارتجاع AsyncHooks destroy.
asyncResource.emitDestroy()

// إرجاع المعرف الفريد المُعيّن لمثيل AsyncResource.
asyncResource.asyncId()

// إرجاع معرف المُشغّل لمثيل AsyncResource.
asyncResource.triggerAsyncId()

new AsyncResource(type[, options])

  • type <string> نوع الحدث غير المتزامن.
  • options <Object>
    • triggerAsyncId <number> معرف سياق التنفيذ الذي أنشأ هذا الحدث غير المتزامن. افتراضيًا: executionAsyncId().
    • requireManualDestroy <boolean> إذا تم تعيينه على true، فإنه يُعطل emitDestroy عند جمع البيانات المهملة للكائن. وعادةً ما لا يلزم تعيين هذا (حتى إذا تم استدعاء emitDestroy يدويًا)، إلا إذا تم استرداد asyncId الخاص بالمورد وتم استدعاء emitDestroy الخاص بواجهة برمجة التطبيقات الحساسة به. عندما يتم تعيينه على false، فإن استدعاء emitDestroy عند جمع البيانات المهملة سيحدث فقط إذا كان هناك خطاف destroy نشط واحد على الأقل. افتراضيًا: false.

مثال على الاستخدام:

js
class DBQuery extends AsyncResource {
  constructor(db) {
    super('DBQuery')
    this.db = db
  }

  getInfo(query, callback) {
    this.db.get(query, (err, data) => {
      this.runInAsyncScope(callback, null, err, data)
    })
  }

  close() {
    this.db = null
    this.emitDestroy()
  }
}

طريقة ثابتة: AsyncResource.bind(fn[, type[, thisArg]])

[السجل]

الإصدارالتغييرات
v20.0.0خاصية asyncResource المضافة إلى الدالة المرتبطة قد عفا عليها الزمن وسيتم إزالتها في إصدار مستقبلي.
v17.8.0، v16.15.0تم تغيير الإعداد الافتراضي عندما يكون thisArg غير مُعرّف لاستخدام this من المُستدعي.
v16.0.0تمت إضافة thisArg الاختياري.
v14.8.0، v12.19.0تمت الإضافة في: v14.8.0، v12.19.0
  • fn <Function> الدالة التي سيتم ربطها بسياق التنفيذ الحالي.
  • type <string> اسم اختياري لربطه بـAsyncResource الأساسي.
  • thisArg <any>

يربط الدالة المعطاة بسياق التنفيذ الحالي.

asyncResource.bind(fn[, thisArg])

[السجل]

الإصدارالتغييرات
v20.0.0تم إهمال خاصية asyncResource المضافة إلى الدالة المُرتبطة، وسيتم إزالتها في إصدار مستقبلي.
v17.8.0، v16.15.0تم تغيير القيمة الافتراضية عندما تكون thisArg غير مُعرّفة لاستخدام this من المُستدعي.
v16.0.0تمت إضافة thisArg الاختياري.
v14.8.0، v12.19.0تمت الإضافة في: v14.8.0، v12.19.0
  • fn <Function> الدالة التي سيتم ربطها بنطاق AsyncResource الحالي.
  • thisArg <any>

يربط الدالة المعطاة للتنفيذ بنطاق AsyncResource هذا.

asyncResource.runInAsyncScope(fn[, thisArg, ...args])

تمت الإضافة في: v9.6.0

  • fn <Function> الدالة التي سيتم استدعاؤها في سياق تنفيذ مورد async هذا.
  • thisArg <any> المُستقبِل الذي سيتم استخدامه لاستدعاء الدالة.
  • ...args <any> وسيطات اختيارية لإرسالها إلى الدالة.

استدعِ الدالة المُقدّمة مع الوسيطات المُقدّمة في سياق تنفيذ مورد async. سيؤدي هذا إلى إنشاء السياق، وتشغيل AsyncHooks قبل المُستدعيات، واستدعاء الدالة، وتشغيل AsyncHooks بعد المُستدعيات، ثم استعادة سياق التنفيذ الأصلي.

asyncResource.emitDestroy()

  • القيمة المُرجعة: <AsyncResource> مرجع إلى asyncResource.

استدعِ جميع مُعلّقات destroy. يجب أن يتم استدعاء هذا مرة واحدة فقط. سيتم إرسال خطأ إذا تم استدعاؤه أكثر من مرة. يجب إجراء هذا يدويًا. إذا تم ترك المورد ليتم جمعه بواسطة GC، فلن يتم استدعاء مُعلّقات destroy أبدًا.

asyncResource.asyncId()

  • الإرجاع: <number> asyncId الفريد المُعيّن للمورد.

asyncResource.triggerAsyncId()

  • الإرجاع: <number> نفس triggerAsyncId الذي يتم تمريره إلى مُنشئ AsyncResource.

استخدام AsyncResource لبركة خيوط Worker

يُظهر المثال التالي كيفية استخدام فئة AsyncResource لتوفير تتبع غير متزامن بشكل صحيح لبركة Worker. يمكن لبرك الموارد الأخرى، مثل برك اتصال قواعد البيانات، اتباع نموذج مشابه.

بافتراض أن المهمة هي إضافة رقمين، باستخدام ملف اسمه task_processor.js مع المحتوى التالي:

js
import { parentPort } from 'node:worker_threads'
parentPort.on('message', task => {
  parentPort.postMessage(task.a + task.b)
})
js
const { parentPort } = require('node:worker_threads')
parentPort.on('message', task => {
  parentPort.postMessage(task.a + task.b)
})

يمكن لبركة عامل حولها استخدام البنية التالية:

js
import { AsyncResource } from 'node:async_hooks'
import { EventEmitter } from 'node:events'
import { Worker } from 'node:worker_threads'

const kTaskInfo = Symbol('kTaskInfo')
const kWorkerFreedEvent = Symbol('kWorkerFreedEvent')

class WorkerPoolTaskInfo extends AsyncResource {
  constructor(callback) {
    super('WorkerPoolTaskInfo')
    this.callback = callback
  }

  done(err, result) {
    this.runInAsyncScope(this.callback, null, err, result)
    this.emitDestroy() // `TaskInfo`s are used only once.
  }
}

export default class WorkerPool extends EventEmitter {
  constructor(numThreads) {
    super()
    this.numThreads = numThreads
    this.workers = []
    this.freeWorkers = []
    this.tasks = []

    for (let i = 0; i < numThreads; i++) this.addNewWorker()

    // في كل مرة يتم فيها إصدار kWorkerFreedEvent، قم بتوزيع
    // المهمة التالية المعلقة في قائمة الانتظار، إن وجدت.
    this.on(kWorkerFreedEvent, () => {
      if (this.tasks.length > 0) {
        const { task, callback } = this.tasks.shift()
        this.runTask(task, callback)
      }
    })
  }

  addNewWorker() {
    const worker = new Worker(new URL('task_processor.js', import.meta.url))
    worker.on('message', result => {
      // في حالة النجاح: اتصل بالدالة المُرجعة التي تم تمريرها إلى `runTask`،
      // قم بإزالة `TaskInfo` المرتبط بـ Worker، وعلامته على أنه مجاني
      // مرة أخرى.
      worker[kTaskInfo].done(null, result)
      worker[kTaskInfo] = null
      this.freeWorkers.push(worker)
      this.emit(kWorkerFreedEvent)
    })
    worker.on('error', err => {
      // في حالة استثناء غير معالج: اتصل بالدالة المُرجعة التي تم تمريرها إلى
      // `runTask` مع الخطأ.
      if (worker[kTaskInfo]) worker[kTaskInfo].done(err, null)
      else this.emit('error', err)
      // قم بإزالة العامل من القائمة وابدأ عاملًا جديدًا ليحل محل
      // العامل الحالي.
      this.workers.splice(this.workers.indexOf(worker), 1)
      this.addNewWorker()
    })
    this.workers.push(worker)
    this.freeWorkers.push(worker)
    this.emit(kWorkerFreedEvent)
  }

  runTask(task, callback) {
    if (this.freeWorkers.length === 0) {
      // لا توجد خيوط مجانية، انتظر حتى يصبح خيط عامل مجانيًا.
      this.tasks.push({ task, callback })
      return
    }

    const worker = this.freeWorkers.pop()
    worker[kTaskInfo] = new WorkerPoolTaskInfo(callback)
    worker.postMessage(task)
  }

  close() {
    for (const worker of this.workers) worker.terminate()
  }
}
js
const { AsyncResource } = require('node:async_hooks')
const { EventEmitter } = require('node:events')
const path = require('node:path')
const { Worker } = require('node:worker_threads')

const kTaskInfo = Symbol('kTaskInfo')
const kWorkerFreedEvent = Symbol('kWorkerFreedEvent')

class WorkerPoolTaskInfo extends AsyncResource {
  constructor(callback) {
    super('WorkerPoolTaskInfo')
    this.callback = callback
  }

  done(err, result) {
    this.runInAsyncScope(this.callback, null, err, result)
    this.emitDestroy() // `TaskInfo`s are used only once.
  }
}

class WorkerPool extends EventEmitter {
  constructor(numThreads) {
    super()
    this.numThreads = numThreads
    this.workers = []
    this.freeWorkers = []
    this.tasks = []

    for (let i = 0; i < numThreads; i++) this.addNewWorker()

    // في كل مرة يتم فيها إصدار kWorkerFreedEvent، قم بتوزيع
    // المهمة التالية المعلقة في قائمة الانتظار، إن وجدت.
    this.on(kWorkerFreedEvent, () => {
      if (this.tasks.length > 0) {
        const { task, callback } = this.tasks.shift()
        this.runTask(task, callback)
      }
    })
  }

  addNewWorker() {
    const worker = new Worker(path.resolve(__dirname, 'task_processor.js'))
    worker.on('message', result => {
      // في حالة النجاح: اتصل بالدالة المُرجعة التي تم تمريرها إلى `runTask`،
      // قم بإزالة `TaskInfo` المرتبط بـ Worker، وعلامته على أنه مجاني
      // مرة أخرى.
      worker[kTaskInfo].done(null, result)
      worker[kTaskInfo] = null
      this.freeWorkers.push(worker)
      this.emit(kWorkerFreedEvent)
    })
    worker.on('error', err => {
      // في حالة استثناء غير معالج: اتصل بالدالة المُرجعة التي تم تمريرها إلى
      // `runTask` مع الخطأ.
      if (worker[kTaskInfo]) worker[kTaskInfo].done(err, null)
      else this.emit('error', err)
      // قم بإزالة العامل من القائمة وابدأ عاملًا جديدًا ليحل محل
      // العامل الحالي.
      this.workers.splice(this.workers.indexOf(worker), 1)
      this.addNewWorker()
    })
    this.workers.push(worker)
    this.freeWorkers.push(worker)
    this.emit(kWorkerFreedEvent)
  }

  runTask(task, callback) {
    if (this.freeWorkers.length === 0) {
      // لا توجد خيوط مجانية، انتظر حتى يصبح خيط عامل مجانيًا.
      this.tasks.push({ task, callback })
      return
    }

    const worker = this.freeWorkers.pop()
    worker[kTaskInfo] = new WorkerPoolTaskInfo(callback)
    worker.postMessage(task)
  }

  close() {
    for (const worker of this.workers) worker.terminate()
  }
}

module.exports = WorkerPool

بدون التتبع الصريح الذي أضافته كائنات WorkerPoolTaskInfo، سيظهر أن الدوال المُرجعة مرتبطة بكائنات Worker الفردية. ومع ذلك، فإن إنشاء كائنات Worker غير مرتبط بإنشاء المهام ولا يوفر معلومات حول موعد جدولة المهام.

يمكن استخدام هذه البركة على النحو التالي:

js
import WorkerPool from './worker_pool.js'
import os from 'node:os'

const pool = new WorkerPool(os.availableParallelism())

let finished = 0
for (let i = 0; i < 10; i++) {
  pool.runTask({ a: 42, b: 100 }, (err, result) => {
    console.log(i, err, result)
    if (++finished === 10) pool.close()
  })
}
js
const WorkerPool = require('./worker_pool.js')
const os = require('node:os')

const pool = new WorkerPool(os.availableParallelism())

let finished = 0
for (let i = 0; i < 10; i++) {
  pool.runTask({ a: 42, b: 100 }, (err, result) => {
    console.log(i, err, result)
    if (++finished === 10) pool.close()
  })
}

دمج AsyncResource مع EventEmitter

قد يتم تشغيل مُستمعي الأحداث التي يتم تشغيلها بواسطة EventEmitter في سياق تنفيذ مختلف عن السياق النشط عند استدعاء eventEmitter.on().

يُظهر المثال التالي كيفية استخدام فئة AsyncResource لربط مُستمع الأحداث بشكل صحيح بسياق التنفيذ الصحيح. يمكن تطبيق نفس النهج على Stream أو فئة مدفوعة بالأحداث مشابهة.

js
import { createServer } from 'node:http'
import { AsyncResource, executionAsyncId } from 'node:async_hooks'

const server = createServer((req, res) => {
  req.on(
    'close',
    AsyncResource.bind(() => {
      // سياق التنفيذ مرتبط بنطاق خارجي حالي.
    })
  )
  req.on('close', () => {
    // سياق التنفيذ مرتبط بالنطاق الذي تسبب في إصدار "close".
  })
  res.end()
}).listen(3000)
js
const { createServer } = require('node:http')
const { AsyncResource, executionAsyncId } = require('node:async_hooks')

const server = createServer((req, res) => {
  req.on(
    'close',
    AsyncResource.bind(() => {
      // سياق التنفيذ مرتبط بنطاق خارجي حالي.
    })
  )
  req.on('close', () => {
    // سياق التنفيذ مرتبط بالنطاق الذي تسبب في إصدار "close".
  })
  res.end()
}).listen(3000)