واجهة مجمع مؤشّرات الترابط Win32 ── تزامن دون إنشاء مؤشّرات ترابط، عبر CreateThreadpoolWork

· · Windows, تعدّد مؤشّرات الترابط, C++, تطوير Windows, Win32 API, تحسين الأداء

«CreateThread واحد لكلّ عميل.» «واحد للمؤقِّت.» «واحد لانتظار حدث.» ── في شيفرة Windows الأصليّة، تميل مؤشّرات الترابط إلى التكاثر هكذا. كلّ مؤشّر ترابط يستهلك مكدّساً وكائن نواة، ولإنشائها وإتلافها تكلفة أيضاً. العمل دقيق الحبيبات، لكن مؤشّر الترابط ثقيل ── مجمع مؤشّرات الترابط هو ما يوفِّره نظام التشغيل لامتصاص ذلك التفاوت.

شهرة راحة ThreadPool وTask.Run في .NET معروفة، لكن في الواقع لدى Win32 الأصليّ أيضاً واجهة مجمع مؤشّرات ترابط قياسيّة في نظام التشغيل، حسنَة التصميم. أُعيد تصميم هذه الواجهة بشكل شامل في Windows Vista، وهي أساس التزامن الأصليّ: يمكنها معالجة العمل والمؤقِّتات والانتظارات والإدخال/الإخراج غير المتزامن عبر آليّة ردّ نداء موحَّدة. موجَّه إلى المطوِّرين الذين يكتبون تطبيقات Windows وخدمات وDLLs بـC/C++، يشرح هذا المقال بنية هذه الواجهة واستخدامها، والمطبّات السهل الوقوع فيها، استناداً إلى المصادر الأوّليّة.

1. الخلاصة أوّلاً

  • لإصدار عدد كبير من المهام قصيرة العمر، ولاستبدال مؤشّرات الترابط المخصَّصة للانتظار فقط، يتفوّق مجمع مؤشّرات الترابط على CreateThread الخاصّ بك. تترك إدارة مؤشّرات الترابط لنظام التشغيل ويمكنك تقليل عددها وتبديلات السياق.1
  • ما ينبغي استخدامه هو الواجهة الجديدة (عائلة CreateThreadpoolWork). جعلتها إعادة تصميم Vista أبسط وأكثر موثوقيّة وأعلى أداءً من الواجهة القديمة (عائلة QueueUserWorkItem)، ويمكنك أيضاً إنشاء عدّة مجامع مستقلّة في عمليّة واحدة.12
  • هناك أربعة أنواع من الكائنات. work، الذي تقدِّم إليه المهام؛ وtimer، الذي يطلق في وقت أو دورة؛ وwait، الذي يطلق عندما يُشار إلى كائن نواة؛ وio، الذي يطلق عند اكتمال إدخال/إخراج غير متزامن. كلّها تركب آليّة ردّ النداء نفسها.3
  • الإيقاف هو «انتظر، ثمّ أغلق». انضباط عدم ترك ردّ نداء يعمل ── انتظار الاكتمال بعائلة WaitForThreadpoolWorkCallbacks، أو معالجته جماعيّاً عبر مجموعة تنظيف ── مطلوب.4
  • داخل ردّ النداء: لا تحجب لوقت طويل (إن فعلت، CallbackMayRunLong)؛ لا تنتظر الاكتمال بشكل متزامن على المجمع نفسه؛ لا توسِّخ حالة مؤشّر الترابط. هذه الثلاث قواعد حديديّة.56
  • الاستخدام من DLL: احذر سباق إلغاء التحميل. انتظر الاكتمال في دالّة إيقاف صريحة، واعرف الواجهات المخصَّصة مثل FreeLibraryWhenCallbackReturns.3

2. لماذا مجمع، ومتى مجمع

فكرة مجمع مؤشّرات الترابط بسيطة. بدلاً من إنشاء مؤشّر ترابط لكلّ مهمّة، ترمي المهام (ردود النداء) إلى مجموعة مؤشّرات ترابط عامل يديرها نظام التشغيل. ينفِّذ العمّال المهام واحدة تلو الأخرى، ويضبط نظام التشغيل العدد حسب الحمل.

تسرد الوثائق الرسميّة أنواعاً ملموسة من التطبيقات حيث يُجدي المجمع.1

  • تطبيقات تُصدر عدداً كبيراً من عناصر العمل الصغيرة على التوازي (بحث، إدخال/إخراج شبكة، وغيرها)
  • تطبيقات تنشئ كثيراً وتمزِّق مؤشّرات ترابط قصيرة العمر
  • تطبيقات تعالج عملاً مستقلّاً على التوازي في الخلفيّة
  • تطبيقات تحتفظ بـمؤشّرات ترابط مخصَّصة لانتظار كائنات نواة أو أحداث

