الإيقاظات الزائفة ── لماذا يستيقظ متغيّر الشرط «دون إشعار» وكيف تنتظر بشكل صحيح على Windows

· آخر تحديث: · · Windows, تعدّد مؤشّرات الترابط, متغيّرات الشرط, التزامن, C++, C#, Win32 API, استكشاف الأخطاء

سجل التعديلات (النسخة الأولى، نُشرت في 22 Aug، 2026)
النشر الأول
الاستشهاد بهذا المقال(DOI (الأرشيف المسجّل): 10.5281/zenodo.22176623)

تشير معرّفات DOI أدناه إلى إصدارات مؤرشفة سابقًا، وقد تختلف عن النص الحالي. للإشارة إلى النص الحالي، استخدم رابط هذه الصفحة.

غو كومورا (2026). الإيقاظات الزائفة ── لماذا يستيقظ متغيّر الشرط «دون إشعار» وكيف تنتظر بشكل صحيح على Windows. شركة كومورا سوفت ذ.م.م.. https://comcomponent.com/ar/blog/windows-condition-variable-spurious-wakeup/

DOI (الأرشيف المسجّل)
10.5281/zenodo.22176623
DOI (آخر إصدار مسجّل)
10.5281/zenodo.22241133

«نضع البيانات قبل الإشعار، ومع ذلك يقرأ العامل طابوراً فارغاً وينهار.» «نحن واثقون أنّنا أشعَرنا، ومع ذلك بين حين وآخر لا يعود المؤشّر من انتظاره أبداً.» كلاهما خطأ مزامنة، لكنّ أماكن الفحص مختلفة.

مركز هذا المقال مبدأ واحد: العودة من wait متغيّر شرط لا تضمن أنّ الشرط الذي كنت تنتظره متحقّق. متى فهمت الإيقاظات الزائفة، التي تعود بلا إشعار، والإيقاظات المسروقة، حيث يُستهلك الشرط بعد الإشعار، صار واضحاً لماذا يلزم while لا if. الإيقاظات المفقودة، حيث يُفوَّت إشعار، تُعالَج منفصلة عن هذين.

ما تريد معرفته أو ما تواجهه أين تقرأ
لماذا يستيقظ الانتظار بلا إشعار، وكيف يختلف ذلك عن إيقاظ مسروق ما الذي يُضمَن عند لحظة العودة، لماذا تسمح المواصفة بذلك
الشيفرة الصحيحة لـ Win32 وC++ وC# الشكل الأساسيّ لكلّ لغة
حُدِّدت مهلة، ومع ذلك يطول الانتظار الانتظار على أساس موعد نهائيّ
مؤشّر لا يعود رغم أنّه أُشعِر الإيقاظ المفقود وPulseEvent
تحقيق انهيار أو تعليق لا يحدث إلا نادراً خطوات التحقيق حسب العَرَض

القرّاء المستهدَفون المطوِّرون الذين يكتبون تطبيقات أعمال وبرمجيّات التحكّم بالمعدّات على Windows. يؤكِّد المقال الآليّة من المصادر الأوّليّة ويربطها بتنفيذات Win32 (C) وC++ وC#.

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

على شيفرة الانتظار أن تحفظ ثلاثة قواعد.

المبدأ ما يجب أن تفعله الشيفرة
انتظر حالة، لا إشعاراً اجعل الشرط «هل الطابور غير فارغ؟» وما شابه، لا «هل أُوقِظت؟»
أعد فحص الشرط كلّ مرّة تعود while (!condition) wait(...)؛ في C++ استخدم زيادة التحميل بالمحمول wait(lock, pred)
احمِ الفحص والتحديث بالقفل نفسه حدِّث الحالة قبل الإشعار، كي لا يتسلّل إشعار من الفجوة بين الفحص والانتظار

Win32 وC++ وPOSIX جميعاً تسمح، بالمواصفة، بإيقاظات غير مرتبطة بإشعار صريح. فوق ذلك، حتّى عندما يصل إشعار، يمكن لمؤشّر آخر أن يستهلك الشرط أوّلاً (إيقاظ مسروق)، لذا إعادة الفحص إلزاميّة. يُستخدَم Monitor.Wait في C# بالانضباط نفسه، مع الإيقاظات المسروقة في الحسبان.12345

التحذير الآخر ألّا تعيد إنشاء إشعار متغيّر الشرط العابر بنبضة على حدث. PulseEvent خصوصاً يمكن أن يفوِّت إشعاراً، وإرشاد مايكروسوفت ألّا تستخدمه في تطبيقات جديدة وأن تستخدم متغيّر شرط بدلاً منه.6

بقيّة المقال تسير بهذا الترتيب: كيف تعمل الإيقاظات (الفصول 2 إلى 4)، التنفيذ الصحيح (الفصل 5)، أنماط تُتجنَّب وكيف تحقِّق (الفصلان 6 و7)، وقائمة تحقّق (الفصل 8).

