أفضل ممارسات تعدّد مؤشّرات الترابط عمليّاً: طبعة C ── الكتابة بأمان على طريقة Win32 API
· غو كومورا · Windows, تعدّد مؤشّرات الترابط, C, Win32 API, تطبيقات الأعمال, التحقيق في الأخطاء, تصميم
«أكتب عمليّة مقيمة للتحكّم بالمعدّات بلغة C.» «انتهينا إلى إضافة مؤشّرات ترابط إلى تطبيق C عمره عشرون عاماً.» «نوقف المؤشّرات بـTerminateThread، لكن العمليّة كلّها تتجمّد أحياناً.» ── تعدّد مؤشّرات الترابط في C هو العالم الذي يقدِّم فيه اللغة أقلّ عون. لا استثناءات ولا RAII ولا قوالب؛ صحّة التزامن تقوم كلّها على أيّ واجهات تختار ومدى انضباطك في استدعائها.
هذا المقال طبعة C من سلسلة تعدّد مؤشّرات الترابط العمليّة. موجَّه إلى المطوِّرين الذين يكتبون C مقابل Win32 API، يأخذ مبادئ تصميم تعدّد المؤشّرات ── التوقّف عن إنتاج المؤشّرات بالجملة، تقليل الحالة المشتركة القابلة للتغيير، حفظ انضباط الأقفال، وتصميم طريقة الإيقاف قبل تصميم طريقة البدء ── ويُسقِطها على أدوات Win32: كيف تُنشئ المؤشّرات (_beginthreadex)، كيف تختار كائنات التزامن، كيف تصمِّم مسار إيقاف بلا TerminateThread، وقيود DllMain، منظَّماً حول مصادر أوّليّة حتّى أغسطس 2026. مكتوب ليُقرأ وحده. المبادئ نفسها، مفصَّلة للغات أخرى، موجودة أيضاً في طبعة .NET وطبعة C++ وطبعة Java.
1. الخلاصة أوّلاً
- أنشئ المؤشّرات بـ
_beginthreadexلا بـCreateThread. إذا أُنشئ مؤشّر يستدعي CRT بـCreateThread، يمكن لـCRT إنهاء العمليّة عند انخفاض الذاكرة.12 - القفل الافتراضيّ داخل العمليّة قفل SRW؛ استخدم CRITICAL_SECTION فقط عندما يلزم الاكتساب التكراريّ. استخدام Mutex للاستبعاد داخل العمليّة «خطأ شائع» يتضمّن دائماً انتقالاً إلى وضع النواة.3
- حدِّث المتغيّرات المفردة بعائلة Interlocked. لا يضمن
volatileذرّيّةً ولا ترتيباً. تحمل معظم دوال Interlocked حاجزاً ذاكريّاً كاملاً.4 - انتظر بمتغيّر شرط (عائلة
SleepConditionVariableCS) أو بحدث مع دالّة انتظار. حلقة اقتراع بـSleepتهدر المعالج والاستجابة معاً.5 - لا تستخدم
TerminateThreadأبداً. دالّة خطرة تُفسد الأقفال والكومة وحالة DLL، وهي هدف تحذير تحليل الشيفرة C6258. صمِّم الإيقاف إيقافاً تعاونيّاً: حدث إيقاف معWaitForMultipleObjects.67 - سَلِّم الأعمال القصيرة إلى مجمع مؤشّرات ترابط Windows (
CreateThreadpoolWork) بدل موازاتها بمؤشّراتك. لا تُنهِ مؤشّرات المجمع بـExitThread/TerminateThread.89 - لا تُنشئ مؤشّرات ولا تزامن ولا تنتظر انتهاء مؤشّرات داخل DllMain. تُستدعى وقفل المحمِّل ممسوك، فتكون مرتعاً للجمود.10
<threads.h>في C11 قابل للاستخدام من VS 2022 17.8 فصاعداً، لكن<stdatomic.h>ما زال تجريبيّاً. لقاعدة شيفرة مخصَّصة لـWindows، نهج Win32 هو الاختيار الواقعيّ.11
2. لماذا تعدّد مؤشّرات الترابط صعب؟ ── حالات السباق والجمود
اختصر المشكلات التي يُدخِلها تعدّد المؤشّرات، أيّاً كانت اللغة، فتبقى نوعين.
حالة سباق (race condition) عيب يتغيّر فيه الناتج بحسب الترتيب الذي تصل به عدّة مؤشّرات إلى قطعة شيفرة. المثال الكلاسيكيّ عدّاد مشترك: تنقسم العبارة الواحدة count++ على مستوى لغة الآلة إلى ثلاث خطوات ── قراءة، جمع، كتابة رجوعاً. إذا دخل مؤشّران هذه الخطوات الثلاث في الوقت نفسه، طُمِست إضافة أحدهما وفُقِدت عندما كتب الآخر رجوعاً.4 يتغيّر الناتج من تشغيل إلى تشغيل، وأيّ ناتج ستحصل عليه غير قابل للتنبّؤ.
sequenceDiagram
participant A as المؤشّر A
participant M as المتغيّر المشترك count
participant B as المؤشّر B
Note over M: count = 10
A->>M: قراءة (10)
B->>M: قراءة (10)
A->>A: جمع محلّيّ (11)
B->>B: جمع محلّيّ (11)
A->>M: كتابة رجوعاً (11)
B->>M: كتابة رجوعاً (11)
Note over M: count = 11 رغم زيادتين<br/>فُقِدت زيادة المؤشّر A
الشكل 1: حالة السباق الكلاسيكيّة التي يفقد فيها عدّاد مشترك زيادة. إذا تداخل مؤشّر آخر داخل خطوات count++ الثلاث، فإنّ الكتابة الرجعيّة الأحدث تطمس الأخرى
الجمود (deadlock) حالة ينتظر فيها مؤشّران كلٌّ منهما قفلاً يمسكه الآخر، فلا يستطيع أيٌّ منهما المضيّ. المؤشّر A يمسك القفل 1 وينتظر القفل 2؛ والمؤشّر B يمسك القفل 2 وينتظر القفل 1 ── وهذا وحده يكفي ليقفا إلى الأبد.
flowchart LR
A["المؤشّر A<br/>يمسك القفل 1"] -->|"ينتظر تحرير القفل 2"| B["المؤشّر B<br/>يمسك القفل 2"]
B -->|"ينتظر تحرير القفل 1"| A
الشكل 2: الانتظار الدائريّ للجمود. لحظة تشكِّل أسهم الانتظار حلقة، يتوقّف كلّ مؤشّر في تلك الحلقة إلى الأبد
كلاهما يعتمد على التوقيت: تركيبة ترتيب تنفيذ تظهر مرّة في عشرات آلاف التشغيلات على آلة التطوير قد تحدث كلّ يوم على آلة الزبون بعدد أنوية وتوقيت مختلفين. «لا يُعاد إنتاجه والمصحِّح موصول» يحدث لأنّ المراقبة نفسها تغيِّر التوقيت ── سلوك نموذجيّ لعيوب السباق. لذا تشير كلّ مبادئ هذا المقال في اتّجاه واحد: قلِّل المواضع التي تحتاج تزامناً، قبل أن تقلق بشأن التزامن الصحيح.
2.1. افتراضات خاصّة بـC ── اللغة لا تحميك من شيء
في C، لأنّ اللغة لا تملك آليّة تفرض هذه المبادئ، يلزم كتابتها صراحةً انضباطاً.
أوّلاً، ابنِ ضمان التحرير في البنية. بلا مكافئ لـRAII في C++، يجب حماية تحرير الأقفال واستدعاء CloseHandle على المقابض بنمط goto cleanup يمرِّر كلّ خروج من الدالّة من موضع واحد، أو باتّفاق ترميز يزاوج كلّ اكتساب بتحرير. إضافة return مبكِّر وتسريب قفل نتيجة لذلك حادث C كلاسيكيّ.
ثانياً، عامل سباقات البيانات كما في C++. قراءة أو كتابة بسيطة لمتغيّر 32 بت محاذًى كما ينبغي ذرّيّة على Windows، لكن لا شيء أبعد من ذلك ── متغيّرات 64 بت على Windows 32 بت، والعمليّات المركَّبة، أو الاتّساق عبر عدّة متغيّرات ── مضمون أصلاً.12 الشيفرة التي «تعمل مصادفة» تنكسر لحظة تغيّر المترجِم أو مستوى التحسين.
ثالثاً، قرِّر الملكيّة. ثقافة التصريح، في تعليق دالّة، «أيّ مؤشّر يكتب هذا الوسيط، ومن متى يصير ملك من» تؤتي أُكُلها في تعدّد مؤشّرات C بقدر اختيار بدائيّة التزامن.
3. كيف تُنشئ المؤشّرات ── _beginthreadex ولا شيء سواه
3.1. لماذا CreateThread الاختيار الخطأ
الواجهة الأصليّة في Win32 هي CreateThread، لكن الإرشاد الرسميّ أنّ أيّ مؤشّر يستدعي دوال CRT (وقت تشغيل C) يجب إنشاؤه بـ_beginthreadex. تُهيِّئ _beginthreadex البيانات الداخليّة لكلّ مؤشّر التي تحتاجها CRT قبل بدء المؤشّر. إذا استدعى مؤشّر أُنشئ بـCreateThread دالة CRT، يمكن لـCRT إنهاء العمليّة في ظروف الذاكرة المنخفضة.12 ولأنّ printf وmalloc وstrtok كلّها دوال CRT، فالقاعدة العمليّة: «المؤشّرات المكتوبة بـC تستخدم دائماً _beginthreadex».
تجنَّب أيضاً _beginthread (دون ex). فيها فخّ: إذا انتهى المؤشّر الذي أنشأته مبكّراً، قد يكون المقبض المُرجَع غير صالح أصلاً ── أو حتّى يشير إلى مؤشّر آخر ── بينما _beginthreadex، التي يمكن تمرير مقبضها بأمان إلى واجهات التزامن، الاختيار الأكثر أماناً. يُغلِق المستدعي المقبض الذي تُرجِعه _beginthreadex بـCloseHandle.13
#include <process.h>
static unsigned __stdcall WorkerMain(void* arg)
{
WorkerContext* ctx = (WorkerContext*)arg;
/* ... حلقة الانتظار من القسمين 5 و6 ... */
return 0;
}
HANDLE hThread = (HANDLE)_beginthreadex(
NULL, 0, WorkerMain, &ctx, 0, NULL);
if (hThread == NULL) { /* معالجة الفشل */ }
/* ...بعد طلب الإيقاف... */
WaitForSingleObject(hThread, INFINITE); /* التحاق */
CloseHandle(hThread);
3.2. الأعمال القصيرة تذهب إلى مجمع مؤشّرات ترابط Windows
إذا أردت «قذف عدد كبير من الأعمال الصغيرة إلى شيء» أو وجدت نفسك «تُنشئ وتُتلِف مؤشّرات قصيرة العمر مراراً»، استخدم مجمع مؤشّرات ترابط Windows (واجهة المجمع المتاحة من Vista فصاعداً) بدل مؤشّراتك. أنشئ كائن عمل بـCreateThreadpoolWork وقدِّمه بـSubmitThreadpoolWork، فتنفِّذ مؤشّرات عامل المجمع ردّ النداء على التوازي.8 تُترَك إدارة عدد المؤشّرات لنظام التشغيل، وتختفي تكلفة الإنشاء والإتلاف. هذه إجابة C على مبدأ «لا تُنشئ مؤشّراتك».
انضباط استخدام المجمع موثَّق رسميّاً أيضاً: لا تُنهِ مؤشّر مجمع بـTerminateThread / ExitThread أبداً؛ أعد أيّ حالة غيَّرتها داخل ردّ نداء (TLS وأولويّة المؤشّر وما شابه) قبل العودة؛ وأبقِ مقابض الانتظار حيّة حتّى ينتهي المجمع منها.9 ملاحظة عمليّة أخرى: يحدّ المجمع عدد مؤشّرات العامل فقط ── يمكن لعدد ردود النداء غير المنفَّذة المصطفّة عبر SubmitThreadpoolWork أن يتراكم بلا حدّ. في تكوين طويل الأمد تسبق فيه التقديمات المعالجة باستمرار، ضع مقيِّد دخول مثل إشارة، أو طابوراً محدود السعة، في جانب التطبيق، بحيث ينتظر جانب التقديم أو يُرفَض إذا امتلأ (هذا ضغط عكسيّ لئلّا يتحوّل الحمل الزائد إلى مشكلة ذاكرة، وهو المبدأ نفسه لتصميم الطابور في القسم 5).
4. قلِّل الحالة المشتركة القابلة للتغيير ── قسِّم، اجعل للقراءة فقط، وسلِّم
لا يحدث سباق إلّا عندما يجتمع «عدّة مؤشّرات» و«بيانات مشتركة قابلة للتغيير». قبل اختيار بدائيّة تزامن (الفصل التالي)، فكِّر هل يمكن تقليل المشاركة أصلاً. ثمّة ثلاث عائلات تقنيّة.
قسِّم. للتجميع المتوازي، بدل أن يكتب كلّ مؤشّر إلى عدّاد مشترك، ابنِ مجموعاً فرعيّاً في متغيّر محلّيّ لكلّ مؤشّر (أو وسيط مخصَّص لكلّ مؤشّر)، وادمجها مرّة واحدة في النهاية بشيء مثل InterlockedAdd. تهبط الكتابات إلى الحالة المشتركة من «كلّ تكرار» إلى «مرّة لكلّ مؤشّر»، ويتقلّص كلٌّ من تكلفة التزامن ونافذة التنازع بمراتب. يصير انضباط الملكيّة من القسم 2.1 ── «هذا الوسيط ملك أيّ مؤشّر» ── مخطَّط كيفيّة التقسيم.
اجعل للقراءة فقط. الإعدادات والجداول المبنيّة عند البدء وغير المعدَّلة بعده آمنة للقراءة من أيّ عدد من المؤشّرات بعد اكتمال التهيئة. إمّا أن تُنهي كلّ التهيئة قبل بدء أيّ مؤشّر، أو، إن لزم تهيئة كسولة، استخدم تهيئة المرّة الواحدة في Win32 (InitOnceExecuteOnce)، واجعل الحدّ ── «من متى تصير للقراءة فقط» ── صريحاً في الشيفرة.3
سلِّم. بدل أن يلمس الجانبان متغيّراً مشتركاً، مرِّر تدفّق البيانات بين المؤشّرات عبر طابور منتج/مستهلك. في C، التنفيذ هو نمط متغيّر الشرط من القسم 5 بعينه (وسيط دائريّ محدود مع SleepConditionVariableCS)، وهو المثال الرسميّ المشغول؛ ويعطي وسيط بحدّ سعة أيضاً ضغطاً عكسيّاً طبيعيّاً ── ينتظر الإنتاج إذا سبق الاستهلاك.5
5. اختيار كائنات التزامن وانضباط الأقفال
لـWin32 أنواع كثيرة من بدائيّات التزامن، واختيار الخاطئ يكلِّفك أداءً وصحّة. إليك الإرشاد الرسميّ ملخَّصاً في صورة واحدة.3
flowchart TB
S{"أتلزم مزامنة<br/>عبر العمليّات؟"} -->|"نعم"| Q2{"لماذا؟"}
Q2 -->|"استبعاد متبادل"| MTX["Mutex مسمًّى"]
Q2 -->|"تقييد عدد الوصول المتزامن"| SEM["إشارة مسمّاة"]
Q2 -->|"إشعار حدث"| EVT["حدث مسمًّى"]
S -->|"لا - داخل العمليّة"| Q3{"أتلزم اكتساباً تكراريّاً<br/>من المؤشّر نفسه؟"}
Q3 -->|"نعم"| CS["CRITICAL_SECTION"]
Q3 -->|"لا"| Q4{"أشيفرة C++ قابلة للنقل<br/>أولويّة؟"}
Q4 -->|"نعم"| STD["std::mutex /<br/>std::shared_mutex"]
Q4 -->|"لا"| SRW["قفل SRW - الاختيار الافتراضيّ"]
الشكل 3: كيف تختار بدائيّة تزامن Win32. الفرع الأوّل «هل تعبر العمليّات» ── والنقطة أن لا تمدّ يدك إلى كائن نواة (Mutex) عندما لا تعبر
| البدائيّة | النطاق | الخصائص | أين تُستخدم |
|---|---|---|---|
| قفل SRW | داخل العمليّة | سريع (يبقى عادةً كلّه في وضع المستخدم)، بحجم مؤشّر، غير تكراريّ | الافتراضيّ للشيفرة الجديدة. يسمح AcquireSRWLockShared أيضاً بوصول قراءة مشترك |
| CRITICAL_SECTION | داخل العمليّة | سريع (يدور ثمّ يسقط إلى انتظار نواة)، تكراريّ | عندما يحتاج المؤشّر نفسه اكتساباً تكراريّاً |
| Mutex | داخل العمليّة / عبر العمليّات | دائماً كائن نواة، لذا أبطأ | استبعاد عبر العمليّات (مسمًّى)، أو مع WaitForMultipleObjects |
| إشارة | داخل العمليّة / عبر العمليّات | كائن نواة | تقييد الوصول المتزامن إلى مجمع موارد |
| حدث | داخل العمليّة / عبر العمليّات | كائن نواة | الإشعار بأنّ «شيئاً حدث» (لا لحماية البيانات) |
| دوال Interlocked | داخل العمليّة (وعبر العمليّات أيضاً، عبر ذاكرة مشتركة) | عمليّات ذرّيّة بلا قفل | عدّادات وأعلام ومبادلة مؤشّرات4 |
حاشية للجدول والمخطَّط. تعمل كائنات النواة مثل الأحداث والإشارات والمُتِكسات جيّداً تماماً لتزامن داخل العمليّة إذا أُنشئت بلا اسم (حدث الإيقاف في القسم 6 حدث بلا اسم بعينه). كائن النواة لا يعني عبر العمليّات فقط. ولا يعني «بلا اسم» بالمعنى الضيق «محصور في عمليّة واحدة» ── إذا ورَّثت المقبض لعمليّة ابنة، أو نسختَه إلى عمليّة أخرى بـDuplicateHandle، يمكن استخدام كائن النواة نفسه من عدّة عمليّات حتّى بلا اسم. القول الدقيق أنّ التسمية وسيلة تمثيليّة لتمكين العمليّات من إعادة فتح الكائن نفسه. يلتقط فرع الشكل 3 نقطة الاختيار كـ«لا تختر كائن نواة لقفل داخل العمليّة» ── لإشعار داخل العمليّة (أحداث) أو تقييد التزامن (إشارات)، يبقى كائن نواة بلا اسم الإجابة الصحيحة.
عائلة Interlocked تقابل صنف Interlocked في طبعة .NET وstd::atomic في طبعة C++. تؤدّي InterlockedIncrement / InterlockedExchange / InterlockedCompareExchange عمليّات على متغيّر واحد بشكل غير قابل للتجزئة، ولأنّ معظم هذه الدوال تحمل حاجزاً ذاكريّاً كاملاً، تحصل أيضاً على ضمان ترتيب.4 «لا بأس لأنّي علَّمتُه volatile» سوء فهم ── لا يضمن volatile ذرّيّةً ولا ترتيباً (انظر الأسئلة الشائعة). ثمّة شرط مسبق آخر: المحاذاة. يجب أن يكون المتغيّر المستهدَف بدالّة Interlocked محاذًى على حدّ طبيعيّ (حدّ 4 بايت لقيمة 32 بت، وحدّ 8 بايت لقيمة 64 بت)؛ وإلّا فالسلوك غير قابل للتنبّؤ.12 لا تستهدف أبداً حقلاً داخل بنية #pragma pack، أو حقلاً في وسيط مرسوم مباشرة على تنسيق سلك، بدالّة Interlocked. احصر العدّادات والأعلام في متغيّرات معلَنة عاديّاً ── تلك التي يحاذيها المترجِم لك. وثمّة تنبيه خاصّ حول مبادلة المؤشّرات بشيء مثل InterlockedExchangePointer: غير القابل للتجزئة هو المبادلة نفسها فقط، ولا أحد يضمن عمر الكتلة القديمة بعد المبادلة. إذا حمَّل قارئ المؤشّر القديم لحظة يبدِّله كاتب ويستدعي free، فذلك وصول إلى ذاكرة محرَّرة. تصميم يحدِّث بيانات مشتركة بمبادلة مؤشّرات لا يعمل إلّا مقروناً ببروتوكول استرداد ── قفل أو عدّ مراجع أو ما شابه (إن ساورك شكّ، حمايته بقفل SRW هو الافتراضيّ الآمن).
لـانتظار شيء، ثمّة متغيّرات الشرط. أنشئ واحداً بـInitializeConditionVariable؛ ينام المستهلك على SleepConditionVariableCS (مقترناً بـCRITICAL_SECTION)، ويوقظه المنتج بـWakeConditionVariable ── هذا شكل المثال الرسميّ لطابور منتج/مستهلك على وسيط محدود بعينه.5 الانضباط المهمّ: عند الاستيقاظ، أعد دائماً فحص الشرط (هل الطابور غير فارغ) داخل القفل، وعُد إلى الانتظار إذا كان زائفاً. قد تعاني متغيّرات الشرط استيقاظات زائفة بلا إشعار أصلاً، وبحلول استيقاظك قد يكون مستهلك آخر أخذ العنصر أوّلاً ── لذا «أُوقِظت» لا تعني بالضرورة «الشرط قائم». هذه أداة C لبناء الشكل نفسه لقناة طبعة .NET القسم 4.3 وBlockingQueue في طبعة C++ القسم 4. عند الاقتران بقفل SRW، استخدم SleepConditionVariableSRW.
5.1. انضباط الأقفال ── ثلاثة مبادئ تصمد أيّاً كانت البدائيّة
اختيار البدائيّة الصحيحة لا يكفي وحده ── بلا استخدام منضبط، لن تمنع السباقات مع ذلك.
- قرِّر، واحداً لواحد، أيّ قفل يحمي أيّ بيانات. عيِّن قفلاً واحداً بالضبط (قفل SRW أو CRITICAL_SECTION) لكلّ مجموعة بيانات قابلة للتغيير تريد حمايتها، وخُذ ذلك القفل نفسه في كلّ موضع يلمس تلك البيانات. في C خصوصاً، يؤتي ثمره التصريح بذلك في تعليق ترويسة ── «هذه البنية يحميها
g_lockFoo». - لا تفعل شيئاً بطيئاً أو خارجيّاً وأنت ممسك قفلاً. الشيء الوحيد السائغ وأنت ممسك قفلاً قراءة البيانات التي يحميها أو كتابتها. إدخال/إخراج ملفّ، أو استدعاءات شبكة، أو استدعاءات ردّ نداء وأنت ما زلت ممسكاً القفل تطيل مدّة الإمساك وتخاطر بأن يحاول المستدعى أخذ قفل آخر، صانعة الانتظار الدائريّ من الشكل 2.
- ثبِّت ترتيب الاكتساب لأقفال متعدّدة. حيثما يُؤخذ قفلان أو أكثر، اجعل قاعدة أن يأخذها كلّ مؤشّر بالترتيب نفسه (هرميّة أقفال). تنصّ وثائق أفضل ممارسات DLL صراحةً على أنّ عكس ذلك الترتيب (قلب ترتيب الأقفال) يُنتج جموداً صعب التصحيح، وأنّ عليك تعريف هرميّة واتّباعها باتّساق.10
6. تصميم طريقة الإيقاف ── لا TerminateThread أبداً
6.1. ما الذي تُفسده TerminateThread
تمحو TerminateThread المؤشّر المستهدَف دون أن تتركه ينفِّذ أيّ شيفرة في وضع المستخدم. العواقب التي تعدِّدها الوثائق الرسميّة شديدة. إذا كان المؤشّر المستهدَف يمسك قسماً حرجاً، فلن يُحرَّر أبداً؛ وإذا كان في وسط عمليّة كومة، بقي قفل الكومة ممسوكاً (ويتعلّق كلّ مؤشّر لاحق يستدعي malloc)؛ وإذا كان يعبث بحالة عامّة لـDLL، تُترَك تلك الحالة فاسدة. الموقف الرسميّ أنّها «دالّة خطرة ينبغي استخدامها في الحالات الأشدّ تطرّفاً فقط»، ويعلِّمها تحليل الشيفرة تحذيراً C6258.67
العثور على TerminateThread أثناء التحقيق في تطبيق «يتجمّد كلّه أحياناً» مشهد شائع حقّاً في العمل. إن وجدته، عامله شيئاً يحتاج إصلاحاً.
6.2. النمط الصحيح: حدث إيقاف مع WaitForMultipleObjects
النمط المستقرّ للإيقاف التعاونيّ في C إنشاء حدث إيقاف بإعادة ضبط يدويّة واحد، وجعل كلّ مؤشّر عامل ينتظر «إشارة العمل» و«إشارة الإيقاف» في الوقت نفسه. تشير وثائق تحذير C6258 نفسها إلى هذا النمط بعينه ── أنشئ حدثاً، واجعل كلّ مؤشّر يراقبه بـWaitForSingleObject، ودَع المؤشّر يُنهي نفسه ── بوصفه طريقة الإنهاء الصحيحة.7
HANDLE hStopEvent; /* CreateEvent(NULL, TRUE, FALSE, NULL): إعادة ضبط يدويّة */
HANDLE hWorkEvent; /* CreateEvent(NULL, FALSE, FALSE, NULL): إعادة ضبط تلقائيّة.
يعود تلقائيّاً إلى غير مُشار إليه عند الاستلام
(حدث إعادة الضبط اليدويّة سيُمرِّر الانتظارات
بعد إشارة واحدة، فيصير حلقة مشغولة
تدور على طابور فارغ) */
static unsigned __stdcall WorkerMain(void* arg)
{
HANDLE waits[2] = { hStopEvent, hWorkEvent };
for (;;) {
DWORD r = WaitForMultipleObjects(2, waits, FALSE, INFINITE);
if (r == WAIT_FAILED) { /* مثلاً مقبض غير صالح. إن تُرك بلا معالجة دار بأقصى سرعة */
LogLastError(); /* سجِّل GetLastError() واخرج */
break;
}
if (r == WAIT_OBJECT_0) /* طُلِب الإيقاف */
break;
if (r == WAIT_OBJECT_0 + 1) { /* عمل متاح */
/* مرِّر حدث الإيقاف أيضاً إلى ProcessNextItem: إذا انتظر طويلاً
داخليّاً لعنصر واحد، ولم يستطع رصد الإيقاف هناك أيضاً،
صار الإيقاف رهينة ذلك العنصر */
while (ProcessNextItem(hStopEvent)) { /* عالج عنصراً من الطابور؛ FALSE إن فرغ */
/* افحص طلب الإيقاف أثناء التفريغ أيضاً. تخطَّ هذا ولن
تستطيع التوقّف ما دام العمل يتراكم (مجاعة إيقاف) */
if (WaitForSingleObject(hStopEvent, 0) == WAIT_OBJECT_0)
break;
}
}
}
Cleanup(); /* نظِّف بنفسك */
return 0; /* انتهِ بنفسك */
}
BOOL StopWorkers(HANDLE* threads, DWORD count)
{
BOOL ok = TRUE;
if (!SetEvent(hStopEvent)) { /* إذا لم يصل طلب الإيقاف، */
LogLastError(); /* فلا تمضِ إلى التحاق بلا حدّ */
return FALSE;
}
/* صار طلب الإيقاف مرفوعاً لدى الجميع دفعة واحدة */
for (DWORD i = 0; i < count; i++) {
if (WaitForSingleObject(threads[i], INFINITE) == WAIT_OBJECT_0) {
CloseHandle(threads[i]); /* أغلق فقط المقابض التي أكَّدنا التحاقها */
} else {
LogLastError(); /* WAIT_FAILED: مثلاً مقبض غير صالح */
ok = FALSE; /* لا تُبلِغ «توقّف الجميع» */
}
}
return ok; /* إن كان FALSE فلا تمضِ إلى تحرير الموارد المشتركة */
}
ثمّة سبب لالتحاق الموقف مؤشّراً تلو آخر بـWaitForSingleObject. تستطيع WaitForMultipleObjects انتظار MAXIMUM_WAIT_OBJECTS (64) مقبضاً على الأكثر دفعة واحدة؛ سلِّمها مصفوفة أكبر فيفشل الانتظار نفسه بـWAIT_FAILED، فتُغلِق مقابض وأنت تعتقد أنّك انتظرت الجميع بينما لم تنتظر أحداً. إن كان كلّ ما تحتاجه انتظار انتهاء الجميع، فحلقة واحداً تلو آخر بلا حدّ أعلى الاختيار الآمن.
flowchart TB
OWNER["جانب الإيقاف"] -->|"SetEvent(hStopEvent)"| SE["حدث الإيقاف<br/>إعادة ضبط يدويّة - ظاهر للجميع دفعة"]
SE --> W1["العامل 1 - ينتظر الإيقاف والعمل<br/>معاً عبر WaitForMultipleObjects"]
SE --> W2["العامل 2 - ينتظر الإيقاف والعمل<br/>معاً عبر WaitForMultipleObjects"]
W1 --> C1["ينظِّف ويعود بنفسه"]
W2 --> C2["ينظِّف ويعود بنفسه"]
C1 --> J["جانب الإيقاف ينتظر مقابض المؤشّرات ويلتحق<br/>الآن فقط يمكن تسميته متوقّفاً"]
C2 --> J
الشكل 4: نمط حدث الإيقاف. استخدام حدث إعادة ضبط يدويّة لإشارة الإيقاف يعني أنّ SetEvent واحدة توقظ كلّ عامل منتظر دفعة واحدة. يقرِّر كلّ مؤشّر كيف ينتهي، ولا يُعدّ الإيقاف مكتملاً إلّا بعد انتهاء الالتحاق
ثلاث نقاط أساسيّة. اجعل حدث الإيقاف إعادة ضبط يدويّة (فتظهر SetEvent واحدة لكلّ عامل)؛ ضع حدث الإيقاف أوّلاً في مصفوفة الانتظار (فإذا أُشِير كلاهما دفعة، حظي الإيقاف بالأولويّة)؛ ويجب على جانب الإيقاف دائماً الالتحاق بمقابض المؤشّرات قبل إغلاقها.
تنبيهان عن النطاق. أوّلاً، نمط «الحدث مع تفريغ الكلّ» هذا لتكوين عامل واحد. مهما دعوت SetEvent على حدث إعادة ضبط تلقائيّة، لا يستطيع التعبير إلّا عن «ثمّة حالة إشارة واحدة» (تندمج الإشارات المتتالية)، فمع عدّة عمّال يستيقظ واحد فقط وينتهي إلى معالجة الدفعة كلّها تسلسليّاً. إذا شارك عدّة عمّال طابوراً، حوِّل إشارة العمل إلى إشارة (semaphore)، زِد العدّ بـReleaseSemaphore(hSem, 1, NULL) كلّما اصطفّ عنصر. يستهلك انتظار إشارة ناجح عدّاً واحداً، معطيّاً التقابل الصحيح: يستيقظ بالضبط بقدر العمّال المنتظرين، واحداً تلو آخر، بقدر العناصر المصطفّة (هذا الاستخدام في صميم اختصاص الإشارة، مثل «تقييد الوصول المتزامن إلى مجمع موارد» في جدول الشكل 3). لكن عند التحويل إلى إشارة، غيِّر أيضاً جانب المستهلك بحيث انتظار ناجح واحد يساوي معالجة عنصر واحد بالضبط من الطابور. اترك حلقة تفريغ الكلّ من العيّنة أعلاه كما هي، فانتظار واحد ── يستهلك إذناً واحداً فقط ── سيُفرِغ الطابور كلّه، فيختلّ الحساب: يستيقظ عمّال آخرون على طابور فارغ بالأذون المتبقّية، ويبدأ ReleaseSemaphore المنتج بالفشل لتجاوز العدّ الأقصى. حفظ التقابل «إذن واحد يساوي عملاً واحداً» شرط مسبق لنهج الإشارة. ثانياً، مرِّر مسار الإيقاف عبر معالجة عنصر واحد أيضاً. إذا انتظر ProcessNextItem داخليّاً انتظاراً حاجباً طويلاً، فإمّا مرِّر حدث الإيقاف هناك أيضاً وانتظر الاثنين معاً، أو أرفق مهلة محدودة. الفحص بين العناصر فقط يترك ثغرة «ينتظر الإيقاف إلى الأبد لأنّ عنصراً لا ينتهي». يقول هذا تماماً ما تقوله StopAsync في طبعة .NET وjthread مع الالتحاق في طبعة C++.
مؤشّر ينتظر على إدخال/إخراج حاجب (أنبوب أو مقبس أو منفذ تسلسليّ) لا يستطيع العودة لفحص الحدث، لذا يحتاج جانب الإدخال/الإخراج تصميماً خاصّاً ── إمّا إدخال/إخراج OVERLAPPED مع حدث تنتظره معاً، أو إيقاظ الإدخال/الإخراج بـCancelIoEx (لمثال تسلسليّ ملموس، انظر «مزالق تطبيقات serial communication - framing وtimeouts وflow control وreconnects ومحوّلات USB وتجمّد الـ UI»).
7. DllMain وقفل المحمِّل ── حقل ألغام لكتّاب DLL
كثيراً ما تصير المكوِّنات المشتركة المكتوبة بـC مكتبات DLL، وللـDLL قيد خاصّ: قفل المحمِّل. يستدعي محمِّل نظام التشغيل DllMain وهو ممسك قفل المحمِّل، ففعل أيٍّ ممّا يلي داخلها يصير مصدراً لجمود أو انهيار.10
- التزامن مع مؤشّرات أخرى (اكتساب أقفال، انتظار انتهاء مؤشّر)
- استدعاء
LoadLibrary/FreeLibrary، مباشرة أو غير مباشرة - إنشاء مؤشّرات (خطر إن تضمّن تزامناً)، أو استدعاء
ExitThread
«انتظار داخل DllMain لانتهاء مؤشّر عامل عند تفريغ الـDLL» يبدو معقولاً تماماً لكنّه جمود كلاسيكيّ: يحاول المؤشّر الذي ينتهي أخذ قفل المحمِّل لتسليم DLL_THREAD_DETACH، فينتهي الجانبان منتظرَين بعضهما. ينبغي لـDLL ذات مؤشّرات خاصّة أن تعرض دوال تهيئة وإيقاف صريحة ── مثل MyLib_Init / MyLib_Shutdown ── وتقوم ببدء المؤشّرات والالتحاق بها هناك. DllMain المثاليّ قريب من بذرة فارغة.10
8. خيار مؤشّرات C11 ── أين نقف
إذا أردت كتابة C قابل للنقل لا يعتمد على Win32، فالخيار <threads.h> في C11 (thrd_create / mtx_lock / cnd_wait) و<stdatomic.h>. وفق جدول المطابقة الرسميّ، يقف دعم MSVC كالتالي: <threads.h> مدعوم من Visual Studio 2022 17.8 (يتطلّب /std:c11 وWindows SDK مطابقاً)، بينما <stdatomic.h> ما زال تجريبيّاً، في مرحلة تتطلّب خيار /experimental:c11atomics.11
إذا كانت مشاركة الشيفرة مع Linux متطلَّباً، لمؤشّرات C11 (أو غلاف pthread) قيمة حقيقيّة، لكن لقاعدة شيفرة مخصَّصة لـWindows، يتمتّع نهج Win32 الذي يعالجه هذا المقال بالأفضليّة في حجم المعلومات المتاحة والسجلّ وسهولة التصحيح. أيّاً اخترت، لا تتغيّر مبادئ التصميم المشروحة حتّى الآن ── تقليل المشاركة، والتقابل بين الأقفال والبيانات، والإيقاف التعاونيّ.
9. التحقّق والتصحيح ── الاستعداد لـ«لا يُعاد إنتاجه»
لا يمكنك توقّع أن تلتقط الاختبارات عيوب السباق. تعدّ الاختبارات العاديّة تشغيلاً «لم يُطلَق فيه السباق مصادفة» نجاحاً. فكِّر في دفاعاتك ثلاث طبقات.
خطّ الدفاع الأوّل التصميم. في المراجعة، أكِّد بجدول: أيّ بيانات قابلة للتغيير مشتركة، وأيّ قفل يحمي كلّ قطعة (التقابل من القسم 5.1)، وهل ترتيب اكتساب الأقفال لا لبس فيه، وهل يصل حدث الإيقاف إلى كلّ عامل. تصميم لا يستطيع ملء هذا الجدول لم يكتمل بعد، حتّى إن كان يعمل الآن.
ثانياً، اجعل الشذوذ قابلاً للرصد. بدل الانتظار بلا شرط بـINFINITE، أرفق مهلة في المواضع الحاسمة وسجِّل المهلة عندما تنفجر، محوِّلاً تعلّقاً كان سيدوم إلى الأبد إلى فشل قابل للكشف. عندما يحدث تعلّق في الميدان، التقط تفريغاً، افحص مكدّس كلّ مؤشّر، وابحث عن دورة في من ينتظر قفل من. الفحص بـApplication Verifier موصًى به رسميّاً لأخطاء حول DLL.10 بناء التفريغات والسجلات مشمول في «كيف نُبقي سجلّات الأعطال في تطبيقات Windows حتّى عند الموت بسبب أخطاء برمجيّة».
ثالثاً، هزّ بالحمل. اختبارات الإجهاد ── التشغيل بأكثر من عدد الأنوية من المؤشّرات، وعشوائية ترتيب المعالجة، وإدراج تأخيرات مصطنعة ── وسيلة عمليّة لزيادة احتمال إصابة «جائزة» سباق على آلة التطوير. لا تنسَ أيضاً اختبار إعادة الإنتاج على بناء إصدار محسَّن تحت حمل ثقيل.
10. الخلاصة ── قائمة فحص طبعة C
- هل كلّ مؤشّر مُنشأ بـ
_beginthreadex(بلا خلطCreateThread/_beginthread)؟ - هل تلتحق بمقابض المؤشّرات (
WaitForSingleObject) قبل استدعاءCloseHandle؟ - هل تنتج مؤشّراتك بالجملة لأعمال قصيرة العمر (هل يمكن تسليمها لواجهة مجمع المؤشّرات)؟
- هل الاستبعاد داخل العمليّة يستخدم قفل SRW / CRITICAL_SECTION (بدل إساءة استخدام Mutex)؟
- هل تُحدَّث العدّادات والأعلام المشتركة بدوال Interlocked بدل الاعتماد على
volatile؟ - هل بقي اقتراع
Sleep(هل استُبدِل بمتغيّر شرط أو انتظار حدث)؟ - هل
TerminateThread(قتل مؤشّر آخر قسراً) غائب في كلّ موضع؟ هل ينتهي العمّال بـreturnمن دالّة المؤشّر بدل استدعاءExitThread(كي يجري تنظيف CRT صحيحاً عبر_endthreadex)؟ - هل لكلّ عامل مسار إيقاف عبر حدث إيقاف مع
WaitForMultipleObjects، وهل تستطيع أيضاً إيقاظ مؤشّرات محجوبة على الإدخال/الإخراج؟ - هل تحرير الأقفال والمقابض مضمون على كلّ مسار عودة (انضباط
goto cleanup)؟ - هل تتجنّب
DllMainإنشاء المؤشّرات والتزامن وانتظار انتهائها؟
مقابل انعدام عون اللغة، جودة تعدّد مؤشّرات الترابط في C هي بالضبط ما تصنعه اختيارات الواجهة والانضباط. اجعل _beginthreadex وأقفال SRW ودوال Interlocked وحدث الإيقاف مجموعتك الافتراضيّة الرباعيّة، وحتّى في C تستطيع التصميم بعيداً عن «يتجمّد أحياناً».
مقالات ذات صلة
- أفضل ممارسات تعدّد مؤشّرات الترابط عمليّاً: طبعة .NET ── ما ينبغي حسمه قبل أن تضيف مزيداً من مؤشّرات الترابط
- أفضل ممارسات تعدّد مؤشّرات الترابط عمليّاً: طبعة C++ ── إزالة الحوادث بالبنية عبر RAII وjthread
- أفضل ممارسات تعدّد مؤشّرات الترابط عمليّاً: طبعة Java ── اصطلاحات عصر مؤشّرات الترابط الافتراضيّة
- المزالق وأفضل الممارسات عند استخدام shared memory - تنظيم مسبق للتزامن، الرؤية، العمر، ABI، والأمان
- لماذا يجب أن يفضّل كود Windows انتظار الأحداث على الـ polling بـ timer
- مزالق تطبيقات serial communication - framing وtimeouts وflow control وreconnects ومحوّلات USB وتجمّد الـ UI
- كيف نُبقي سجلّات الأعطال في تطبيقات Windows حتّى عند الموت بسبب أخطاء برمجيّة - أفضل الممارسات مع WER وعلامات نهائيّة وتصميم watchdog
مجالات الاستشارة ذات الصلة
تتعامل شركة كومورا سوفت ذ.م.م. مع مراجعة تصميم تعدّد مؤشّرات الترابط للعمليّات المقيمة وتطبيقات التحكّم بالمعدّات ومكتبات DLL المكتوبة بـC؛ والتحقيق في السبب الجذريّ (تحليل التفريغ) للتعلّقات والانهيارات الناجمة عن TerminateThread أو أقفال مسربة؛ والاستشارة التقنيّة حول إضافة مؤشّرات إلى شيفرة C قديمة.
- الاستشارات التقنيّة ومراجعة التصميم
- التحقيق في الأخطاء وتحليل السبب الجذري
- تطوير تطبيقات ويندوز
- التواصل معنا
روابط مرجعيّة
-
Microsoft Learn, CreateThread function. حول حاجة المؤشّرات داخل تنفيذيّ يستدعي CRT إلى إدارتها بـ_beginthreadex / _endthreadex لا بـCreateThread / ExitThread، وحول إمكان أن تُنهي CRT العمليّة في ظروف الذاكرة المنخفضة عندما يستدعي مؤشّر أُنشئ بـCreateThread الـCRT. ↩ ↩2
-
Microsoft Learn, Multithreading with C and Win32. حول حاجة البرامج التي تستدعي مكتبة CRT إلى بدء المؤشّرات بـ_beginthread / _beginthreadex لا بـCreateThread / ExitThread في Win32؛ وحول تهيئة عائلة _beginthread متغيّرات CRT لكلّ مؤشّر؛ وحول إمكان أن يوقف SuspendThread مؤشّراً وهو يصل إلى بنى بيانات CRT الداخليّة، ممّا قد يؤدّي إلى جمود. ↩ ↩2
-
Microsoft Learn, About Synchronization. حول إرشاد اختيار بدائيّات تزامن Win32: أقفال SRW افتراضيّ الشيفرة الجديدة، بحجم مؤشّر وتبقى عادةً في وضع المستخدم؛ CRITICAL_SECTION لحالات الاكتساب التكراريّ؛ Mutex دائماً كائن نواة، يُستخدم للتزامن المسمّى عبر العمليّات ومع WaitForMultipleObjects؛ استخدام Mutex لتزامن داخل العمليّة «خطأ شائع» أبطأ بكثير تحت العمليّات المتكرِّرة؛ والإشارات لتقييد الوصول المتزامن إلى مجمع موارد، والأحداث للإشعار. ↩ ↩2 ↩3
-
Microsoft Learn, Interlocked Variable Access. حول مزامنة دوال Interlocked الوصول إلى متغيّر مشترك عبر عدّة مؤشّرات وأداء العمليّة بشكل غير قابل للتجزئة؛ وحول جمع InterlockedIncrement / Decrement القراءة والجمع والكتابة رجوعاً في عمليّة ذرّيّة واحدة، إذ بلا تزامن يمكن لزيادة متزامنة من مؤشّرين أن تفقد إحدى الزيادتين؛ وحول عائلة InterlockedExchange / InterlockedCompareExchange؛ وحول إمكان الاستخدام بين مؤشّرات في عمليّات مختلفة عندما يكون المتغيّر في ذاكرة مشتركة؛ وحول تقديم معظم دوال Interlocked حاجزاً ذاكريّاً كاملاً، مع تنويعات Acquire / Release لاختيار دلالات الترتيب. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Using Condition Variables. حول المثال المشغول لطابور منتج/مستهلك منفَّذ على وسيط دائريّ محدود محميّ بـCRITICAL_SECTION؛ وحول البنية حيث يُنشئ InitializeConditionVariable متغيّر شرط، وينتظر المستهلك بـSleepConditionVariableCS، ويوقظه المنتج بـWakeConditionVariable؛ وحول دعم متغيّرات الشرط من Windows Vista فصاعداً. ↩ ↩2 ↩3
-
Microsoft Learn, TerminateThread function. حول إنهاء TerminateThread المؤشّر المستهدَف دون تركه ينفِّذ أيّ شيفرة في وضع المستخدم؛ وحول عدم تحرير القسم الحرج للمستهدَف إن كان يمسكه؛ وحول عدم تحرير قفل الكومة إن كان المؤشّر يخصِّص ذاكرة من الكومة؛ وحول إمكان فساد حالة kernel32 أو الحالة العامّة لـDLL؛ وحول أنّها «دالّة خطرة ينبغي استخدامها في الحالات الأشدّ تطرّفاً فقط»، لا تُستدعى إلّا إذا عرفت وتحكّمت تماماً في كلّ مسار شيفرة قد ينفِّذه المؤشّر المستهدَف. ↩ ↩2
-
Microsoft Learn, Warning C6258. حول اكتشاف تحذير تحليل الشيفرة C6258 لاستخدام TerminateThread؛ وحول عجز TerminateThread عن تنظيف المؤشّر كما ينبغي؛ وحول إجراء الإنهاء الصحيح المعروض كإنشاء حدث بـCreateEvent، وجعل كلّ مؤشّر يراقب حالة الحدث بـWaitForSingleObject، وجعل المؤشّر يُنهي تنفيذه بنفسه متى صار الحدث مُشاراً إليه. ↩ ↩2 ↩3
-
Microsoft Learn, CreateThreadpoolWork function. حول إنشاء كائن عمل بـCreateThreadpoolWork وجعل مؤشّر عامل المجمع ينفِّذ ردّ النداء كلّما استُدعيت SubmitThreadpoolWork؛ وحول إمكان تحديد بيئة التنفيذ عبر بيئة ردّ نداء (TP_CALLBACK_ENVIRON)؛ وحول التوفّر من Windows Vista فصاعداً. ↩ ↩2
-
Microsoft Learn, Thread Pools. حول ملاءمة مجمع المؤشّرات للتطبيقات التي تنفِّذ أعداداً كبيرة من الأعمال غير المتزامنة القصيرة، أو التي تُنشئ كثيراً مؤشّرات قصيرة العمر؛ وحول مكوِّنات واجهة المجمع الجديدة المعاد تصميمها في Vista؛ وحول أفضل الممارسات بعدم إنهاء مؤشّر مجمع بـTerminateThread أو استدعاء ExitThread من داخل ردّ نداء، وتنظيف أيّ حالة أُنشئت في ردّ نداء قبل العودة، وإبقاء مقابض الانتظار حيّة حتّى ينتهي المجمع منها. ↩ ↩2
-
Microsoft Learn, Dynamic-Link Library Best Practices. حول استدعاء DllMain وقفل المحمِّل ممسوك، ممّا يضع قيوداً جدّيّة على أيّ واجهات يمكن استدعاؤها بأمان؛ وحول أنّ التزامن مع مؤشّرات أخرى داخل DllMain يؤدّي إلى جمود؛ وحول أنّ استدعاء LoadLibrary في قائمة الأفعال المحظورة؛ وحول النمط الذي يجمد فيه انتظار انتهاء مؤشّر داخل DllMain أثناء تفريغ الـDLL مقابل محاولة ذلك المؤشّر اكتساب قفل المحمِّل لتسليم DLL_THREAD_DETACH؛ وحول أنّ DllMain المثاليّ قريب من بذرة فارغة، مع تأخير التهيئة قدر الإمكان؛ وحول تعريف هرميّة أقفال يكون قفل المحمِّل في أعلاها. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Microsoft C/C++ language conformance by Visual Studio version. حول جدول مطابقة مكتبة C القياسيّة، مظهراً دعم مؤشّرات C11 (threads.h) من Visual Studio 2022 17.8؛ ومعاملة stdatomic.h تجريبيّاً (خلف خيار /experimental:c11atomics)؛ وحاجة دعم مترجِم C11 / C17 إلى Visual Studio 2019 16.8 أو أحدث مع Windows SDK مطابق. ↩ ↩2
-
Microsoft Learn, Interlocked Variable Access. حول ذرّيّة قراءة أو كتابة بسيطة لمتغيّر 32 بت محاذًى كما ينبغي، دون ضمان تزامن (ترتيب) الوصول؛ وحول ذرّيّة قراءة أو كتابة بسيطة لمتغيّر 64 بت على Windows 64 بت دون ضمان على Windows 32 بت؛ وحول عدم ضمان ذرّيّة متغيّرات الأحجام الأخرى على أيّ منصّة. ↩ ↩2
-
Microsoft Learn, _beginthread, _beginthreadex. حول لماذا _beginthreadex أكثر أماناً من _beginthread: يمكن لمؤشّر أُنشئ بـ_beginthread أن يترك المقبض المُرجَع غير صالح (أو يشير إلى مؤشّر آخر) إذا انتهى مبكّراً؛ يجب أن يُغلِق المستدعي مقبض _beginthreadex بـCloseHandle وصحّته مضمونة؛ تسمح _beginthreadex بتمرير المقبض إلى واجهات التزامن؛ تُرجِع دالّة المؤشّر رمز خروج مؤشّر وفق اتّفاق استدعاء __stdcall؛ ويلزم الربط بـCRT متعدّد المؤشّرات. ↩
مقالات ذات صلة
أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.
واجهة مجمع مؤشّرات الترابط Win32 ── تزامن دون إنشاء مؤشّرات ترابط، عبر CreateThreadpoolWork
هل تنثر استدعاءات CreateThread في شيفرتك الأصليّة؟ يشرح هذا المقال واجهة مجمع مؤشّرات الترابط Win32 التي أُعيد تصميمها في Vista ── كائنات...
تطبيقات تتعطّل عند الاستئناف من السكون ── كيف تعمل أحداث الطاقة في Windows وكيف تبني تطبيقات أعمال تصمد أمامها
فتحت الحاسوب المحمول فوجدت اتّصالات تطبيق الأعمال ميّتة ── السبب تصميم لم يحسب حساب السكون. يغطّي المقال تدفّق إشعار WM_POWERBROADCAST، و...
DllMain وقفل المحمِّل ── السبب الحقيقي لعبارة «لا تفعل شيئاً في تهيئة الـDLL»
لماذا يجب ألا تستدعي LoadLibrary أو تتزامن مع مؤشّرات ترابط أخرى من DllMain. يستند المقال إلى المصادر الأوّليّة ليشرح كيف يسلسل قفل المحم...
ما الذي يعنيه «لا يستجيب» حقّاً ── كيف يقرِّر Windows أنّ تطبيقاً قد تجمّد، وكيف تصمِّم تطبيقات لا تتجمّد
«لا يستجيب» في Windows آليّة يحكم فيها نظام التشغيل أنّ نافذة لم تسترجع رسالة لمدّة 5 ثوانٍ ويستبدلها بنافذة شبح. يغطّي المقال دواخل ذلك ...
الإيقاظات الزائفة ── لماذا تستيقظ متغيّرات الشرط «دون إشعار» وكيف تنتظر بشكل صحيح على Windows
يمكن لانتظار متغيّر شرط أن يعود حتّى عندما لا يكون إشعار قد وصل (إيقاظ زائف). يشرح هذا المقال، من تنفيذ Windows، لماذا تسمح المواصفة بذلك...
أين يتصل هذا الموضوع
ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.
تطوير تطبيقات ويندوز
ندعم تطوير برامج ويندوز للأعمال، وتكامل الأجهزة، وأدوات التواصل.
الأسئلة الشائعة
أسئلة شائعة حول موضوع هذه المقالة.
- هل ينبغي أن أستخدم CreateThread أم _beginthreadex؟
- استخدم _beginthreadex لأيّ مؤشّر ترابط يستدعي دوال مكتبة وقت تشغيل C (CRT). تُهيِّئ _beginthreadex البيانات الداخليّة لكلّ مؤشّر ترابط التي تحتاجها CRT قبل بدء المؤشّر. تنصّ الوثائق الرسميّة بوضوح على أنّه إذا استدعى مؤشّر ترابط أُنشئ بـCreateThread دالة CRT، يمكن لـCRT إنهاء العمليّة عند انخفاض الذاكرة. عمليّاً، تكاد مؤشّرات الترابط في تطبيق C تستدعي دائماً دالة CRT في موضع ما (printf وmalloc وstrtok وما شابه)، لذا لا ضرر من تذكّر القاعدة «دائماً _beginthreadex». تجنَّب أيضاً _beginthread (دون ex) ── فيها فخّ أنّ المقبض المُرجَع قد يصبح غير صالح إذا انتهى المؤشّر المُنشأ مبكّراً، لذا _beginthreadex، التي يمكن تمرير مقبضها إلى واجهات التزامن، هي الاختيار.
- ألا يُسمح لي بإيقاف مؤشّر ترابط بـTerminateThread؟
- لا، لا ينبغي. تمحو TerminateThread المؤشّر المستهدَف دون أن تتركه ينفِّذ أيّ شيفرة في وضع المستخدم، فإذا كان ذلك المؤشّر يمسك قسماً حرجاً فلن يُحرَّر أبداً، وإذا كان يخصِّص ذاكرة من الكومة بقي قفل الكومة ممسوكاً، وإذا كان يعبث بحالة عامّة لـDLL فُسِدت تلك الحالة. تنصّ الوثائق الرسميّة صراحةً على أنّها «دالّة خطرة ينبغي استخدامها في الحالات الأشدّ تطرّفاً فقط»، ويعلِّمها تحليل الشيفرة أيضاً تحذيراً C6258. طريقة الإيقاف الصحيحة هي إيقاف تعاونيّ: أنشئ حدث إيقاف، واجعل كلّ مؤشّر يراقبه بـWaitForSingleObject / WaitForMultipleObjects، ودَع كلّ مؤشّر ينظِّف نفسه وينتهي بنفسه.
- كنت أستخدم Mutex للاستبعاد داخل عمليّة. ما الخطأ؟
- يعمل، لكنّه يكلِّفك أداءً كثيراً. Mutex في Win32 دائماً كائن نواة، فكلّ اكتساب وتحرير يُطلِق انتقالاً إلى وضع النواة. للاستبعاد داخل عمليّة واحدة، قفل SRW أو CRITICAL_SECTION ── اللذان يبقيان في وضع المستخدم ولا يسقطان إلى انتظار نواة إلّا عند التنازع ── أسرع بكثير، وتسمّي الوثائق الرسميّة صراحةً استخدام Mutex لتزامن داخل العمليّة «خطأ شائع». يستحقّ Mutex مكانه عندما تحتاج استبعاداً عبر العمليّات ككائن مسمًّى، أو عندما تريد انتظاره مع كائنات نواة أخرى بـWaitForMultipleObjects.
- هل يمكنني استخدام threads.h وstdatomic.h من C11 على Windows؟
- في MSVC، صارت مؤشّرات ترابط C11 (threads.h) مدعومة منذ Visual Studio 2022 17.8 (تتطلّب /std:c11 وWindows SDK مطابقاً). أمّا stdatomic.h فما زال يُعامَل تجريبيّاً ويتطلّب خيار /experimental:c11atomics (وفق جدول المطابقة الرسميّ حتّى أغسطس 2026). خيار قابل للتطبيق إذا كانت قابليّة النقل أولويّتك القصوى، لكن لقاعدة شيفرة مخصَّصة لـWindows، الكتابة على Win32 API (_beginthreadex وأقفال SRW ومتغيّرات الشرط ودوال Interlocked) هي الاختيار الواقعيّ نظراً للسجلّ وحجم المعلومات المتاحة.
- هل إضافة volatile تجعل علماً مشتركاً آمناً؟
- لا. لا يكبت volatile في C سوى تحسينات المترجِم مثل تخزين قيمة في سجلّ ── لا يضمن ذرّيّة العمليّة ولا ترتيب الذاكرة عبر المعالجات. قراءة أو كتابة بسيطة لمتغيّر 32 بت محاذًى كما ينبغي ذرّيّة بذاتها على Windows، لكن «اقرأ وأضِف واكتب رجوعاً» ينقسم إلى خطوات منفصلة، ولا ضمان لترتيبه نسبةً إلى عمليّات الذاكرة المحيطة. استخدم عائلة Interlocked لتحديث عدّاد أو علم مشترك. تحمل معظم دوال Interlocked حاجزاً ذاكريّاً كاملاً، فتحصل على ضمان ترتيب في الوقت نفسه. عندما تحتاج حماية عدّة متغيّرات معاً، استخدم قفل SRW أو CRITICAL_SECTION.
الملف الشخصي للمؤلف
صفحة الملف الشخصي لمؤلف المقالة.
غو كومورا
مؤسّس شركة كومورا سوفت ذ.م.م.
يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.