العنصر الأخير سهل الإغفال. إذا كان لديك خمسة مؤشّرات ترابط موجودة فقط للنوم حتّى «تعمل عندما يُشار إلى الحدث»، يمكن استبدالها بخمسة كائنات wait على المجمع، ويُجمَّع الانتظار على مؤشّرات ترابط الانتظار في المجمع.

بالمقابل، ثمّة عمل لا يناسب مجمعاً. عمل يحتاج تغييراً في أولويّة مؤشّر الترابط، أو يتطلّب COM STA، أو يستمرّ طوال عمر العمليّة ── العمل الذي يحتاج «شخصيّة» على مؤشّر الترابط يُحفَظ على مؤشّر ترابط مخصَّص. مؤشّر ترابط العامل مورد مشترك؛ يُستعار.

الاختيار بين مؤشّر ترابط مخصَّص ومجمعافحص أوّلاً هل تُحتاج شخصيّة مؤشّر ترابط كالأولويّة أو STA، وهل يعمل وقتاً طويلاً؛ العمل قصير العمر أو عالي الحجم أو على شكل انتظار الذي لا يطابق أياً منهما يذهب إلى مجمع مؤشّرات الترابطنعملانعملاتحتاج شخصيّة كالأولويّة أو STA؟أبقه على مؤشّر مخصَّصهل يعمل وقتاً طويلاً؟ضعه على مجمع الترابطعمل قصير، انتظار، مؤقِّتات، اكتمال I/O

الشكل 1: العمل الوحيد الذي يجوز وضعه على مجمع هو عمل «لا يحتاج شخصيّة وينتهي قصيراً». كلّ شيء آخر يبقى على مؤشّر ترابط مخصَّص كما كان.

ثمّة أيضاً قطعة تاريخ يجب تثبيتها. لواجهة مجمع مؤشّرات الترابط جيلان. الواجهة القديمة المستمرّة منذ Windows 2000 (QueueUserWorkItem وRegisterWaitForSingleObject وغيرها)، والواجهة الجديدة التي أُعيد تصميمها بشكل شامل في Vista (عائلة CreateThreadpoolWork). توحِّد الواجهة الجديدة أنواع مؤشّر ترابط العامل، وتوفِّر مؤشّرات ترابط دائمة مخصَّصة، وعدّة مجامع في عمليّة واحدة، ومجموعات تنظيف، وغير ذلك، وتنصّ الوثائق الرسميّة صراحةً على أنّها «أبسط وأكثر موثوقيّة وأفضل أداءً وأكثر مرونة».1 للواجهة القديمة أيضاً قيود بنيويّة مثل «لا يمكنك إلغاء عمل بعد وضعه في الطابور».2 من هنا فصاعداً، يعالج هذا المقال الواجهة الجديدة فقط.

التقابل بين واجهة مجمع الترابط القديمة والجديدةQueueUserWorkItem في الواجهة القديمة تقابل كائن work في الجديدة، وطوابير المؤقِّتات تقابل timer، والانتظارات المسجَّلة تقابل wait، وBindIoCompletionCallback تقابل ioQueueUserWorkItemworkطوابير المؤقِّتاتtimerانتظارات مسجَّلةwaitBindIoCompletionCallbackio

الشكل 2: هدف الترحيل من الواجهة القديمة واحد لواحد. يمكن لجرد الشيفرة القائمة أن يبدأ من هذا التقابل.

3. الكائنات الأربعة ── work وtimer وwait وio

في مركز الواجهة الجديدة أربعة أنواع من الكائنات تختلف شروط إطلاق ردّ ندائها.3

الكائن دالّة الإنشاء متى يُطلَق ردّ النداء
work CreateThreadpoolWork عندما يُقدَّم بـSubmitThreadpoolWork
timer CreateThreadpoolTimer عندما يحين الوقت أو الدورة المحدَّدة
wait CreateThreadpoolWait عندما يصبح كائن نواة مُشاراً إليه
io CreateThreadpoolIo عندما يكتمل إدخال/إخراج غير متزامن على المقبض المرتبط
كائنات مجمع الترابط الأربعة وآليّة ردّ النداءيُطلَق work عند تقديم صريح، وtimer عند الوقت، وwait عند إشارة كائن نواة، وio عند اكتمال إدخال/إخراج غير متزامن؛ كلّها تعمل ردود نداء على مجموعة مؤشّرات ترابط العامل نفسهاأيّ كائن؟work(عند التقديم)timer أو wait أو io؟timer(وقت / دورة)wait أو io؟wait(عند الإشارة)io(اكتمال I/O)العمّال ينفِّذون ردّ النداء

الشكل 3: شروط الإطلاق تختلف، لكن الأربعة موحَّدة في آليّة «ينفِّذ العمّال على المجمع نفسه ردّ النداء».