في المخطّط، يشير الخطّ المتّصل إلى علاقة قائمة دائماً، ويشير الخطّ المتقطّع إلى علاقة مشروطة (شروط قيامها مذكورة في شرح كلّ علاقة في الصفحة التفصيليّة). القائمة الكاملة للعلاقات (المجموع 17، مع الأدلّة ودرجة اليقين) وتعريفات المفاهيم الرئيسة مجمّعة في صفحة تفاصيل خريطة المعرفة (باليابانية). البيانات: JSON-LD / Turtle

2. ما الإيقاظ الزائف ── «استيقظت» لا تعني «الشرط متحقّق»

2.1 متغيّر الشرط يحرِّر القفل، ينتظر، ويعيد أخذه قبل العودة

متغيّر الشرط (condition variable) بدائيّة مزامنة تجعل مؤشّراً ينتظر حتّى يتحقّق شرط ما. على Win32 يستخدم البنية وواجهات البرمجة التالية.1

الدور نوع Win32 أو واجهته
يمثّل متغيّر الشرط CONDITION_VARIABLE
يحرِّر القفل وينتظر SleepConditionVariableCS / SleepConditionVariableSRW
يوقظ المؤشّرات المنتظِرة WakeConditionVariable / WakeAllConditionVariable

واجهة الانتظار تحرِّر ذريّاً القسم الحرج أو قفل SRW الذي يمسكه المؤشّر وتدخل الانتظار. بعد الاستيقاظ، تعيد أخذ ذلك القفل قبل العودة إلى المستدعي.1

غير أنّ إعادة أخذ القفل والعودة شيء مختلف عن تحقّق الشرط الذي كان التطبيق ينتظره.

2.2 ميّز الإشعار الحقيقيّ، والإيقاظ الزائف، والإيقاظ المسروق

تاركين معالجة المهلات للفصل 5، قارن أوّلاً حالات الاستيقاظ الثلاث.

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

الشكل 1: ثمّة ثلاثة مسارات للعودة من wait، والمستدعي لا يستطيع تمييز أيّها سلك، لذا يجب دائماً إعادة فحص الشرط.

الإيقاظات الزائفة ليست مقصورة على الحالة التي لم يُرسَل فيها إشعار في أيّ مكان في النظام. عندما تصل إشعارات في دفعة قصيرة، قد يوقظ التنفيذ، لأسبابه، مؤشّرات منتظِرة إضافيّة دفعة واحدة. من وجهة نظر مؤشّر ليس له إشعار صريح مقابل، ذلك أيضاً إيقاظ زائف.

تنصّ Microsoft Learn أنّ متغيّرات الشرط خاضعة لإيقاظات زائفة وإيقاظات مسروقة معاً، وتطلب إعادة فحص المحمول، عادةً في حلقة while، بعد عودة الانتظار. المحمول هنا يعني الشرط المُنتظَر، مثل «الطابور غير فارغ».1

2.3 قرِّر التقدّم من الشرط الحاليّ، لا من سبب استيقاظك

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

الـ while لا يمنع الإيقاظ الزائف أو الإيقاظ المسروق نفسه. إنّه هناك كي لا تتقدّم المعالجة، أيّاً منهما حدث، والشرط غير متحقّق. احفظ إعادة الفحص هذه وحماية القفل نفسه المشروحة في الفصل 5، فتعالج الشيفرة كلّ مسار إيقاظ بشكل صحيح.

3. لماذا تسمح المواصفة بذلك ── الإشعار الدقيق مكلف

3.1 كي لا تدفع كلّ عمليّة ثمن إشعار صارم

تنفيذ لا يُنتج إيقاظات زائفة أبداً ممكن نظريّاً. سبب سماح POSIX وWindows وC++ بها رغم ذلك يكمن في الأداء وفي تصميم يعيد فيه المنتظِر فحص الشرط. يشرح Rationale لـ pthread_cond_wait في POSIX هذا القرار أيضاً.3

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

لحلقة إعادة الفحص فائدة أخرى: تجعل القصد صريحاً في الشيفرة وتجعل الشيفرة أكثر متانة. عامل الإشعار لا كـ «ضمان أنّ الشرط متحقّق» بل كـ تلميح أنّ الشرط قد يكون تغيّر، فتحتمل الشيفرة تغييرات في جانب الإشعار كإيقاظ مؤشّرات إضافيّة أو إيقاظها دفعة.3

3.2 حتّى بلا إيقاظات زائفة، الإيقاظات المسروقة تبقى

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

الخطّ الزمنيّ لإيقاظ مسروقيضع المنتج عنصراً واحداً في الطابور ويوقظ المستهلك A المنتظِر، لكن قبل أن يعيد A أخذ القفل يأخذ المستهلك B القفل ويأخذ العنصر الواحد، لذا يكون الطابور فارغاً بحلول استيقاظ Aالمستهلك Bالمنتجالمستهلك A (منتظِر)المستهلك Bالمنتجالمستهلك A (منتظِر)استيقظ، ينتظر إعادة أخذ القفلالطابور فارغ (مسروق)إضافة عنصر واحد إلى الطابورWakeConditionVariableأخذ القفل وأخذ عنصر واحدإعادة أخذ القفل والعودة من waitإعادة الفحص في حلقة while والانتظار مرّة أخرى

