واجهة مجمع مؤشّرات الترابط 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، أو يستمرّ طوال عمر العمليّة ── العمل الذي يحتاج «شخصيّة» على مؤشّر الترابط يُحفَظ على مؤشّر ترابط مخصَّص. مؤشّر ترابط العامل مورد مشترك؛ يُستعار.
flowchart TB
accTitle: الاختيار بين مؤشّر ترابط مخصَّص ومجمع
accDescr: افحص أوّلاً هل تُحتاج شخصيّة مؤشّر ترابط كالأولويّة أو STA، وهل يعمل وقتاً طويلاً؛ العمل قصير العمر أو عالي الحجم أو على شكل انتظار الذي لا يطابق أياً منهما يذهب إلى مجمع مؤشّرات الترابط
q1{"تحتاج شخصيّة كالأولويّة أو STA؟"} -->|"نعم"| ded["أبقه على مؤشّر مخصَّص"]
q1 -->|"لا"| q2{"هل يعمل وقتاً طويلاً؟"}
q2 -->|"نعم"| ded
q2 -->|"لا"| pool["ضعه على مجمع الترابط"]
pool -.-> ex["عمل قصير، انتظار، مؤقِّتات، اكتمال I/O"]
الشكل 1: العمل الوحيد الذي يجوز وضعه على مجمع هو عمل «لا يحتاج شخصيّة وينتهي قصيراً». كلّ شيء آخر يبقى على مؤشّر ترابط مخصَّص كما كان.
ثمّة أيضاً قطعة تاريخ يجب تثبيتها. لواجهة مجمع مؤشّرات الترابط جيلان. الواجهة القديمة المستمرّة منذ Windows 2000 (QueueUserWorkItem وRegisterWaitForSingleObject وغيرها)، والواجهة الجديدة التي أُعيد تصميمها بشكل شامل في Vista (عائلة CreateThreadpoolWork). توحِّد الواجهة الجديدة أنواع مؤشّر ترابط العامل، وتوفِّر مؤشّرات ترابط دائمة مخصَّصة، وعدّة مجامع في عمليّة واحدة، ومجموعات تنظيف، وغير ذلك، وتنصّ الوثائق الرسميّة صراحةً على أنّها «أبسط وأكثر موثوقيّة وأفضل أداءً وأكثر مرونة».1 للواجهة القديمة أيضاً قيود بنيويّة مثل «لا يمكنك إلغاء عمل بعد وضعه في الطابور».2 من هنا فصاعداً، يعالج هذا المقال الواجهة الجديدة فقط.
flowchart TB
accTitle: التقابل بين واجهة مجمع الترابط القديمة والجديدة
accDescr: QueueUserWorkItem في الواجهة القديمة تقابل كائن work في الجديدة، وطوابير المؤقِّتات تقابل timer، والانتظارات المسجَّلة تقابل wait، وBindIoCompletionCallback تقابل io
o1["QueueUserWorkItem"] --> n1["work"]
o2["طوابير المؤقِّتات"] --> n2["timer"]
o5["انتظارات مسجَّلة"] --> n3["wait"]
o4["BindIoCompletionCallback"] --> n4["io"]
الشكل 2: هدف الترحيل من الواجهة القديمة واحد لواحد. يمكن لجرد الشيفرة القائمة أن يبدأ من هذا التقابل.
3. الكائنات الأربعة ── work وtimer وwait وio
في مركز الواجهة الجديدة أربعة أنواع من الكائنات تختلف شروط إطلاق ردّ ندائها.3
| الكائن | دالّة الإنشاء | متى يُطلَق ردّ النداء |
|---|---|---|
| work | CreateThreadpoolWork | عندما يُقدَّم بـSubmitThreadpoolWork |
| timer | CreateThreadpoolTimer | عندما يحين الوقت أو الدورة المحدَّدة |
| wait | CreateThreadpoolWait | عندما يصبح كائن نواة مُشاراً إليه |
| io | CreateThreadpoolIo | عندما يكتمل إدخال/إخراج غير متزامن على المقبض المرتبط |
flowchart TB
accTitle: كائنات مجمع الترابط الأربعة وآليّة ردّ النداء
accDescr: يُطلَق work عند تقديم صريح، وtimer عند الوقت، وwait عند إشارة كائن نواة، وio عند اكتمال إدخال/إخراج غير متزامن؛ كلّها تعمل ردود نداء على مجموعة مؤشّرات ترابط العامل نفسها
kind{"أيّ كائن؟"}
kind --> w["work(عند التقديم)"]
kind --> more{"timer أو wait أو io؟"}
more --> t["timer(وقت / دورة)"]
more --> rest{"wait أو io؟"}
rest --> wt["wait(عند الإشارة)"]
rest --> io["io(اكتمال I/O)"]
w --> pool["العمّال ينفِّذون ردّ النداء"]
t --> pool
wt --> pool
io --> pool
الشكل 3: شروط الإطلاق تختلف، لكن الأربعة موحَّدة في آليّة «ينفِّذ العمّال على المجمع نفسه ردّ النداء».
هذا التوحيد قوّة عمليّة. بدلاً من كتابة معالجة دوريّة واستجابة لحدث ومعالجة اكتمال إدخال/إخراج كلّ منها على مؤشّر ترابط مخصَّص، يمكنك محاذاتها على أسلوب ردّ نداء واحد. تُجمَع المؤقِّتات في طابور مؤقِّت واحد للمجمع كلّه، وتُجمَّع الانتظارات على عدد صغير من مؤشّرات ترابط الانتظار ── مؤشّرات الترابط التي «تنام فقط» تختفي من العمليّة.1
flowchart TB
accTitle: استبدال مؤشّرات انتظار فقط بكائنات wait
accDescr: مؤشّرات الترابط المخصَّصة للانتظار التي كانت تنام واحداً لكلّ حدث تصبح كائنات wait وتُجمَّع على مؤشّرات انتظار المجمع، فيعمل ردّ النداء فقط عند الإشارة
old2["5 مؤشّرات انتظار تنام فرادى"] -.-> waste["تستهلك 5 مكدّسات و5 مؤشّرات"]
new2["5 كائنات wait"] --> agg["تُجمَّع على مؤشّرات انتظار المجمع"]
agg --> cb2["ردّ النداء يعمل عند الإشارة فقط"]
الشكل 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 يحاول أيضاً إلغاء تقديمات لم تبدأ بعد.
flowchart TB
accTitle: دورة حياة كائن work
accDescr: أنشئ بـ CreateThreadpoolWork؛ قدِّم بـ SubmitThreadpoolWork وتعمل ردود النداء على التوازي. عند الإيقاف، أوقف التقديمات الجديدة أوّلاً، وانتظر اكتمال كلّ ردود النداء بـ WaitForThreadpoolWorkCallbacks، ثمّ أغلق بـ CloseThreadpoolWork
c["أنشئ بـ CreateThreadpoolWork"] --> s["قدِّم بـ SubmitThreadpoolWork(قابل للتكرار)"]
s --> run["ردود النداء تعمل على التوازي"]
run --> stop3["أوقف التقديمات الجديدة"]
stop3 --> w["انتظر الاكتمال بـ WaitForThreadpoolWorkCallbacks"]
w --> cl["أغلق بـ CloseThreadpoolWork"]
الشكل 5: ترتيب الإيقاف هو «أوقف التقديمات → انتظر الاكتمال → أغلق». تخطَّ أياً منها فتحصل على استخدام بعد التحرير أو سباق.
افتراضيّاً، تعمل ردود النداء على مجمع العمليّة الافتراضيّ. لكثير من الاستخدامات ذلك يكفي. الفصل التالي عندما تريد تقسيم المجامع.
5. المجامع المخصَّصة ومجموعات التنظيف
قسِّم المجمع. يمكنك إنشاء مجمع مستقلّ بـCreateThreadpool وضبط الحدّ الأعلى والأدنى لعدد مؤشّرات الترابط بـSetThreadpoolThreadMaximum / SetThreadpoolThreadMinimum.8 الاستخدام النموذجيّ هو العزل. حتّى لا يأكل «عمل دفعيّ قد يكون بطيئاً» عمّال «عمل يحتاج استجابة فوريّة»، تقسِّم المجامع وتعطي كلّاً ميزانيّته من مؤشّرات الترابط.
اربطها ببيئة ردّ نداء. أيّ مجمع يعمل عليه العمل يُحدَّد بتهيئة TP_CALLBACK_ENVIRON (بيئة ردّ نداء)، والإشارة إليها إلى المجمع بـSetThreadpoolCallbackPool، وتمرير ذلك وسيطاً ثالثاً لـCreateThreadpoolWork وما شابه.9
اطوها بمجموعة تنظيف. في وحدة تنشئ كثيراً من الكائنات، تميل معالجة الإيقاف إلى أن تصبح تلاوة «انتظر كلّها، أغلق كلّها». إذا أنشأت مجموعة بـCreateThreadpoolCleanupGroup وأرفقت كلّ كائن بها عبر بيئة ردّ النداء، فإنّ CloseThreadpoolCleanupGroupMembers واحداً ينفِّذ انتظار الاكتمال والتحرير لكلّ كائنات الأعضاء معاً.34
flowchart TB
accTitle: ربط الإعداد عبر بيئة ردّ النداء
accDescr: تشير بيئة ردّ النداء إلى مجمع مخصَّص ومجموعة تنظيف؛ يعمل work وtimer المنشأَان بتلك البيئة على ذلك المجمع، وتجمع عمليّة جماعيّة على مجموعة التنظيف انتظار الاكتمال والتحرير
env["بيئة ردّ النداء(TP_CALLBACK_ENVIRON)"] --> cp["مجمع مخصَّص(تحكّم بالعدد)"]
env --> cg["مجموعة تنظيف"]
env --> obj["تُمرَّر عند إنشاء work / timer / wait / io"]
cg -.-> close["انتظر الاكتمال وحرِّر دفعة واحدة"]
الشكل 6: بيئة ردّ النداء هي الآليّة التي تحقن «على أيّ مجمع يعمل، ومن ينظِّف» وقت إنشاء الكائن.
6. مطبّات ── الانضباط داخل ردّ النداء
تكاد كلّ علّة في مجمع مؤشّرات الترابط تأتي من «التصرّف كما تشاء على مؤشّر ترابط مستعار».
الحجب لوقت طويل. يضبط المجمع عدد مؤشّرات الترابط على افتراض أنّ ردود النداء تعود بسرعة. عمل طويل أو انتظار طويل بالافتراضيّات يؤخِّر تنفيذ ردود النداء الأخرى. ردّ نداء قد يعمل طويلاً ينبغي أن يعلن «هذا سيعمل طويلاً» بـCallbackMayRunLong (يتّخذه المجمع تلميحاً لإضافة مؤشّر ترابط)، أو يُرسَل إلى مؤشّر ترابط مخصَّص أصلاً. لاحظ أنّ CallbackMayRunLong يُرجع FALSE عندما لا يستطيع تجهيز عامل لردود النداء الأخرى. إن واصلت الحجب دون التحقّق من قيمة الإرجاع فما زلت تسدّ المجمع، لذا عندما تكون FALSE، انزل إلى الجانب الذي لا يحجب ── قسِّم العمل، أرسله إلى مؤشّر ترابط مخصَّص، وهكذا.5
انتظار الاكتمال بشكل متزامن على المجمع نفسه. شكل تنتظر فيه، داخل ردّ النداء أ، بـWaitForThreadpoolWorkCallbacks أو ما شابه اكتمال عمل ب مقدَّم إلى المجمع نفسه يصبح جمود تجويع المجمع في اللحظة التي يكون فيها كلّ عامل «ينتظر عاملاً آخر». أعد كتابة اعتماد بين مهام لا كانتظار بل كاستمرار «يقدِّم التالي من ردّ نداء اكتمال ب».
flowchart TB
accTitle: بنية جمود تجويع المجمع
accDescr: إذا انتظر كلّ مؤشّر ترابط عامل بشكل متزامن اكتمال عمل آخر مقدَّم إلى المجمع نفسه، لا يبقى عامل حرّ لتشغيل ذلك العمل، وينتظر الجميع إلى الأبد
w1["العامل 1: ينتظر اكتمال العمل X"] --> q["العملان X وY ينتظران التشغيل"]
w2["العامل 2: ينتظر اكتمال العمل Y"] --> q
q -.-> none["لا عامل حرّ لتشغيلهما"]
none -.-> dead["الجميع ينتظر إلى الأبد(جمود تجويع)"]
الشكل 7: إذا انتظرت عاملاً بشكل متزامن من داخل عامل، لا يبقى من يشغِّل العمل المُنتظَر.
توسيخ حالة مؤشّر الترابط. يُعاد استخدام مؤشّر ترابط العامل لردّ النداء التالي. تغيير أولويّة مؤشّر الترابط، أو حالة تهيئة COM، أو قيمة متروكة في TLS، أو قفل نسيت مغادرته ── أيّ من هذه يصبح تلويثاً لردّ النداء التالي (غير المتعلّق). «دالّة ترميها إلى مجمع يجب ألا تعتمد على شخصيّة مؤشّر الترابط» تحذير رسميّ منذ عصر الواجهة القديمة.6 ثمّة آليّات مخصَّصة للتنظيف؛ مثلاً، يمكن لـLeaveCriticalSectionWhenCallbackReturns أن يطلب من المجمع «حرِّر هذا القفل عندما يعود ردّ النداء هذا».3
سباق مع إلغاء تحميل DLL. إذا أُلغي تحميل الـDLL الذي يحوي الشيفرة بينما ردّ النداء يعمل، تحصل على انتهاك وصول. الشكل الأساسيّ هو انتظار الاكتمال بدقّة في دالّة إيقاف الـDLL؛ تُوفَّر FreeLibraryWhenCallbackReturns للحالة «ردّ النداء هذا هو المهمّة الأخيرة، وعندما ينتهي أريد تحرير الـDLL بما فيه أنا». لكن هذه الواجهة فقط «تُفلِت مرجعاً واحداً عندما يعود ردّ النداء العامل»؛ ولا تمنع إلغاء تحميل قبل بدء ردّ النداء. تستخدمها زوجاً: خذ مرجع وحدة خاصّاً بك بـGetModuleHandleEx قبل التقديم، واجعل ردّ النداء يُفلِت ذلك المرجع بهذه الواجهة.3 ويجب ألا تفعل انتظار الاكتمال هذا داخل DllMain ── كما ورد في «DllMain وقفل المحمِّل»، انتظار مؤشّر ترابط آخر داخل DllMain نمط جمود.
flowchart TB
accTitle: الاستعداد لسباق بين إلغاء تحميل DLL وردّ نداء
accDescr: إلغاء تحميل الـDLL بينما ردّ النداء يعمل يصبح انتهاك وصول، لذا الشكل الأساسيّ هو انتظار الاكتمال في دالّة إيقاف صريحة ثمّ الإغلاق؛ عندما يحرِّر آخر ردّ نداء نفسه الـDLL، استخدم FreeLibraryWhenCallbackReturns
risk["إلغاء تحميل أثناء ردّ النداء"] -.-> av["انتهاك وصول"]
g1["انتظر، ثمّ أغلق"] --> safe["إلغاء تحميل آمن"]
g1 -.-> g1N["في دالّة إيقاف"]
g2["FreeLibraryWhenCallbackReturns"] --> safe
g2 -.-> g2n["آخر ردّ نداء يحرِّر الـDLL"]
g1 -.-> ng["ليس داخل DllMain"]
av ~~~ g1
الشكل 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».
flowchart TB
accTitle: تقرير أيّ طبقة أدوات تكتب بها
accDescr: إذا كفت أدوات C++ القياسيّة async أو thread فاستخدمها؛ استخدم مجمع مؤشّرات الترابط Win32 مباشرةً عندما تحتاج تكامل مؤقِّتات وانتظارات واكتمالات إدخال/إخراج، أو تقسيم مجمع أو تحكّم بالعدد، أو لا تريد مؤشّرات ترابط خاصّة داخل DLL أو COM
q1{"هل تكفي أدوات C++ القياسيّة؟"} -->|"نعم"| std["std::async / std::thread"]
q1 -->|"لا"| q2{"ماذا تحتاج؟"}
q2 -->|"تكامل timer وwait وio"| tp["مجمع ترابط Win32"]
q2 -->|"تقسيم مجمع / تحكّم بالعدد"| tp
q2 -->|"تجنّب ترابط خاص داخل DLL"| tp
الشكل 9: عند الشكّ، ابدأ بالمكتبة القياسيّة؛ دور هذه الواجهة يأتي عندما يظهر متطلَّب لا تستطيع التعبير عنه.
بعبارة أخرى، هذه الواجهة أساس التزامن عند نقطة قرَّرت فيها «الكتابة أصليّاً». تنظيف واقعيّ متدرِّج: هدف الترحيل من تكاثر CreateThread الخاصّ بك، ابدأ بإدخال كائن work، ثمّ استبدل مؤشّرات الترابط المخصَّصة للانتظار فقط بـwait ومؤشّرات ترابط المؤقِّت بـtimer.
8. الخلاصة
- لإصدار عدد كبير من المهام قصيرة العمر، ولتنظيف مؤشّرات الترابط المخصَّصة للانتظار فقط وللمؤقِّت فقط، مجمع مؤشّرات الترابط القياسيّ في نظام التشغيل لا مؤشّرات ترابطك. ما تستخدمه هو الواجهة الجديدة من Vista فصاعداً.
- في المركز الكائنات الأربعة work وtimer وwait وio. شروط الإطلاق تختلف؛ وهي موحَّدة على مجموعة العمّال نفسها وأسلوب ردّ النداء نفسه.
- الآداب هي «أنشئ → قدِّم → انتظر الاكتمال → أغلق». التقديمات المتعدّدة تعمل على التوازي. يمكن لمجموعة تنظيف تجميع معالجة الإيقاف.
- القواعد الحديديّة الثلاث لردّ النداء: لا تحجب لوقت طويل (إن فعلت،
CallbackMayRunLong)؛ لا تنتظر بشكل متزامن على المجمع نفسه؛ لا توسِّخ حالة مؤشّر الترابط. - الاستخدام من DLL: احذر سباق إلغاء التحميل. انتظر الاكتمال في دالّة إيقاف صريحة؛ لا تفعله في
DllMain. - حيث تكفي C++ القياسيّة أو .NET، استخدمها. دور هذه الواجهة عندما تحتاج تكامل timer/wait/io أو تحكّم بالمجمع.
واجهة مجمع مؤشّرات الترابط، من بين واجهات Win32، في الجانب الأحدث والأفضل تصميماً. متى أنهيت الانتقال من فكرة «إنشاء مؤشّر ترابط» إلى فكرة «رمي ردّ نداء»، يصبح التزامن في الشيفرة الأصليّة أوضح كتابةً بكثير.
مقالات ذات صلة
- أعماق الإدخال/الإخراج في Windows(الجزء 3)── منافذ اكتمال الإدخال/الإخراج(IOCP)ومجمع مؤشّرات الترابط في .NET: الطبقة التحتيّة تحت async/await
- أفضل ممارسات تعدّد مؤشّرات الترابط عمليّاً: طبعة C++ ── إزالة الحوادث بالبنية عبر RAII وjthread
- أفضل ممارسات تعدّد مؤشّرات الترابط عمليّاً: طبعة C ── الكتابة بأمان على طريقة Win32 API
- الإيقاظ الزائف ── لماذا تستيقظ متغيِّرات الشرط «دون إشعار» وكيف تنتظر بشكل صحيح على Windows
- DllMain وقفل المحمِّل ── السبب الحقيقيّ الذي يُقال لك من أجله «لا تفعل شيئاً في تهيئة DLL»
- لماذا يجب أن يفضّل كود Windows انتظار الأحداث على الـ polling بـ timer
مجالات الاستشارة ذات الصلة
تتعامل شركة كومورا سوفت ذ.م.م. مع تصميم الترحيل من شيفرة أصليّة تكاثرت مؤشّرات ترابطها إلى مجمع مؤشّرات الترابط، ومراجعات تصميم المعالجة المتزامنة في تطبيقات C++ وDLLs، والتحقيق في أسباب التجمّد والانهيار الناتجة عن تجويع المجمع أو ردود النداء. مرحَّب باستشارتنا بدءاً من جرد الشيفرة القائمة.
- تطوير تطبيقات Windows
- الاستشارة التقنيّة ومراجعة التصميم
- التحقيق في الأخطاء وتحليل الأسباب الجذريّة
- التواصل معنا
روابط مرجعيّة
-
Microsoft Learn, Thread Pools. حول كون مجمع مؤشّرات الترابط مجموعة مؤشّرات ترابط عامل تنفِّذ بكفاءة ردود نداء غير متزامنة نيابةً عن تطبيق؛ وحول أنواع التطبيقات التي تناسبه (إصدار عدد كبير من عناصر العمل الصغيرة على التوازي، والإنشاء والتمزيق المتكرِّر لمؤشّرات ترابط قصيرة العمر، ومعالجة عمل مستقلّ على التوازي، والانتظارات الحصريّة على كائنات النواة، وغيرها)؛ وحول إعادة تصميم Vista الشاملة (توحيد أنواع مؤشّر ترابط العامل، وطابور مؤقِّت واحد، ومؤشّرات ترابط دائمة مخصَّصة، ومجموعات تنظيف، وعدّة مجامع في عمليّة واحدة، والواجهة الجديدة). ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Thread Pooling. حول بنية واجهات مجمع مؤشّرات الترابط الأقدم (QueueUserWorkItem، وطوابير المؤقِّتات، والانتظارات المسجَّلة، وBindIoCompletionCallback)؛ وحول عدم وجود طريقة لإلغاء عمل بعد وضعه في الطابور؛ وحول النصّ على أنّ واجهة مجمع مؤشّرات الترابط الجديدة المقدَّمة في Vista أبسط وأفضل في الموثوقيّة والأداء والمرونة. ↩ ↩2
-
Microsoft Learn, threadpoolapiset.h header. حول قائمة الدوال التي تشمل دوال إنشاء الكائنات الأربعة CreateThreadpoolWork وCreateThreadpoolTimer وCreateThreadpoolWait وCreateThreadpoolIo؛ ومجموعات التنظيف (CreateThreadpoolCleanupGroup)؛ والتنظيف المرتبط باكتمال ردّ النداء (LeaveCriticalSectionWhenCallbackReturns وFreeLibraryWhenCallbackReturns وغيرها). ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft Learn, Using the Thread Pool Functions. حول الإجراء الأساسيّ للإنشاء بـCreateThreadpoolWork، والتقديم بـSubmitThreadpoolWork، وانتظار الاكتمال بـWaitForThreadpoolWorkCallbacks، والإغلاق بـCloseThreadpoolWork؛ وحول مثال إعداد يجمع مجمعاً مخصَّصاً مع بيئة ردّ نداء ومجموعة تنظيف. ↩ ↩2 ↩3
-
Microsoft Learn, CallbackMayRunLong function (threadpoolapiset.h). حول إشعار المجمع بأنّ ردّ النداء الحاليّ قد يعمل وقتاً طويلاً، فيستخدم المجمع ذلك مادّة لتقرير ما إذا كان سيؤمِّن مؤشّر ترابط لردود النداء الأخرى؛ وحول النظر في مؤشّر ترابط مخصَّص لردّ نداء طويل الأمد حيث أمكن. ↩ ↩2
-
Microsoft Learn, Thread Pooling. حول وجوب كون عناصر العمل المقدَّمة إلى مجمع مؤشّرات الترابط، والدوال التي تستدعيها، آمنة للمجمع؛ وحول عدم افتراض أنّ مؤشّر الترابط المنفِّذ مؤشّر ترابط مخصَّص دائم؛ وحول تجنّب استخدام TLS والاستدعاءات غير المتزامنة التي تتطلّب مؤشّر ترابط دائماً. ↩ ↩2
-
Microsoft Learn, SubmitThreadpoolWork function (threadpoolapiset.h). حول إمكان تقديم كائن work نفسه أكثر من مرّة دون انتظار اكتمال ردّ نداء سابق، فتعمل ردود النداء على التوازي؛ وحول إمكان ضبط المجمع (خنق) عدد مؤشّرات الترابط للكفاءة. ↩
-
Microsoft Learn, SetThreadpoolThreadMaximum function (threadpoolapiset.h). حول إمكان ضبط حدّ أعلى لعدد مؤشّرات ترابط العامل لمجمع أُنشئ بـCreateThreadpool (الحدّ الأدنى هو SetThreadpoolThreadMinimum). ↩
-
Microsoft Learn, CreateThreadpoolWork function (threadpoolapiset.h). حول إنشاء كائن work من دالّة ردّ نداء ومؤشِّر سياق؛ وحول إمكان تحديد الوسيط الثالث، TP_CALLBACK_ENVIRON، لبيئة تنفيذ ردّ النداء (المجمع الذي ينتمي إليه، وغيرها)، مع معنى NULL أنّه يعمل في البيئة الافتراضيّة. ↩
-
Microsoft Learn, <future>. حول توفير التنفيذ غير المتزامن لكلّ مهمّة عبر std::async وfuture كمكتبة قياسيّة، فيمكنك كتابة التزامن دون إدارة مؤشّرات الترابط مباشرةً. ↩
مقالات ذات صلة
أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.
الأنابيب المسمّاة عمليّاً ── IPC القياسيّ في Windows من التصميم إلى الأمان
دليل عمليّ للأنابيب المسمّاة، وسيلة الاتّصال القياسيّة بين العمليّات في Windows. ينظّم المقال، من المصادر الأوّليّة، الاختيار بين وضع الب...
منع التشغيل المتعدّد لتطبيقات Windows ── Mutex المسمّى وتفعيل النافذة عند التشغيل المزدوج
نرتّب هنا طريقة تنفيذ منع التشغيل المتعدّد لتطبيقات Windows للأعمال باستخدام Mutex مسمّى. نشرح الفرق بين Global\ وLocal\ ومطبّاته في بيئا...
التعهيد الخارجي والتطوير التعاقدي لتطبيقات Windows: ما ينبغي تنظيمه قبل الطلب
قبل تكليف جهة خارجية بتطوير تطبيق Windows تعاقديّاً، إليك النقاط التي ينبغي تنظيمها: تعديل البرمجيّات القائمة، وتكامل الأجهزة، وCOM/Activ...
حبّ المطوّر الغريب، أو كيف تعلّمت التوقف عن القلق وأحببتُ Windows
Windows نظام مرهق. لكنّ هذا الإرهاق هو إرهاق نظام تشغيل حمل على عاتقه أعمالاً واقعية طوال عقود.
لماذا تظهر رسالة «قامت Windows بحماية جهاز الكمبيوتر الخاص بك» في Windows
نرتّب من منظور عمليّ أسباب ظهور تحذير SmartScreen عند توزيع تطبيقات Windows، بدءاً من توقيع الشيفرة، وشهادات EV/OV، وAzure Artifact Signi...
أين يتصل هذا الموضوع
ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.
تطوير تطبيقات ويندوز
ندعم تطوير برامج ويندوز للأعمال، وتكامل الأجهزة، وأدوات التواصل.
الأسئلة الشائعة
أسئلة شائعة حول موضوع هذه المقالة.
- ما الذي يميِّز مجمع مؤشّرات الترابط عن إنشاء مؤشّرات ترابط خاصّة بك عبر 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 مشروحة في مقال ذي صلة، وما دمت تكتب أصليّاً، فمعرفة هذه الآليّة التي تقع في الطبقة الأدنى ليست هدراً.
الملف الشخصي للمؤلف
صفحة الملف الشخصي لمؤلف المقالة.
غو كومورا
مؤسّس شركة كومورا سوفت ذ.م.م.
يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.