هذا التوحيد قوّة عمليّة. بدلاً من كتابة معالجة دوريّة واستجابة لحدث ومعالجة اكتمال إدخال/إخراج كلّ منها على مؤشّر ترابط مخصَّص، يمكنك محاذاتها على أسلوب ردّ نداء واحد. تُجمَع المؤقِّتات في طابور مؤقِّت واحد للمجمع كلّه، وتُجمَّع الانتظارات على عدد صغير من مؤشّرات ترابط الانتظار ── مؤشّرات الترابط التي «تنام فقط» تختفي من العمليّة.1

استبدال مؤشّرات انتظار فقط بكائنات waitمؤشّرات الترابط المخصَّصة للانتظار التي كانت تنام واحداً لكلّ حدث تصبح كائنات wait وتُجمَّع على مؤشّرات انتظار المجمع، فيعمل ردّ النداء فقط عند الإشارة5 مؤشّرات انتظار تنام فرادىتستهلك 5 مكدّسات و5 مؤشّرات5 كائنات waitتُجمَّع على مؤشّرات انتظار المجمعردّ النداء يعمل عند الإشارة فقط

الشكل 4: مؤشّرات الترابط التي «تنام وتنتظر فقط» يمكن إزالتها بتحويلها إلى كائنات wait. هذه نقلة أولى واضحة لترحيل إلى مجمع.

4. النمط الأساسيّ ── ذهاب وإياب واحد مع كائن work

نمرّ على الآداب مرّة مع كائن work، الأكثر استخداماً.4

VOID CALLBACK WorkCallback(PTP_CALLBACK_INSTANCE instance,
                           PVOID context, PTP_WORK work)
{
    // The context is fixed at creation time. Per-item data is passed through a synchronised queue
    WORK_QUEUE* queue = (WORK_QUEUE*)context;
    ITEM* item = Dequeue(queue);        // Take one item under exclusive control
    ProcessItem(item);
}

// 1) Create (bind the callback to the shared context = the queue)
PTP_WORK work = CreateThreadpoolWork(WorkCallback, &queue, NULL);
if (!work) { /* Failure handling with GetLastError */ }

// 2) Submit once for each item you enqueue (keep the item count and the submit count in step)
Enqueue(&queue, item);
SubmitThreadpoolWork(work);

// 3) Stop the submitter, then wait for completion (TRUE also attempts to cancel work that has not yet started)
WaitForThreadpoolWorkCallbacks(work, FALSE);

// 4) Close
CloseThreadpoolWork(work);

نقطتان يجب الإمساك بهما. أولاً، يجوز SubmitThreadpoolWork لكائن work نفسه أكثر من مرّة. كلّ تقديم يشغِّل ردّ النداء (على التوازي).7 لكن السياق الممرَّر إلى ردّ النداء ثابت وقت الإنشاء، لذا عندما تكتب «N عنصراً من نوع العمل نفسه» بكائن work واحد، تضع طابوراً متزامناً في السياق كما في الشيفرة أعلاه وتأخذ عنصراً واحداً لكلّ تقديم (تصميم ينشئ كائن work لكلّ عنصر جائز أيضاً). ثانياً، انتظر الاكتمال دائماً قبل الإغلاق. إغلاق الكائن بينما يبقى ردّ نداء يعمل أو في الطابور، أو تحرير ذاكرة يشير إليها ردّ النداء، استخدام بعد التحرير كما هو. لجعل هذا الانتظار إغلاقاً آمناً، إيقاف المقدِّم أوّلاً شرط مسبق ── في بنية يمكن فيها لمؤشّر ترابط آخر أن Submit على التوازي مع الانتظار، تقديم بعد الانتظار يتسابق مع Close. تمرير TRUE وسيطاً ثانياً لـWaitForThreadpoolWorkCallbacks يحاول أيضاً إلغاء تقديمات لم تبدأ بعد.

دورة حياة كائن workأنشئ بـ CreateThreadpoolWork؛ قدِّم بـ SubmitThreadpoolWork وتعمل ردود النداء على التوازي. عند الإيقاف، أوقف التقديمات الجديدة أوّلاً، وانتظر اكتمال كلّ ردود النداء بـ WaitForThreadpoolWorkCallbacks، ثمّ أغلق بـ CloseThreadpoolWorkأنشئ بـ CreateThreadpoolWorkقدِّم بـ SubmitThreadpoolWork(قابل للتكرار)ردود النداء تعمل على التوازيأوقف التقديمات الجديدةانتظر الاكتمال بـ WaitForThreadpoolWorkCallbacksأغلق بـ CloseThreadpoolWork

الشكل 5: ترتيب الإيقاف هو «أوقف التقديمات → انتظر الاكتمال → أغلق». تخطَّ أياً منها فتحصل على استخدام بعد التحرير أو سباق.

افتراضيّاً، تعمل ردود النداء على مجمع العمليّة الافتراضيّ. لكثير من الاستخدامات ذلك يكفي. الفصل التالي عندما تريد تقسيم المجامع.