الشكل 2: إيقاظ مسروق، فيه يستهلك مؤشّر ثالث الشرط أثناء فجوة الزمن بين الإشعار والاستيقاظ، يمكن أن يحدث تحت أيّ تنفيذ.

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

4. في أيّ طبقات تظهر على Windows

4.1 انضباط إعادة الفحص مشترك؛ أسباب الاستيقاظ تُميَّز

الواجهة أو المكتبة لماذا تلزم إعادة الفحص
متغيّرات شرط Win32 الإيقاظات الزائفة والإيقاظات المسروقة موثَّقة صراحة2
WaitOnAddress مسموح بالعودة مبكّراً لأسباب غير إشارة إلى العنوان المعطى7
std::condition_variable في C++ wait بلا محمول يمكن أن يستيقظ زائفاً48
Monitor.Wait في .NET يمكن لمؤشّر آخر أن يستهلك الشرط بين الاستيقاظ وإعادة أخذ القفل5

عيّنة طابور المنتج-المستهلك الرسميّة في Win32 تكتب انتظارها أيضاً في حلقة while.9

WaitOnAddress، المتاح على Windows 8 فما بعده، واجهة منخفضة المستوى تنتظر تغيّر القيمة عند عنوان معطى. كأمثلة للعودة مبكّراً بلا إشارة، تسرد الوثائق الرسميّة حالة ذاكرة منخفضة، والتخلي عن إيقاظ سابق للعنوان نفسه، والتشغيل على بناء checked. مثال استخدامها كذلك حلقة while تقارن القيمة مرّة أخرى.7

4.2 انتظار المحمول في C++ يؤدّي الحلقة عنك

تشرح وثائق MSVC أنّ wait(lock, pred) ينفِّذ فعليّاً ما يلي.4

while (!Pred())
    wait(Lck);

تنصّ cppreference أيضاً صراحة أنّ wait بلا محمول يمكن أن يعود زائفاً. بشكل المحمول، يمكن ترك حلقة إعادة الفحص هذه للمكتبة.8

4.3 في C#، ركِّز على الإيقاظات المسروقة وإعادة أخذ القفل

يستخدم Monitor.Wait / Pulse طابور انتظار وطابور جاهزيّة. مؤشّر يوقظه Pulse / PulseAll ينتقل إلى طابور الجاهزيّة ولا يغادر Wait إلا بعد إعادة أخذ القفل. أنّ مؤشّراً آخر يمكنه استهلاك الشرط أوّلاً في تلك الفترة هو نفسه كما على Win32.510

مع Monitor.Wait في .NET، بدل افتراض إيقاظ بلا سبب من النوع الذي لمتغيّرات الشرط، فكِّر فيه منفصلاً: انضباط while نفسه لازم بسبب الإيقاظات المسروقة والمهلات. تفترض الوثائق كذلك استخداماً يعيد فيه المؤشّر تقييم الشرط الذي جعله ينتظر ويستدعي Wait مرّة أخرى إن لزم.5

كلّ طبقة تتطلّب إعادة فحص المحمولفي كلّ طبقة، std::condition_variable في C++ وMonitor في .NET وCONDITION_VARIABLE في Win32 وWaitOnAddress المنخفض، تتطلّب الوثائق الرسميّة إعادة فحص الشرط بعد الاستيقاظC++ std::condition_variableعند الاستيقاظ، أعد فحص الشرط (while).NET Monitor.WaitWin32 CONDITION_VARIABLEWaitOnAddress

الشكل 3: أيّاً كانت اللغة أو الإطار، كلّ طبقة بدائيّة انتظار تتطلّب رسميّاً إعادة فحص بعد الاستيقاظ.

5. الطريقة الصحيحة للانتظار ── اكتبه بـ while ومحمول

الشكل المشترك: افحص الشرط، وتقدّم فقط عندما يتحقّق

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

تدفّق حلقة انتظار صحيحةخذ القفل وافحص الشرط؛ إن لم يتحقّق، حرِّر القفل ونَم؛ عند الاستيقاظ، أعد أخذ القفل وعُد إلى فحص الشرط. تقدّم والقفل ممسوك فقط عندما يتحقّق الشرطلانعمأخذ القفلهل الشرط متحقّق؟wait (حرِّر القفل ونَم)استيقظ (أعد أخذ القفل)عالِج وأنت ما زلت تمسك القفل

الشكل 4: الانتظار الصحيح حلقة، ولا فجوة بين فحص الشرط والمعالجة (كلاهما يحدث والقفل ممسوك).

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

الشكل الأساسيّ في Win32 (C)

CRITICAL_SECTION cs;
CONDITION_VARIABLE cv;
int queueCount = 0;   // حالة مشتركة يحميها cs

// هيِّئ مرّة واحدة عند البدء (للتهيئة الساكنة، cv = CONDITION_VARIABLE_INIT)
InitializeCriticalSection(&cs);
InitializeConditionVariable(&cv);

// جانب المنتظِر (المستهلك)
EnterCriticalSection(&cs);
while (queueCount == 0) {                       // دائماً while، لا if أبداً
    SleepConditionVariableCS(&cv, &cs, INFINITE);
}
// هنا يُمسَك القفل ويُضمَن queueCount > 0
--queueCount;
LeaveCriticalSection(&cs);

// جانب المُشعِر (المنتج)
EnterCriticalSection(&cs);
++queueCount;                                    // حدِّث الحالة داخل القفل
LeaveCriticalSection(&cs);
WakeConditionVariable(&cv);                      // الإشعار بعد تحرير القفل جائز

التمييز الذي يُحفَظ هو بين تحديث الحالة وأين يُستدعى الإشعار. يجب أن يحدث ++queueCount دائماً داخل القفل. WakeConditionVariable، من جهة أخرى، يمكن استدعاؤه من داخل القفل أو خارجه. تقول مايكروسوفت إنّه عادةً أفضل الإيقاظ بعد تحرير القفل، لتقليل تبديلات السياق.1

الشكل الأساسيّ في C++ ── اجعل انتظار المحمول الافتراضيّ

std::mutex m;
std::condition_variable cv;
std::queue<Item> q;

// جانب المنتظِر
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, [&] { return !q.empty(); });   // داخليّاً while(!pred) wait
Item item = std::move(q.front());
q.pop();
lk.unlock();

// جانب المُشعِر
{
    std::lock_guard<std::mutex> lk(m);
    q.push(std::move(item));
}
cv.notify_one();

الشيفرة الجديدة ينبغي أن تجعل wait بالمحمول افتراضيّاً. while (q.empty()) cv.wait(lk); القائم أيضاً شكل صحيح، لذا لا حاجة لإعادة كتابته فقط لأنّ الحلقة مكتوبة يدوياً. المشكلة if (q.empty()) cv.wait(lk);، الذي لا يعيد الفحص.

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

الشكل الأساسيّ في C#

private readonly object _gate = new();
private readonly Queue<Item> _queue = new();

// جانب المنتظِر
lock (_gate)
{
    while (_queue.Count == 0)          // دائماً while، لا if أبداً
    {
        Monitor.Wait(_gate);
    }
    var item = _queue.Dequeue();
}

// جانب المُشعِر
lock (_gate)
{
    _queue.Enqueue(item);
    Monitor.Pulse(_gate);              // لا يُستدعى Pulse في Monitor إلا داخل القفل
}

يُستدعى Monitor.Wait / Pulse / PulseAll داخل كتلة متزامنة تمسك القفل المعنيّ. خارج القفل ترمي SynchronizationLockException. لا تخلط هذا بـ Win32، حيث يمكن نقل الإشعار خارج القفل.10

الانتظار بمهلة ── احسب الزمن المتبقّي من موعد نهائيّ

تمرير قيمة المهلة نفسها كلّ مرّة يضيف إلى الانتظار كلّ مرّة يعود إيقاظ زائف. استخدم الشكل قرِّر الموعد النهائيّ أوّلاً، واحسب الزمن المتبقّي كلّ مرّة تعود.

ULONGLONG deadline = GetTickCount64() + timeoutMs;
EnterCriticalSection(&cs);
while (queueCount == 0) {
    ULONGLONG now = GetTickCount64();
    if (now >= deadline) {
        break;                          // مهلة (الشرط ما زال غير متحقّق)
    }
    if (!SleepConditionVariableCS(&cv, &cs, (DWORD)(deadline - now)) &&
        GetLastError() != ERROR_TIMEOUT) {
        break;                          // عند فشل غير المهلة، أوقف الانتظار واخرج
    }
    // ERROR_TIMEOUT يتلقّى فحصه النهائيّ في شرط while وفحص الموعد النهائيّ
}
BOOL ready = (queueCount > 0);
if (ready) { --queueCount; }
LeaveCriticalSection(&cs);
التدفّق الصحيح لانتظار بمهلةقرِّر الموعد النهائيّ أوّلاً، وكلّ مرّة تستيقظ افحص الشرط والموعد؛ إن لم يمرّ الموعد، أعد حساب الزمن المتبقّي وعُد إلى الانتظارنعملانعملاقرِّر الموعد النهائيّهل الشرط متحقّق؟تقدّم إلى المعالجةهل مرّ الموعد النهائيّ؟عالج المهلةاحسب الزمن المتبقّي وانتظر

الشكل 5: انتظار بمهلة لا يمرِّر «زمن الانتظار نفسه» مرّة أخرى؛ يعيد حساب الزمن المتبقّي من الموعد النهائيّ.

في C++، يمكن ترك هذه المعالجة لزيادة تحميل المحمول في wait_until، التي تأخذ زمناً مطلقاً. لأنّها تُرجع القيمة النهائيّة للمحمول حتّى عند المهلة، يمكنك القرار بما إذا كان الشرط قد تحقّق في النهاية.4

6. أنماط تُتجنَّب

6.1 فحص الشرط مرّة واحدة فقط بـ if