5. المجامع المخصَّصة ومجموعات التنظيف

قسِّم المجمع. يمكنك إنشاء مجمع مستقلّ بـCreateThreadpool وضبط الحدّ الأعلى والأدنى لعدد مؤشّرات الترابط بـSetThreadpoolThreadMaximum / SetThreadpoolThreadMinimum.8 الاستخدام النموذجيّ هو العزل. حتّى لا يأكل «عمل دفعيّ قد يكون بطيئاً» عمّال «عمل يحتاج استجابة فوريّة»، تقسِّم المجامع وتعطي كلّاً ميزانيّته من مؤشّرات الترابط.

اربطها ببيئة ردّ نداء. أيّ مجمع يعمل عليه العمل يُحدَّد بتهيئة TP_CALLBACK_ENVIRON (بيئة ردّ نداء)، والإشارة إليها إلى المجمع بـSetThreadpoolCallbackPool، وتمرير ذلك وسيطاً ثالثاً لـCreateThreadpoolWork وما شابه.9

اطوها بمجموعة تنظيف. في وحدة تنشئ كثيراً من الكائنات، تميل معالجة الإيقاف إلى أن تصبح تلاوة «انتظر كلّها، أغلق كلّها». إذا أنشأت مجموعة بـCreateThreadpoolCleanupGroup وأرفقت كلّ كائن بها عبر بيئة ردّ النداء، فإنّ CloseThreadpoolCleanupGroupMembers واحداً ينفِّذ انتظار الاكتمال والتحرير لكلّ كائنات الأعضاء معاً.34

ربط الإعداد عبر بيئة ردّ النداءتشير بيئة ردّ النداء إلى مجمع مخصَّص ومجموعة تنظيف؛ يعمل work وtimer المنشأَان بتلك البيئة على ذلك المجمع، وتجمع عمليّة جماعيّة على مجموعة التنظيف انتظار الاكتمال والتحريربيئة ردّ النداء(TP_CALLBACK_ENVIRON)مجمع مخصَّص(تحكّم بالعدد)مجموعة تنظيفتُمرَّر عند إنشاء work / timer / wait / ioانتظر الاكتمال وحرِّر دفعة واحدة

الشكل 6: بيئة ردّ النداء هي الآليّة التي تحقن «على أيّ مجمع يعمل، ومن ينظِّف» وقت إنشاء الكائن.

6. مطبّات ── الانضباط داخل ردّ النداء

تكاد كلّ علّة في مجمع مؤشّرات الترابط تأتي من «التصرّف كما تشاء على مؤشّر ترابط مستعار».

الحجب لوقت طويل. يضبط المجمع عدد مؤشّرات الترابط على افتراض أنّ ردود النداء تعود بسرعة. عمل طويل أو انتظار طويل بالافتراضيّات يؤخِّر تنفيذ ردود النداء الأخرى. ردّ نداء قد يعمل طويلاً ينبغي أن يعلن «هذا سيعمل طويلاً» بـCallbackMayRunLong (يتّخذه المجمع تلميحاً لإضافة مؤشّر ترابط)، أو يُرسَل إلى مؤشّر ترابط مخصَّص أصلاً. لاحظ أنّ CallbackMayRunLong يُرجع FALSE عندما لا يستطيع تجهيز عامل لردود النداء الأخرى. إن واصلت الحجب دون التحقّق من قيمة الإرجاع فما زلت تسدّ المجمع، لذا عندما تكون FALSE، انزل إلى الجانب الذي لا يحجب ── قسِّم العمل، أرسله إلى مؤشّر ترابط مخصَّص، وهكذا.5

انتظار الاكتمال بشكل متزامن على المجمع نفسه. شكل تنتظر فيه، داخل ردّ النداء أ، بـWaitForThreadpoolWorkCallbacks أو ما شابه اكتمال عمل ب مقدَّم إلى المجمع نفسه يصبح جمود تجويع المجمع في اللحظة التي يكون فيها كلّ عامل «ينتظر عاملاً آخر». أعد كتابة اعتماد بين مهام لا كانتظار بل كاستمرار «يقدِّم التالي من ردّ نداء اكتمال ب».

بنية جمود تجويع المجمعإذا انتظر كلّ مؤشّر ترابط عامل بشكل متزامن اكتمال عمل آخر مقدَّم إلى المجمع نفسه، لا يبقى عامل حرّ لتشغيل ذلك العمل، وينتظر الجميع إلى الأبدالعامل 1: ينتظر اكتمال العمل Xالعملان X وY ينتظران التشغيلالعامل 2: ينتظر اكتمال العمل Yلا عامل حرّ لتشغيلهماالجميع ينتظر إلى الأبد(جمود تجويع)

الشكل 7: إذا انتظرت عاملاً بشكل متزامن من داخل عامل، لا يبقى من يشغِّل العمل المُنتظَر.