عندما يحدث إيقاظ زائف أو إيقاظ مسروق، تتقدّم المعالجة والشرط غير متحقّق. يظهر كانهيار نادر أو فساد بيانات: أخذ من طابور فارغ، قراءة بيانات غير مهيَّأة، تحرير مزدوج. غيِّره إلى while أو wait بالمحمول في الفصل 5.

6.2 فحص الشرط أو تحديثه خارج القفل

هذه ليست مسألة «الاستيقاظ أكثر ممّا ينبغي» بل إيقاظ مفقود: تفويت الإشعار والنوم إلى الأبد.

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

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

الشكل 6: فحص الشرط خارج القفل يتيح لإشعار أن يمرّ مباشرةً من الفجوة بين الفحص والانتظار: إيقاظ مفقود.

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

6.3 إعادة إنشاء إشعار عابر بنبضة على حدث

استخدام CreateEvent وSetEvent ليس بذاته نمطاً سيّئاً. للأحداث استخدامات مشروعة كالتي تلي.

الغرض متى تستخدم حدثاً
إيقاظ مستهلك واحد تصميم يعالج فيه المستهلك الموقَظ الطابور حتّى يفرغ
الإشارة إلى توقّف تمثيل تعليمات توقّف، متى رُفعت لا تُخفَض أبداً، بحدث إعادة ضبط يدويّ
الانتظار مع أهداف انتظار أخرى تضمينه في WaitForMultipleObjects
مزامنة عبر العمليّات استخدام لا يستطيع متغيّر شرط، الذي لا يُشارَك بين العمليّات، معالجته

متغيّر الشرط كائن وضع مستخدم لا يمكن مشاركته بين العمليّات. بحسب الاستخدام، استبدال الحدث بمتغيّر شرط غير ممكن أصلاً.1

ما هو خطر محاولة إعادة إنشاء بحدث سلوك متغيّر الشرط «إيقاظ من ينتظر في تلك اللحظة فقط وعدم ترك حالة إشعار خلفه». تلك الفكرة تؤدّي إلى مشكلة PulseEvent التالية.

6.4 استخدام PulseEvent

PulseEvent واجهة، على حدث إعادة ضبط يدويّ، توقظ المؤشّرات المنتظِرة في تلك اللحظة وتعيد الحدث فوراً إلى الحالة غير المُشارة. غير أنّ مايكروسوفت تنصّ صراحة أنّها غير موثوقة وتوجد أساساً للتوافق الخلفيّ، لذا لا ينبغي للتطبيقات الجديدة استخدامها وأن تستخدم متغيّر شرط بدلاً منها.6

السبب أنّ مؤشّراً منتظِراً يمكن إخراجه مؤقّتاً من حالة الانتظار بـ APC في وضع النواة والعودة إلى الانتظار بعد اكتمال APC. إن استُدعي PulseEvent في تلك الفترة، لا يكون المؤشّر ضمن «المؤشّرات المنتظِرة لحظة الاستدعاء» ولا يُوقَظ. APC النواة سلوك داخليّ لنظام التشغيل لا يستطيع التطبيق التحكّم فيه.611

هذه المشكلة أيضاً تحذير التحليل الساكن C28648.12 الإيقاظ الزائف مشكلة «الاستيقاظ أكثر ممّا ينبغي»؛ هذه مشكلة «عدم الاستيقاظ عندما ينبغي». لأنّ الإشعار نفسه يُفقَد، لفّ الانتظار في while وحده لا ينقذك.

6.5 الإشعار دون إمساك القفل، قبل تحديث الحالة

إن أشعرت أوّلاً دون إمساك القفل ثمّ أخذت القفل بعد ذلك وحدَّثت الحالة، قد يرى المؤشّر الموقَظ الحالة القديمة ويعود إلى النوم. إن لم يتبع الإشعار التحديث، يبقى هناك.

ميّز الحالتين التاليتين مع ذلك.

الترتيب النتيجة
أشعِر خارج القفل، ثمّ خذ القفل وحدِّث الحالة ثمّة فجوة يعيد فيها المؤشّر الموقَظ الفحص قبل التحديث وينام
أشعِر، حدِّث، ثمّ حرِّر، كلّ ذلك وأنت تمسك القفل نفسه لا يستطيع المنتظِر الفحص حتّى يعيد أخذ القفل، لذا هذا الترتيب لا يسبّب ضرراً فعليّاً

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

7. كيف تحقِّق عندما تواجهه

7.1 افصل «يتقدّم رغم أنّ الشرط غير متحقّق» عن «لا يستيقظ»