توسيخ حالة مؤشّر الترابط. يُعاد استخدام مؤشّر ترابط العامل لردّ النداء التالي. تغيير أولويّة مؤشّر الترابط، أو حالة تهيئة COM، أو قيمة متروكة في TLS، أو قفل نسيت مغادرته ── أيّ من هذه يصبح تلويثاً لردّ النداء التالي (غير المتعلّق). «دالّة ترميها إلى مجمع يجب ألا تعتمد على شخصيّة مؤشّر الترابط» تحذير رسميّ منذ عصر الواجهة القديمة.6 ثمّة آليّات مخصَّصة للتنظيف؛ مثلاً، يمكن لـLeaveCriticalSectionWhenCallbackReturns أن يطلب من المجمع «حرِّر هذا القفل عندما يعود ردّ النداء هذا».3

سباق مع إلغاء تحميل DLL. إذا أُلغي تحميل الـDLL الذي يحوي الشيفرة بينما ردّ النداء يعمل، تحصل على انتهاك وصول. الشكل الأساسيّ هو انتظار الاكتمال بدقّة في دالّة إيقاف الـDLL؛ تُوفَّر FreeLibraryWhenCallbackReturns للحالة «ردّ النداء هذا هو المهمّة الأخيرة، وعندما ينتهي أريد تحرير الـDLL بما فيه أنا». لكن هذه الواجهة فقط «تُفلِت مرجعاً واحداً عندما يعود ردّ النداء العامل»؛ ولا تمنع إلغاء تحميل قبل بدء ردّ النداء. تستخدمها زوجاً: خذ مرجع وحدة خاصّاً بك بـGetModuleHandleEx قبل التقديم، واجعل ردّ النداء يُفلِت ذلك المرجع بهذه الواجهة.3 ويجب ألا تفعل انتظار الاكتمال هذا داخل DllMain ── كما ورد في «DllMain وقفل المحمِّل»، انتظار مؤشّر ترابط آخر داخل DllMain نمط جمود.

الاستعداد لسباق بين إلغاء تحميل DLL وردّ نداءإلغاء تحميل الـDLL بينما ردّ النداء يعمل يصبح انتهاك وصول، لذا الشكل الأساسيّ هو انتظار الاكتمال في دالّة إيقاف صريحة ثمّ الإغلاق؛ عندما يحرِّر آخر ردّ نداء نفسه الـDLL، استخدم FreeLibraryWhenCallbackReturnsإلغاء تحميل أثناء ردّ النداءانتهاك وصولانتظر، ثمّ أغلقإلغاء تحميل آمنفي دالّة إيقافFreeLibraryWhenCallbackReturnsآخر ردّ نداء يحرِّر الـDLLليس داخل DllMain

الشكل 8: الشكل الأساسيّ هو فعل «انتظر، ثمّ أغلق» في دالّة إيقاف صريحة. الانتظار داخل DllMain يدعو إلى جمود مختلف.

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

7. علاقة هذا بالمكتبة القياسيّة و.NET ── في أيّ طبقة تكتب

أخيراً، نرتِّب كيف يجلس هذا مع الأدوات الأخرى.

  • إذا كفت std::async / std::thread في C++، فهما المرشّح الأوّل. قابلتان للنقل، والشيفرة قصيرة، وحتّى دلالات future محسومة بالقياس.10
  • أسباب استخدام مجمع مؤشّرات الترابط Win32 مباشرةً هي عندما (1) تريد آليّة ردّ نداء موحَّدة تشمل timer وwait وio، أو (2) تريد تقسيم مجمع أو تحكّماً بعدد مؤشّرات الترابط، أو (3) لا تريد الاحتفاظ بمؤشّرات ترابط خاصّة داخل DLL أو مكوِّن COM.
  • في جانب .NET، يلعب ThreadPool وTask الدور نفسه، وارتباط اكتمال الإدخال/الإخراج بـIOCP. هذه البنية التحتيّة مشروحة في «IOCP ومجمع مؤشّرات الترابط في .NET».
تقرير أيّ طبقة أدوات تكتب بهاإذا كفت أدوات C++ القياسيّة async أو thread فاستخدمها؛ استخدم مجمع مؤشّرات الترابط Win32 مباشرةً عندما تحتاج تكامل مؤقِّتات وانتظارات واكتمالات إدخال/إخراج، أو تقسيم مجمع أو تحكّم بالعدد، أو لا تريد مؤشّرات ترابط خاصّة داخل DLL أو COMنعملاتكامل timer وwait وioتقسيم مجمع / تحكّم بالعددتجنّب ترابط خاص داخل DLLهل تكفي أدوات C++ القياسيّة؟std::async / std::threadماذا تحتاج؟مجمع ترابط Win32

الشكل 9: عند الشكّ، ابدأ بالمكتبة القياسيّة؛ دور هذه الواجهة يأتي عندما يظهر متطلَّب لا تستطيع التعبير عنه.

بعبارة أخرى، هذه الواجهة أساس التزامن عند نقطة قرَّرت فيها «الكتابة أصليّاً». تنظيف واقعيّ متدرِّج: هدف الترحيل من تكاثر CreateThread الخاصّ بك، ابدأ بإدخال كائن work، ثمّ استبدل مؤشّرات الترابط المخصَّصة للانتظار فقط بـwait ومؤشّرات ترابط المؤقِّت بـtimer.

8. الخلاصة

  • لإصدار عدد كبير من المهام قصيرة العمر، ولتنظيف مؤشّرات الترابط المخصَّصة للانتظار فقط وللمؤقِّت فقط، مجمع مؤشّرات الترابط القياسيّ في نظام التشغيل لا مؤشّرات ترابطك. ما تستخدمه هو الواجهة الجديدة من Vista فصاعداً.
  • في المركز الكائنات الأربعة work وtimer وwait وio. شروط الإطلاق تختلف؛ وهي موحَّدة على مجموعة العمّال نفسها وأسلوب ردّ النداء نفسه.
  • الآداب هي «أنشئ → قدِّم → انتظر الاكتمال → أغلق». التقديمات المتعدّدة تعمل على التوازي. يمكن لمجموعة تنظيف تجميع معالجة الإيقاف.
  • القواعد الحديديّة الثلاث لردّ النداء: لا تحجب لوقت طويل (إن فعلت، CallbackMayRunLong)؛ لا تنتظر بشكل متزامن على المجمع نفسه؛ لا توسِّخ حالة مؤشّر الترابط.
  • الاستخدام من DLL: احذر سباق إلغاء التحميل. انتظر الاكتمال في دالّة إيقاف صريحة؛ لا تفعله في DllMain.
  • حيث تكفي C++ القياسيّة أو .NET، استخدمها. دور هذه الواجهة عندما تحتاج تكامل timer/wait/io أو تحكّم بالمجمع.

واجهة مجمع مؤشّرات الترابط، من بين واجهات Win32، في الجانب الأحدث والأفضل تصميماً. متى أنهيت الانتقال من فكرة «إنشاء مؤشّر ترابط» إلى فكرة «رمي ردّ نداء»، يصبح التزامن في الشيفرة الأصليّة أوضح كتابةً بكثير.

مقالات ذات صلة

مجالات الاستشارة ذات الصلة

تتعامل شركة كومورا سوفت ذ.م.م. مع تصميم الترحيل من شيفرة أصليّة تكاثرت مؤشّرات ترابطها إلى مجمع مؤشّرات الترابط، ومراجعات تصميم المعالجة المتزامنة في تطبيقات C++ وDLLs، والتحقيق في أسباب التجمّد والانهيار الناتجة عن تجويع المجمع أو ردود النداء. مرحَّب باستشارتنا بدءاً من جرد الشيفرة القائمة.