العَرَض ما تفحصه أوّلاً كيف تحقِّق
أخذ من طابور فارغ، انهيار، نتائج ناقصة ما إذا أُعيد فحص الشرط بعد الانتظار ابحث حول cv.wait( بلا محمول، وSleepConditionVariableCS، وMonitor.Wait
مؤشّر ينبغي أن يستيقظ لا يعود، وتعلَّق العمليّة ما إذا كان ثمّة إيقاظ مفقود أو PulseEvent افحص مكدّس كلّ مؤشّر في تفريغ، وتتبع من واجهة الانتظار العالق فيها إلى جانب الإشعار

إيجاد cv.wait(lk) بلا محمول ليس مشكلة إن كان ملفوفاً في while صحيح. اجمع المرشّحين بالبحث، ثمّ راجع ما إذا كانت الشيفرة المحيطة if فقط، وما إذا فُحص الشرط وحُدِّث تحت القفل نفسه. هذه مواضع يمكنك فحصها دون انتظار إعادة إنتاج.

للتعليق، حدِّد موضع الانتظار من تفريغ، ثمّ تتبّع في الشيفرة من كان يُفترَض أن يُشعِر، وبأيّ ترتيب. افحص فحوص الشرط خارج القفل، والإشعارات خارج القفل قبل تحديث الحالة، وPulseEvent.

تدفّق الفرز من العَرَضإن كان العَرَض تقدّم المعالجة والشرط غير متحقّق، ابحث في الشيفرة عن انتظارات بلا محمول؛ إن كان العَرَض مؤشّراً لا يستيقظ، حدِّد موضع الانتظار من تفريغ واشتبه في إيقاظ مفقود أو PulseEventخطأ لا يظهر إلا نادراًالمعالجة تتقدّم والشرط غير متحقّقمؤشّر ينبغي أن يستيقظ لا يفعلابحث في الشيفرة عن انتظارات بلا محمولحدِّد المؤشّرات المنتظِرة من تفريغغيِّر if إلى while أو wait بمحمولاشتبه في إيقاظ مفقود أو PulseEvent

الشكل 7: ما إذا كان العَرَض «التقدّم أبعد ممّا ينبغي» أو «عدم الاستيقاظ أبداً» يقرِّر أين تنظر وكيف تحقِّق.

7.2 قارن قبل الإصلاح وبعده تحت شروط الإجهاد نفسها

لإعادة إنتاج خطأ نادر، وسِّع نافذة السباق وزِد تباين التوقيت. تشمل الطرق استخدام مؤشّرات أكثر من الأنوية الفيزيائيّة، وإدخال Sleep تشخيصيّ بين الانتظار والإشعار، وتجربة بناءَي التصحيح والإصدار.

عندما تقارن لتستنتج «توقّف عن إعادة الإنتاج بعد أن أصلحنا الانتظار»، طبِّق الإجهاد نفسه قبل الإصلاح وبعده.

8. الخلاصة ── قائمة تحقّق

الإشعار تلميح أنّ الشرط قد يكون تغيّر؛ مسوّغ الاستمرار هو الشرط نفسه، المفحوص داخل القفل.

البند الشكل الصحيح
بعد العودة من الانتظار لا تحاول تمييز إشعار حقيقيّ وإيقاظ زائف وإيقاظ مسروق؛ أعد فحص الشرط
كيف يُكتَب الانتظار لفّه في while. شيفرة C++ جديدة تجعل wait(lock, pred) بالمحمول افتراضيّاً
الحالة المشتركة احمِ الفحص والتحديث بالقفل نفسه، وحدِّث الحالة قبل الإشعار
أين تُشعِر Win32/C++ يجوز الإشعار بعد تحرير القفل. Pulse في C# يُستدعى داخل القفل
المهلات احسب الزمن المتبقّي من موعد نهائيّ. في C++ استخدم wait_until بمحمول
استخدام الأحداث ميّز الاستخدامات المشروعة عن النبضات العابرة، ولا تعتمد على PulseEvent

الإيقاظات الزائفة مواصفة سمح بها Win32 وC++ وPOSIX عن قصد. الاستجابة انضباط في جانب الانتظار، لا انتظار إصلاح من نظام التشغيل أو تبديل مكتبات. Monitor.Wait في .NET يُميَّز عن الإيقاظات بلا سبب، لكنّه يؤدّي إعادة الفحص نفسها للتصدّي للإيقاظات المسروقة والمهلات.

للانهيار، ابدأ من «الموضع الذي يتقدّم وإن لم يتحقّق الشرط»؛ للتعليق، من «الموضع الذي يفوِّت الإشعار». استخدم هذا التمييز وقائمة التحقّق عند مراجعة شيفرة قائمة أيضاً.

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

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

تتعامل شركة كومورا سوفت ذ.م.م. مع مراجعات تصميم شيفرة متعدّدة مؤشّرات الترابط، وتحقيق السبب الجذريّ (تحليل التفريغ) لانهيارات وتعليقات «لا تُعاد إنتاجها إلا أحياناً»، وإعادة صياغة شيفرة مزامنة تراثيّة (شيفرة تعتمد على أحداث وPulseEvent وما شابه) إلى أساس متغيّر شرط. البدء من عزل العَرَض مقبول؛ لا تتردّد في التواصل.

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

  1. Microsoft Learn, Condition Variables. حول كون متغيّر الشرط كائن وضع مستخدم يحرِّر القفل ويدخل الانتظار ذريّاً؛ وحول وجود إيقاظات زائفة (إيقاظات غير مرتبطة بإيقاظ صريح) وإيقاظات مسروقة (مؤشّر آخر يعمل قبل المؤشّر الموقَظ)، لذا ينبغي إعادة فحص المحمول في حلقة while بعد العودة من الانتظار؛ وحول إمكان الإشعار من داخل القفل أو خارجه، لكنّ الإيقاظ بعد تحرير القفل أفضل لتقليل تبديلات السياق. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7

  2. Microsoft Learn, SleepConditionVariableCS function (synchapi.h). حول تحرير القسم الحرج المحدَّد والانتظار على متغيّر الشرط ذريّاً؛ وحول إعادة المؤشّر الموقَظ أخذ القسم الحرج قبل العودة؛ وحول إرجاع ERROR_TIMEOUT عند المهلة؛ وحول وجود إيقاظات زائفة وإيقاظات مسروقة، لذا ينبغي إعادة فحص المحمول (عادةً في حلقة while) بعد العودة من الانتظار. ↩ ↩2

  3. The Open Group Base Specifications, pthread_cond_timedwait, pthread_cond_wait. حول إمكان إيقاظات زائفة من pthread_cond_wait / pthread_cond_timedwait؛ وحول أنّ العودة من الانتظار لا تعني شيئاً عن قيمة المحمول، لذا ينبغي إعادة تقييم المحمول؛ وحول نصّ Rationale أنّ تنفيذاً «يوقظ واحداً بالضبط» يمكن أن يبطئ عمليّات متغيّر الشرط، خصوصاً على متعدّدات المعالجات، وأنّ السماح بالإيقاظات الزائفة يفرض حلقة فحص محمول ويجعل التطبيقات أكثر متانة. ↩ ↩2 ↩3 ↩4

  4. Microsoft Learn, condition_variable Class. حول النصّ أنّ wait بلا محمول يُرفَع بـ notify_one / notify_all ويمكن أيضاً أن يستيقظ زائفاً؛ وحول تنفيذ wait(lock, pred) بالمحمول فعليّاً while (!Pred()) wait(Lck);؛ وحول امتلاك wait_for / wait_until الخاصيّة نفسها وزيادات تحميل بالمحمول. ↩ ↩2 ↩3 ↩4

  5. Microsoft Learn, Monitor.Wait Method. حول تحرير Wait للقفل ودخوله طابور الانتظار؛ وحول عدم العودة بعد الإيقاظ بـ Pulse / PulseAll حتّى يُعاد أخذ القفل؛ وحول الاستخدام المقصود أنّ المؤشّر الموقَظ يعيد تقييم الشرط الذي جعله ينتظر ويستدعي Wait مرّة أخرى إن لزم. ↩ ↩2 ↩3 ↩4

  6. Microsoft Learn, PulseEvent function (winbase.h). حول إمكان إخراج مؤشّر منتظِر مؤقّتاً من حالة الانتظار بـ APC في وضع النواة والعودة بعد اكتمال APC، لذا لا يُحرَّر المؤشّر إن استُدعي PulseEvent في تلك الفترة؛ وحول كون PulseEvent لذلك غير موثوق، لا يُستخدم في تطبيقات جديدة، ويُستبدَل بمتغيّر شرط. ↩ ↩2 ↩3

  7. Microsoft Learn, WaitOnAddress function (synchapi.h). حول ضمان عودة الدالّة التي تنتظر تغيّر القيمة عند عنوان عند الإشارة، مع السماح أيضاً بالعودة لأسباب أخرى؛ وحول أمثلة الاستيقاظ المبكّر كونها حالة ذاكرة منخفضة، والتخلي عن إيقاظ سابق للعنوان نفسه، والتشغيل على بناء checked؛ وحول حاجة مقارنة القيمة مرّة أخرى بعد العودة، والعيّنة الرسميّة نفسها حلقة while. ↩ ↩2

  8. cppreference.com, std::condition_variable::wait. حول إمكان رفع wait بلا محمول بإيقاظ زائف؛ وحول تكافؤ زيادة التحميل بالمحمول مع while (!pred()) wait(lock); وتعريفها كحلقة تعيد أخذ القفل وتفحص المحمول عند كلّ إشعار أو إيقاظ زائف. ↩ ↩2

  9. Microsoft Learn, Using Condition Variables. حول العيّنة الرسميّة التي تنفِّذ طابور منتج-مستهلك بقسم حرج واحد ومتغيّري شرط (BufferNotEmpty وBufferNotFull). الانتظار يُؤدَّى داخل حلقة تفحص المحمول. ↩

  10. Microsoft Learn, Monitor.PulseAll Method. حول نقل PulseAll المؤشّرات في طابور الانتظار إلى طابور الجاهزيّة، وأخذ المؤشّر التالي في طابور الجاهزيّة القفل عندما يُحرَّر القفل؛ وحول استدعاء Pulse / PulseAll / Wait فقط من داخل كتلة متزامنة. ↩ ↩2

  11. Microsoft Learn, Waits and APCs. حول تنفيذ APC النواة بشكل استباقيّ، ومقاطعة النظام داخليّاً للانتظار واستئنافه دون العودة من واجهة الانتظار، لذا يمكن تفويت إشارة عابرة مثل KePulseEvent في تلك الفترة. ↩

  12. Microsoft Learn, C28648: PulseEvent is an unreliable function. حول تحذير التحليل الساكن من استخدام PulseEvent؛ وحول أنّ مؤشّراً كان خارج الانتظار بسبب APC لا يُحرَّر ويمكن أن يعلَّق إلى الأبد؛ وحول الإرشاد لاستبداله بـ SetEvent أو كائن مزامنة آخر. ↩

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

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

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

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

هل الإيقاظ الزائف خطأ في نظام التشغيل أو المكتبة؟
ليس خطأ؛ إنّه سلوك منصوص عليه في المواصفة. SleepConditionVariableCS في Win32، وstd::condition_variable في C++، وpthread_cond_wait في POSIX، كلّها لها وثائق رسميّة أو معيار ينصّ صراحةً أنّ إيقاظاً غير مرتبط بإشعار يمكن أن يحدث. تنفيذ يمنعه ممكن نظريّاً، لكنّه سيبطئ كلّ عمليّة متغيّر شرط (الإشعار على متعدّدات المعالجات خصوصاً)، لذا يُسمَح بالسلوك على فهم أنّ الصحّة تُحفَظ ما دام المنتظِر يعيد فحص الشرط. العلاج إذن ليس انتظار إصلاح من نظام التشغيل، بل كتابة الانتظار دائماً داخل حلقة while (أو انتظار بمحمول).
هل لفّ الانتظار في حلقة while يضرّ بالأداء؟
عمليّاً الكلفة ضئيلة. كلّ ما تضيفه حلقة while فحص شرط واحد في كلّ مرّة يستيقظ المؤشّر، وذلك مقارنة رخيصة وأنت تمسك القفل أصلاً. الإيقاظات الزائفة نفسها نادرة، لذا دورة الحلقة الإضافيّة تحدث في حالات استثنائيّة فقط. ثمن ترك if في مكانه، من جهة أخرى، خطأ لا يُعاد إنتاجه إلا نادراً، فيه تتقدّم المعالجة والشرط غير متحقّق؛ لا مقارنة. ما يهيمن فعليّاً على كلفة انتظار متغيّر الشرط هو تنازع الأقفال وكم مرّة تُشعِر، لا ما إذا كان الـ while موجوداً.
إن استخدمت انتظار المحمول في C++، هل يمكنني نسيان الإيقاظات الزائفة؟
لحلقة الانتظار، نعم: cv.wait(lock, pred) ينفِّذ فعليّاً while (!pred()) wait(lock); لذا تُمتَصّ الإيقاظات الزائفة والإيقاظات المسروقة تلقائيّاً. شيفرة C++ جديدة ينبغي أن تجعل زيادة التحميل بالمحمول افتراضيّة. ما زال عليك حماية تحديثات الحالة المشتركة التي يقرأها المحمول بالقفل نفسه، وما زال على المُشعِر تحديث تلك الحالة قبل استدعاء notify. انتظار المحمول يأخذ الحلقة عن يديك؛ لا يأخذ انضباط الأقفال عن يديك.
هل تحدث المشكلة نفسها مع Monitor.Wait في C#؟
نعم. مؤشّر ينتظر في Monitor.Wait يوقظه Pulse/PulseAll ثمّ يعيد أخذ القفل قبل مغادرة Wait، لكن في تلك الفترة قد يكون مؤشّر آخر أخذ القفل أوّلاً واستهلك الشرط (إيقاظ مسروق). وثائق مايكروسوفت مكتوبة أيضاً على افتراض أنّ المؤشّر الموقَظ يعيد تقييم الشرط الذي جعله ينتظر، ويستدعي Wait مرّة أخرى إن لزم. لذا الشكل الأساسيّ في C# أيضاً while (!condition) Monitor.Wait(gate);. أنّ Wait/Pulse لا يُستدعيان إلا داخل عبارة lock قيد يختلف عن Win32.
هل تحدث إيقاظات زائفة أيضاً عندما تنتظر حدثاً بـ WaitForSingleObject؟
في انتظار عاديّ (غير قابل للتنبيه)، يُرجَع WAIT_OBJECT_0 فقط عندما يصير الكائن مُشاراً إليه فعليّاً، لذا لا يوجد «إيقاظ بلا سبب» من النوع الذي لمتغيّرات الشرط. غير أنّ «صار الحدث مُشاراً إليه» و«شرط تطبيقك متحقّق» شيئان مختلفان. في تصميم يُوقَظ فيه عدّة مستهلكين بالحدث نفسه، فإنّ المؤشّر الذي يأخذ القفل أوّلاً يستهلك الشرط، لذا ما زلت تحتاج فحص الشرط بعد الاستيقاظ. كذلك، تصميم يحاول إعادة إنشاء إشعار متغيّر الشرط العابر، الذي يوقظ فقط من ينتظر في تلك اللحظة، بحدث يميل إلى الاصطدام بمشكلة موثوقيّة PulseEvent، لذا لانتظار تحقّق حالة داخل عمليّة، متغيّر الشرط الخيار الآمن.

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

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

غو كومورا

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

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

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