روابط مرجعيّة

  1. Microsoft Learn, Thread Pools. حول كون مجمع مؤشّرات الترابط مجموعة مؤشّرات ترابط عامل تنفِّذ بكفاءة ردود نداء غير متزامنة نيابةً عن تطبيق؛ وحول أنواع التطبيقات التي تناسبه (إصدار عدد كبير من عناصر العمل الصغيرة على التوازي، والإنشاء والتمزيق المتكرِّر لمؤشّرات ترابط قصيرة العمر، ومعالجة عمل مستقلّ على التوازي، والانتظارات الحصريّة على كائنات النواة، وغيرها)؛ وحول إعادة تصميم Vista الشاملة (توحيد أنواع مؤشّر ترابط العامل، وطابور مؤقِّت واحد، ومؤشّرات ترابط دائمة مخصَّصة، ومجموعات تنظيف، وعدّة مجامع في عمليّة واحدة، والواجهة الجديدة).  2 3 4 5

  2. Microsoft Learn, Thread Pooling. حول بنية واجهات مجمع مؤشّرات الترابط الأقدم (QueueUserWorkItem، وطوابير المؤقِّتات، والانتظارات المسجَّلة، وBindIoCompletionCallback)؛ وحول عدم وجود طريقة لإلغاء عمل بعد وضعه في الطابور؛ وحول النصّ على أنّ واجهة مجمع مؤشّرات الترابط الجديدة المقدَّمة في Vista أبسط وأفضل في الموثوقيّة والأداء والمرونة.  2

  3. Microsoft Learn, threadpoolapiset.h header. حول قائمة الدوال التي تشمل دوال إنشاء الكائنات الأربعة CreateThreadpoolWork وCreateThreadpoolTimer وCreateThreadpoolWait وCreateThreadpoolIo؛ ومجموعات التنظيف (CreateThreadpoolCleanupGroup)؛ والتنظيف المرتبط باكتمال ردّ النداء (LeaveCriticalSectionWhenCallbackReturns وFreeLibraryWhenCallbackReturns وغيرها).  2 3 4 5 6

  4. Microsoft Learn, Using the Thread Pool Functions. حول الإجراء الأساسيّ للإنشاء بـCreateThreadpoolWork، والتقديم بـSubmitThreadpoolWork، وانتظار الاكتمال بـWaitForThreadpoolWorkCallbacks، والإغلاق بـCloseThreadpoolWork؛ وحول مثال إعداد يجمع مجمعاً مخصَّصاً مع بيئة ردّ نداء ومجموعة تنظيف.  2 3

  5. Microsoft Learn, CallbackMayRunLong function (threadpoolapiset.h). حول إشعار المجمع بأنّ ردّ النداء الحاليّ قد يعمل وقتاً طويلاً، فيستخدم المجمع ذلك مادّة لتقرير ما إذا كان سيؤمِّن مؤشّر ترابط لردود النداء الأخرى؛ وحول النظر في مؤشّر ترابط مخصَّص لردّ نداء طويل الأمد حيث أمكن.  2

  6. Microsoft Learn, Thread Pooling. حول وجوب كون عناصر العمل المقدَّمة إلى مجمع مؤشّرات الترابط، والدوال التي تستدعيها، آمنة للمجمع؛ وحول عدم افتراض أنّ مؤشّر الترابط المنفِّذ مؤشّر ترابط مخصَّص دائم؛ وحول تجنّب استخدام TLS والاستدعاءات غير المتزامنة التي تتطلّب مؤشّر ترابط دائماً.  2

  7. Microsoft Learn, SubmitThreadpoolWork function (threadpoolapiset.h). حول إمكان تقديم كائن work نفسه أكثر من مرّة دون انتظار اكتمال ردّ نداء سابق، فتعمل ردود النداء على التوازي؛ وحول إمكان ضبط المجمع (خنق) عدد مؤشّرات الترابط للكفاءة. 

  8. Microsoft Learn, SetThreadpoolThreadMaximum function (threadpoolapiset.h). حول إمكان ضبط حدّ أعلى لعدد مؤشّرات ترابط العامل لمجمع أُنشئ بـCreateThreadpool (الحدّ الأدنى هو SetThreadpoolThreadMinimum). 

  9. Microsoft Learn, CreateThreadpoolWork function (threadpoolapiset.h). حول إنشاء كائن work من دالّة ردّ نداء ومؤشِّر سياق؛ وحول إمكان تحديد الوسيط الثالث، TP_CALLBACK_ENVIRON، لبيئة تنفيذ ردّ النداء (المجمع الذي ينتمي إليه، وغيرها)، مع معنى NULL أنّه يعمل في البيئة الافتراضيّة. 

  10. Microsoft Learn, <future>. حول توفير التنفيذ غير المتزامن لكلّ مهمّة عبر std::async وfuture كمكتبة قياسيّة، فيمكنك كتابة التزامن دون إدارة مؤشّرات الترابط مباشرةً. 

أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.

ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.

الأسئلة الشائعة

أسئلة شائعة حول موضوع هذه المقالة.

ما الذي يميِّز مجمع مؤشّرات الترابط عن إنشاء مؤشّرات ترابط خاصّة بك عبر CreateThread؟
الكفاءة عندما يكون لديك عدد كبير من المهام قصيرة العمر، وتقليل شيفرة إدارة مؤشّرات الترابط. لإنشاء مؤشّر ترابط وإتلافه تكلفة لا يمكن تجاهلها، فالتطبيق الذي يكرِّر «CreateThread لكلّ مهمّة وإتلافه عند الانتهاء»، أو الذي يحتفظ بعدد كبير من مؤشّرات الترابط الموجودة فقط للنوم في انتظار حدث، يمكنه تقليل عدد مؤشّرات الترابط وتبديلات السياق بالانتقال إلى مجمع. تسرد الوثائق الرسميّة أيضاً كمرشّحين للمجمع التطبيقات التي تُصدر عدداً كبيراً من عناصر العمل الصغيرة على التوازي، والتطبيقات التي تنشئ كثيراً من مؤشّرات الترابط قصيرة العمر، والتطبيقات التي لديها مؤشّرات ترابط مخصَّصة فقط لانتظار كائنات النواة. بالمقابل، العمل الذي «يحتاج شخصيّة خاصّة به على مؤشّر الترابط» ── تغيير أولويّة، COM STA، معالجة مخصَّصة طويلة الأمد ── ينبغي الإبقاء عليه على مؤشّر ترابط مخصَّص كما كان.
ما الفرق عن دوال مجمع مؤشّرات الترابط الأقدم مثل QueueUserWorkItem؟
أُعيد تصميم مجمع مؤشّرات الترابط بشكل شامل في Windows Vista. واجهات threadpoolapiset الحاليّة (CreateThreadpoolWork وغيرها) هي الواجهة الجديدة؛ أمّا QueueUserWorkItem وRegisterWaitForSingleObject وما شابه فهي الواجهة القديمة (التراثيّة). توحِّد الواجهة الجديدة أنواع مؤشّرات ترابط العامل، وتتيح إنشاء عدّة مجامع مستقلّة في عمليّة واحدة، وتوفِّر آليّات مثل التحرير الجماعيّ عبر مجموعة تنظيف وتحرير قفل أو إلغاء تحميل DLL مرتبط باكتمال ردّ النداء. تقول الوثائق الرسميّة أيضاً إنّ الواجهة الجديدة أبسط وأفضل في الموثوقيّة والأداء والمرونة. للواجهة القديمة قيود بنيويّة أيضاً، مثل «لا توجد طريقة لإلغاء عمل بعد وضعه في الطابور»، لذا استخدم الواجهة الجديدة في الشيفرة الجديدة.
هل ثمّة أمور يجب ألا تفعلها داخل ردّ النداء؟
ثلاث كبيرة. أولاً، الحجب لوقت طويل أو عمل طويل بالافتراضيّات. يضبط المجمع عدد مؤشّرات الترابط على افتراض أنّ ردود النداء تنتهي بسرعة، لذا للعمل الذي سيستغرق وقتاً طويلاً إمّا تعلنه بـCallbackMayRunLong أو تستخدم مؤشّر ترابط مخصَّصاً. ثانياً، انتظار اكتمال عمل آخر قدَّمتَه إلى المجمع نفسه بشكل متزامن. إذا انتهى كلّ عامل إلى «انتظار عامل آخر»، تحصل على جمود تجويع المجمع. ثالثاً، الاعتماد على شخصيّة مؤشّر الترابط. مؤشّرات ترابط العامل مشتركة عبر ردود النداء، فالعودة بأولويّة مؤشّر ترابط أو حالة تهيئة COM مغيَّرة، أو ترك حالة في TLS، يلوِّث ردّ النداء التالي. للتنظيف في النهاية (تحرير قفل أو إلغاء تحميل DLL)، تُوفَّر آليّات مخصَّصة مثل LeaveCriticalSectionWhenCallbackReturns وFreeLibraryWhenCallbackReturns.
هل ثمّة نقاط يجب الانتباه إليها عند استخدام مجمع مؤشّرات الترابط من DLL؟
الخطر الأكبر هو «إلغاء تحميل الـDLL بينما ردّ النداء ما زال يعمل». إذا عمل ردّ النداء بعد إلغاء التحميل، تحصل على انتهاك وصول. يجب على جانب الـDLL، في معالجة الإيقاف، انتظار اكتمال ردود النداء التي أصدرها بموثوقيّة ── بدالّة انتظار مثل WaitForThreadpoolWorkCallbacks، أو CloseThreadpoolCleanupGroupMembers على مجموعة تنظيف ── ثمّ إغلاق الكائنات فقط بعد ذلك. لكن انتظار هذا داخل DllMain يمكن أن يجمد عبر التفاعل مع قفل المحمِّل، لذا القاعدة هي فعل ذلك في دالّة إيقاف صريحة، لا في DllMain. للحالة التي يريد فيها ردّ النداء نفسه تحرير الـDLL لأنّ «هذا العمل هو الأخير»، تُوفَّر واجهة مخصَّصة، FreeLibraryWhenCallbackReturns.
بعد وجود C++ std::async وThreadPool في .NET، هل ما زالت ثمّة مناسبات لاستخدام هذه الواجهة مباشرةً؟
نعم. المعيار هو «هل تكفي الأداة في تلك الطبقة». إذا كانت حبيبات التزامن التي تحتاجها في C++ مغطّاة بـstd::async أو std::thread، فالمكتبة القياسيّة هي المرشّح الأوّل من منظور قابليّة النقل أيضاً. في المقابل، الرغبة في توحيد المؤقِّتات وانتظارات كائنات النواة واكتمالات الإدخال/الإخراج غير المتزامن في آليّة ردّ نداء واحدة؛ والرغبة في تقسيم المجامع والتحكّم بعدد مؤشّرات الترابط حسب نوع العمل؛ وعدم الرغبة في الاحتفاظ بمؤشّرات ترابط خاصّة داخل DLL أو مكوِّن COM ── تلك المتطلّبات هي ما يغطّيه مجمع مؤشّرات الترابط Win32. العلاقة مع ThreadPool في .NET وIOCP مشروحة في مقال ذي صلة، وما دمت تكتب أصليّاً، فمعرفة هذه الآليّة التي تقع في الطبقة الأدنى ليست هدراً.

الملف الشخصي للمؤلف

صفحة الملف الشخصي لمؤلف المقالة.

غو كومورا

مؤسّس شركة كومورا سوفت ذ.م.م.

يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.

العودة إلى المدونة