لماذا يُفضَّل انتظار الأحداث على Sleep(1) في Windows

· آخر تحديث: · · تطوير Windows, مزامنة, أحداث, مؤقّتات, تصميم

سجل التعديلات (2 تحديثات، آخر تحديث 2 Sep، 2026)

سجل بالتغييرات التي أُجريت على هذا المقال. وحيثما حُفظت نسخة سابقة، تبقى متاحة للقراءة عبر رابط دائم يحمل معرّف DOI.

أُعيدَت الترجمة العربية كترجمة كاملة عن النص الياباني الأصلي، وأُضيفَت خريطة المعرفة.
أعيدت الترجمة كترجمة كاملة عن النص الياباني الأصلي. كانت النسخة العربية السابقة مختصراً يسقط أبواباً وجداول ورسوم Mermaid وتعليقات الأشكال وFAQ. أُعيدت هذه العناصر وفق الأصل الياباني، والادّعاءات التقنية مطابقة للنسخة اليابانية.
النشر الأول
الاستشهاد بهذا المقال(DOI: 10.5281/zenodo.21621392)

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

小村 豪 (2026). لماذا يُفضَّل انتظار الأحداث على Sleep(1) في Windows. شركة كومورا سوفت ذ.م.م.. https://doi.org/10.5281/zenodo.21621392 https://comcomponent.com/ar/blog/2026/03/16/006-windows-timer-vs-event-wait/

DOI (أحدث نسخة)
10.5281/zenodo.21621392
DOI (هذه النسخة)
10.5281/zenodo.22240849

في الدليل العملي للزمن الناعم على Windows السابق كتبت عن تجنّب الحلقة الدورية التي تعتمد على Sleep. هذه المرّة أضيّق ذلك إلى نقطة واحدة: لماذا يُفضَّل انتظار الحدث على انتظار المؤقّت القصير.

تُقرأ هذه المقالة وحدها. خلاصة السابق سطر واحد: «حلقة التحسّس التي تكل أمر الدورة إلى Sleep لا تضمن زمن الانتظار ولا توقيت الاستيقاظ، فلا تُتَّخذ أساساً لمعالجة دورية». إن ثبت هذا في الرأس، يمكن المتابعة دون قراءة المقال السابق.

على Windows، تصميم «أتحسّس كل فترة» بـ Sleep(1) أو انتظار بـ timeout قصير يتأثّر لا محالة بـ دقّة ساعة النظام وتأخير الجدولة بعد ذلك. في الإعداد العادي كثيراً ما يكون الافتراض platform timer resolution من رتبة 15.6ms، لذلك نيّة «أنظر مرّة أخرى بعد 1ms» تصير انتظاراً خشناً بسهولة.

بالمقابل، إن كان ما تريد انتظاره فعلاً ليس «زمناً» بل «واقعة» مثل وصول عمل أو اكتمال I/O أو طلب إيقاف أو تغيّر حالة، فلا حاجة للنظر على فترات ثابتة. الجانب الذي وقعت فيه الواقعة يُشير، والجانب المنتظر ينتظر الحدث أوضح لزمن التأخير ولـ CPU وللطاقة.

طريقة الانتظار المدفوع بالأحداثمخطّط يبيّن أنه إن كان ما تريد انتظاره واقعة لا زمناً، فالأوضح لزمن التأخير ولـ CPU وللطاقة ألا تنظر على فترات ثابتة، بل أن يُشير الجانب الذي وقع وينتظر الجانب الآخر الحدث.ما يُراد انتظاره واقعةالجانب الذي وقع يُشيرالجانب المنتظر ينتظر الحدثأوضح لزمن التأخير ولـ CPU وللطاقة

الشكل 1: إن كنت تنتظر واقعة، فاطلب الإشعار من الجانب الذي وقع بدل التحسّس على فترات ثابتة.

الأسئلة التي تريد هذه المقالة الإجابة عنها أربعة.

  • لماذا Sleep(1) وانتظار المؤقّت القصير أقل دقّة ممّا يُظن
  • لماذا يتأثّر انتظار الحدث بهذا القيد أقل
  • في أي مشهد ينبغي اختيار الحدث لا المؤقّت
  • مع ذلك، متى ينبغي استخدام المؤقّت

مصطلحات تظهر في هذه المقالة

نضع أولاً الاختصارات التي تظهر في المتن بلا شرح.

المصطلح المعنى
platform timer resolution / system clock resolution فاصل تحديث نظام التشغيل للوقت. حكم timeout في الانتظار المؤقّت ينجذب إلى هذه الدقّة
ISR (Interrupt Service Routine) معالجة تجري بأولويّة قصوى عند وقوع مقاطعة. بينما تجري، ينتظر خيطك
DPC (Deferred Procedure Call) معالجة مؤجّلة عالية الأولويّة يكدّسها ISR «ليكمل لاحقاً». مع ISR يكفي في هذه المقالة قراءتها عامل تأخير مرتبط بمعالجة المقاطعة
IOCP (I/O Completion Port) آلية Windows تجمع إشعارات اكتمال I/O غير المتزامن في طابور وتستقبلها مجموعة خيوط مخصّصة
WaitOnAddress واجهة مزامنة «انتظر حتى تتغيّر قيمة عنوان ذاكرة». داخل العملية نفسها فقط (تُعالج في 5.3)
الإشارة (signal) تحقيق شرط الجانب المنتظر. للحدث يعني استدعاء SetEvent

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

  • إن كنت تنتظر وصول عمل أو اكتمال I/O، فانتظار الحدث أولى من المؤقّت.
  • انتظار Windows المؤقّت يتأثّر لا محالة بدقّة ساعة النظام.
  • Sleep(1) لا يعني «استيقظ بدقّة بعد 1ms».
  • ثم حتى بعد مرور timeout يصير الخيط ready أولاً فقط، بلا ضمان تنفيذ فوري.
  • لذلك تصميم «ينتظر واقعة في الحقيقة، ويتحسّس بمؤقّت» غير مواتٍ لزمن التأخير وللطاقة.
  • استخدم المؤقّت فقط عندما يكون الزمن نفسه هو الشرط، فهذا أوضح.

بلغة الميدان تقريباً هذا.

  • «أرسل مقاييس كل 5 ثوانٍ» -> عمل مؤقّت
  • «تحرّك فور دخول عمل إلى الطابور» -> عمل event / semaphore / condition variable / WaitOnAddress
  • «نفّذ المتابعة بعد انتهاء I/O» -> عمل completion / event
  • «توقّف عند وصول طلب إيقاف» -> عمل stop event / cancellation
خط الفصل بين المؤقّت والحدثمخطّط يبيّن أن المؤقّت يُستخدم فقط عندما يكون الزمن نفسه هو الشرط، وأن واقعة مثل وصول عمل أو اكتمال I/O أو طلب إيقاف تُنتظر بحدث.الزمن نفسهواقعةما يُراد انتظاره زمن أم واقعةعمل مؤقّتعمل انتظار حدثوصول عمل / اكتمال I/O / طلب إيقاف

الشكل 2: احصر المؤقّت في الزمن نفسه شرطاً، وانتظر الوقائع بحدث.

خريطة المعرفة لهذه المقالة

انتظار المؤقّت القصير على Windows مقيّد بدقّة system clock resolution، وحتى عند حلول timeout يصير الخيط ready فقط ويُترك بدء التنفيذ للـ scheduler، لذلك تصميم polling يتحسّس الطابور أو طلب الإيقاف بـ Sleep(1) أقل دقّة ممّا يبدو. التحويل إلى تصميم مدفوع بالأحداث يُعلم فيه المنتج بـ SetEvent وينتظر المستهلك بـ WaitForSingleObject أو WaitForMultipleObjects يجعل نهاية الانتظار إشارة لا انتهاء مهلة، فيزول الدوران الفارغ. اختيار الأداة يعني حدث overlapped I/O أو IOCP لاكتمال I/O، وWaitOnAddress لتغيّر قيمة داخل العملية نفسها، وwaitable timer فقط عندما يكون الزمن نفسه هو الشرط، ورفع الدقّة بـ timeBeginPeriod ليس حلاً جذرياً.

خريطة معرفة polling بالمؤقّت مقابل الانتظار المدفوع بالأحداثمخطّط يبيّن أن انتظار المؤقّت القصير يحمل عدم يقين مزدوجاً من system clock resolution وتأخير الجدولة، وكيف يعالج الانتظار المدفوع بالأحداث وصول العمل إلى الطابور واكتمال I/O وطلب الإيقاف وتغيّر قيمة داخل العملية نفسها، وكيف يُفرَّق استخدام waitable timer وWaitOnAddressيشترطقد يسبّبقد يسبّبغير موصى به لـموصى به لـيستخدمموصى به لـموصى به لـغير موصى به لـموصى به لـغير موصى به لـموصى به لـموصى به لـقد يسبّبموصى به لـغير موصى به لـيُتحقّق بـقد يسبّبيستخدميستخدمغير موصى به لـpolling بمؤقّت (حلقة تحسّس)تصميم انتظار مدفوع بالأحداثsystem clock resolution (platform timer resolution)تأخير الجدولةانتظار وصول عمل إلى الطابوركائن حدث Windowsدوال الانتظار في Windows (Wait Functions)Overlapped I/Oانتظار اكتمال I/Oمنفذ اكتمال الإدخال/الإخراج (IOCP)انتظار طلب الإيقافWaitOnAddress APIانتظار تغيّر قيمة داخل العملية نفسهاسباق بيانات (data race)waitable timer (مؤقّت قابل للانتظار)انتظار شرطه الزمن نفسهtimeBeginPeriod (طلب دقّة المؤقّت)GetSystemTimeAdjustmentعامل تأخير مرتبط بمعالجة المقاطعة (ISR/DPC)

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

2. ما المشكلة

2.1 الانتظار المؤقّت مقيّد بدقّة ساعة النظام

دقّة timeout في دوال الانتظار على Windows تعتمد على system clock resolution. Sleep كذلك: الميلي ثانية المعيّنة لا تُضمن «بطولها كما هي».

المهم هنا: تعيين 1ms لا يعني الاستيقاظ بعد 1ms.

افحص دقّة بيئتك

«رتبة 15.6ms» تعميم، فالأسرع أن ترى القيمة عندك. هناك طريقتان.

الأولى استدعاء GetSystemTimeAdjustment. في الوسيط الثاني lpTimeIncrement يُرجع فاصل تحديث النظام لساعة الوقت من اليوم بوحدة 100 نانوثانية. إن كانت من رتبة 15.6ms صارت القيمة في حدود 150 ألفاً.

#include <windows.h>
#include <cstdio>

int main()
{
    DWORD adjustment = 0;
    DWORD increment = 0;
    BOOL adjustmentDisabled = FALSE;

    if (!GetSystemTimeAdjustment(&adjustment, &increment, &adjustmentDisabled))
    {
        std::printf("GetSystemTimeAdjustment failed. GetLastError=%lu\n", GetLastError());
        return 1;
    }

    // increment は 100ns 単位なので、ms に直して見る
    std::printf("time increment = %lu (100ns) = %.4f ms\n",
                increment,
                increment / 10000.0);
    return 0;
}

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

في الوثائق، lpTimeIncrement قيمة ثابتة يقرّرها النظام عند الإقلاع ولا تتغيّر أثناء التشغيل. أي أنها قيمة لمعرفة «دقّة البيئة الخام»، لا لقياس نتيجة استدعاء timeBeginPeriod. ذلك في 6.3.

طريقتان لفحص الدقّة الخاممخطّط يبيّن أن دقّة مؤقّت بيئتك تُفحص باستدعاء GetSystemTimeAdjustment لرؤية فاصل التحديث بوحدة 100 نانوثانية، أو بتشغيل ClockRes من Sysinternals الذي يستدعي الواجهة نفسها داخلياً.أريد دقّة الجهازاستدع GetSystemTimeAdjustmentشغّل ClockResيُرجع فاصل تحديث بوحدة 100 نانوثانيةيستدعي الواجهة نفسها داخلياً

الشكل 3: طريقتان، استدعاء في كود أو ClockRes، لفحص الدقّة الخام للبيئة.

2.2 حلول الأجل لا يعني التنفيذ فوراً

الأعقد أن الخيط لا يُنفَّذ فوراً لحظة مرور timeout.

كما في شرح Sleep، بعد انتهاء الانتظار يصير الخيط ready، لكن لا ضمان أن يأخذ CPU ويجري الآن. يتأثّر بخيوط أخرى والأولويّة وحالة خمول CPU وDPC / ISR وتنافس الأقفال.

أي أن انتظار المؤقّت القصير فيه درجتان على الأقل من عدم اليقين.

  1. حكم timeout نفسه ينجذب إلى دقّة المؤقّت
  2. بعد timeout أيضاً بدء التنفيذ بحسب scheduler
درجتا عدم اليقين في انتظار المؤقّت القصيرمخطّط يبيّن أن انتظار المؤقّت القصير يحمل درجتين من عدم اليقين: انجذاب حكم timeout إلى دقّة المؤقّت، ثم صيرورة الخيط ready فقط بعد timeout وبدء التنفيذ بحسب scheduler.بدء انتظار مؤقّت قصيرحكم timeout يعتمد على دقّة المؤقّتالخيط يصير ready فقطبدء التنفيذ بحسب schedulerأثر خيوط أخرى وDPC / ISR

الشكل 4: ينحرف زمن الانتظار المعيّن عند موضعين: حكم timeout وبدء التنفيذ.

2.3 Sleep(1) لا يعني دورة 1ms

رؤية Sleep(1) توحي بحلقة «تدور كل 1ms». لكن لا تُقرأ كذلك.

while (!g_stop)
{
    Step();
    Sleep(1);
}

حقيقة هذه الحلقة كالتالي.

  • زمن تنفيذ Step() يُضاف كل مرّة
  • زمن انتظار Sleep(1) نفسه ينجذب إلى الدقّة
  • حتى بعد الاستيقاظ لا ضمان الجري فوراً
حقيقة حلقة Sleep(1)مخطّط يبيّن أن حلقة Sleep(1) لا تعني دورة 1 ميلي ثانية لأن زمن تنفيذ Step يُضاف كل مرّة، وزمن الانتظار ينجذب إلى الدقّة، ولا ضمان الجري فوراً بعد الاستيقاظ.يُضاف زمن تنفيذ Stepلا تصير دورة 1 ميلي ثانيةالانتظار ينجذب إلى الدقّةلا ضمان الجري فوراً بعد الاستيقاظ

الشكل 5: ثلاثة انحرافات تتراكب، فلا يصير Sleep(1) دورة كل 1 ميلي ثانية.

3. لماذا انتظار الحدث أوفر

3.1 شرط نهاية الانتظار يصير «إشارة» لا «انتهاء مهلة»

فضل انتظار الحدث أن معنى الانتظار يتغيّر.

انتظار المؤقّت كالتالي.

  • حتى إن لم يقع شيء بعد
  • يستيقظ عند حلول زمن ثابت
  • ثم يفحص «هل وقع شيء»

انتظار الحدث كالتالي.

  • الجانب الذي وقع يُشير
  • تُلبّى الانتظار عند الإشارة
  • عند الاستيقاظ السبب موجود أصلاً

في شكل، يظهر أن طريقة انتهاء الانتظار نفسها مختلفة.

انتظار حدث: يُوقَظ لأن شيئاً وقعالجانب الذي وقع يُشيرينتظرعند الاستيقاظ السبب ثابتيعالجانتظار مؤقّت: يستيقظ لأن الزمن حللانعميستيقظ بدقّة المؤقّتينتظرهل وقع شيء؟يعالج

الشكل 6: حلقة الدوران الفارغ في انتظار المؤقّت وحده، وفي انتظار الحدث السبب ثابت عند الاستيقاظ.

في جانب انتظار المؤقّت وحده حلقة تعود فارغة. هنا يؤثّر في زمن التأخير والطاقة معاً.

3.2 فرّق الأداة بحسب ما تريد انتظاره

أي أداة تختار فعلاً. الحكم الأوّل يكفي بهذا الجدول تقريباً.

ما تريد انتظاره مثال سيئ الاختيار الأوّل
دخول عمل إلى الطابور TryPop مع Sleep(1) event / semaphore
اكتمال I/O النظر إلى الحالة بمؤقّت حدث overlapped I/O / IOCP
وصول طلب إيقاف النظر إلى stop flag كل 100ms stop event / cancellation
تغيّر قيمة داخل العملية نفسها while (flag == 0) Sleep(1) WaitOnAddress
حلول زمن ليّ الحدث قسراً timer / waitable timer

3.3 الحدث ليس سحراً أيضاً

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

حتى انتظار الحدث يتأثّر بما يلي.

  • scheduler latency
  • أولويّة الخيط
  • حالة طاقة CPU
  • تنافس الأقفال
  • page fault
  • DPC / ISR

غير أنه على الأقل يزيل طريقة انتظار زائدة هي «النوم حتى tick المؤقّت التالي».

ما يزيله انتظار الحدث وما يبقى من أثرمخطّط يبيّن أن انتظار الحدث يبقى متأثّراً بـ scheduler latency وأولويّة الخيط وDPC / ISR ونحوها، لكنه يزيل انتظار النوم حتى tick المؤقّت التالي.انتظار بحدثأثر scheduler ونحوه يبقىانتظار النوم حتى tick المؤقّت يُزالالإشارة ليست تأخيراً صفراً فورياً

الشكل 7: الحدث ليس سحراً، لكن الحاجة للاستيقاظ بدقّة المؤقّت تزول يقيناً.

4. أنماط سيئة شائعة

4.1 polling للطابور بـ Sleep(1)

الأكثر مشاهدة هذا.

for (;;)
{
    if (g_stop)
    {
        break;
    }

    WorkItem item;
    if (TryPop(item))
    {
        Process(item);
        continue;
    }

    Sleep(1);
}

الكتابة تبدو بسيطة، لكن المشاكل ثلاث.

  1. يستيقظ دورياً حتى والطابور فارغ
  2. زمن التأخير ينجذب إلى دقّة المؤقّت
  3. خسارة في الطاقة أيضاً
المشاكل الثلاث في polling بـ Sleep(1)مخطّط يبيّن أن polling الطابور بـ Sleep(1) فيه ثلاث مشاكل: الاستيقاظ دورياً حتى والطابور فارغ، وانجذاب زمن التأخير إلى دقّة المؤقّت، والخسارة في الطاقة.تحسّس الطابور بـ Sleep(1)يستيقظ دورياً حتى وهو فارغزمن التأخير ينجذب إلى الدقّةخسارة في الطاقة أيضاً

الشكل 8: حلقة polling البسيطة ظاهراً فيها ثلاث خسائر: استيقاظ وتأخير وطاقة.

4.2 مراقبة حالة بـ Thread.Sleep(1) / Task.Delay(1)

الرائحة نفسها تظهر في C# / .NET.

while (!stoppingToken.IsCancellationRequested)
{
    if (_queue.TryDequeue(out WorkItem? item))
    {
        await ProcessAsync(item, stoppingToken);
        continue;
    }

    await Task.Delay(1, stoppingToken);
}

المظهر async هادئ، لكن جوهر التصميم polling.

5. هكذا يُصلَح

5.1 المنتج يُشير عند الوصول

لانتظار وصول الطابور، بدّل polling إلى شكل المنتج يُشير.

  • يضع المنتج عنصراً في الطابور
  • يستدعي SetEvent فور وضع العنصر
  • المستهلك ينتظر بـ WaitForSingleObject أو WaitForMultipleObjects
  • عند الاستيقاظ يفرّغ الطابور (drain)
شكل إشارة المنتجمخطّط يبيّن أن المنتج يستدعي SetEvent فور وضع عنصر في الطابور، وأن المستهلك ينتظر بـ WaitForSingleObject ونحوه، وعند الاستيقاظ يفرّغ الطابور.المستهلكالطابورالمنتجالمستهلكالطابورالمنتجيضع عنصراًSetEvent فوراًيستيقظ من الانتظاريفرّغ الطابور

الشكل 9: الجانب الذي يعرف الوصول (المنتج) يُعلم، فيزول دوران المستهلك الفارغ.

5.2 انتظر العمل والإيقاف معاً بـ WaitForMultipleObjects

لعامل بسيط هذا الشكل أوضح.

HANDLE waits[2] = { _stopEvent, _workEvent };  // index 0 = stop, index 1 = work

for (;;)
{
    // bWaitAll = FALSE なので、戻り値は「最初に signal された handle の index」
    DWORD rc = WaitForMultipleObjects(2, waits, FALSE, INFINITE);

    // 失敗は WAIT_FAILED ((DWORD)0xFFFFFFFF)。理由は GetLastError でしか分からない
    if (rc == WAIT_FAILED)
    {
        throw std::system_error(
            static_cast<int>(GetLastError()),
            std::system_category(),
            "WaitForMultipleObjects failed.");
    }

    if (rc == WAIT_OBJECT_0)  // stop
    {
        return;
    }

    if (rc == WAIT_OBJECT_0 + 1)  // work
    {
        DrainQueue();
        continue;
    }

    // ここに来るのは INFINITE 待ちでは想定外
    // (WAIT_TIMEOUT や WAIT_ABANDONED_0 系)。握りつぶさず落とす
    throw std::runtime_error("WaitForMultipleObjects returned an unexpected value.");
}

نقاط هذا المثال ثلاث.

  • اختفى Sleep(1)
  • المنتج يستدعي SetEvent عند وصول العنصر
  • العامل ينتظر stop و work معاً

معاملة القيمة الراجعة موضع يسهل إسقاطه في الميدان، فنكمّله.

  • عندما يكون bWaitAll هو FALSE، القيمة الراجعة عند النجاح في نطاق WAIT_OBJECT_0 إلى WAIT_OBJECT_0 + nCount - 1، والقيمة بعد طرح WAIT_OBJECT_0 هي فهرس المصفوفة. إن كتبت فرعين فقط == WAIT_OBJECT_0 و != WAIT_OBJECT_0 + 1 انكسر الأمر لحظة زيادة المقابض إلى ثلاثة
  • إن أُشير إلى عدّة معاً، يُرجع الأصغر فهرساً. وضع stop في الفهرس 0 في المثال أعلاه حتى لا يُفوَّت طلب الإيقاف
  • الفشل ليس استثناء بل قيمة راجعة WAIT_FAILED ((DWORD)0xFFFFFFFF). السبب لا يُعرف دون GetLastError. إن كتبت rc != القيمة المتوقّعة جملة «فشل»، اختفى سبب مثل إغلاق المقبض أو غياب حق SYNCHRONIZE
  • إن خلطت mutex في أهداف الانتظار فقد ترجع عائلة WAIT_ABANDONED_0. هذا المثال ينتظر أحداثاً فقط فيُعاملها غير متوقّعة

5.3 داخل العملية نفسها WaitOnAddress مرشّح أيضاً

إن أردت داخل العملية نفسها فقط «الانتظار حتى تتغيّر قيمة»، فـ WaitOnAddress قوي جدّاً. يسقط عبء إنشاء حدث وتهيئته ومزامنة القيمة بلا انحراف.

حسّ الفصل تقريباً كالتالي.

الجانب event / semaphore / waitable object WaitOnAddress
نطاق هدف الانتظار بين العمليات أيضاً. يمكن تسميته داخل العملية نفسها فقط
جانب الإيقاظ SetEvent / ReleaseSemaphore ونحوه WakeByAddressSingle / WakeByAddressAll
التحضير المسبق يحتاج إنشاء كائن نواة وإدارة مقابض يكفي المتغيّر المنتظَر
الإصدارات المتاحة متاح منذ زمن Windows 8 / Windows Server 2012 فما بعده
الربط Kernel32.lib Synchronization.lib

ثلاث نقاط لا تريد إسقاطها عند الاستخدام.

  1. استخدمه حتماً زوجاً مع WakeByAddressSingle أو WakeByAddressAll. إن لم يستدع الجانب الذي غيّر القيمة هذا، لا يستيقظ الخيط المنتظر. واحد فقط فـ Single، والكل فـ All
  2. قد يعود WaitOnAddress دون إشارة. الوثائق تنصّ على احتمال استيقاظ مبكر في حالة ذاكرة منخفضة ونحوها. عند العودة اقرأ القيمة حتماً مرّة أخرى في حلقة while وتتأكّد أنها تغيّرت فعلاً
  3. الحجم القابل للانتظار أحد 1 / 2 / 4 / 8 بايت
  4. اجعل العلم ذرّياً. WakeByAddressSingle يوقظ الخيط المنتظر فقط، ولا يجعل الكتابة السابقة ذرّية ولا مرئية. قراءة وكتابة متغيّر خام من الجانبين تنافس بيانات (سلوك غير معرّف) في C++، وقد يبقى التحديث غير مرئي في بناء محسَّن فيستمر التوقّف بعد الإيقاظ. جانب الكتابة release، وجانب القراءة acquire
// الجانب المنتظر وجانب الإيقاظ خيطان مختلفان، لذا اجعل العلم ذرّياً حتماً.
// قراءة ULONG خام وكتابته من الجانبين تنافس بيانات (سلوك غير معرّف) في C++،
// وفي بناء محسَّن قد تبقى القيمة في السجل فلا يظهر التحديث،
// فيستمر التوقّف حتى بعد الإيقاظ
std::atomic<ULONG> g_ready{ 0 };
static_assert(std::atomic<ULONG>::is_always_lock_free,
              "يُمرَّر إلى WaitOnAddress، لذا يلزم أن يكون lock-free");

// الشكل الأدنى لـ «انتظر حتى لا يعود g_ready صفراً»
ULONG undesired = 0;
ULONG captured = g_ready.load(std::memory_order_acquire);

while (captured == undesired)
{
    // قد يعود مبكّراً، لذا عند العودة أعد القراءة حتماً
    WaitOnAddress(&g_ready, &undesired, sizeof(ULONG), INFINITE);
    captured = g_ready.load(std::memory_order_acquire);
}

جانب التغيير يحدّث القيمة ثم يوقظ.

// اكتب بـ release. بهذا يرى جانب القراءة بـ acquire أيضاً البيانات
// التي أُعدَّت قبل هذا السطر (الـ payload أدناه). ما يضمن الترتيب هو هذا الـ store،
// لا WakeByAddressSingle ── فهو يوقظ الخيط المنتظر فقط،
// ولا يجعل الكتابة السابقة ذرّية ولا مرئية
g_payload = ...;                                  // بيانات تريد تمريرها معه
g_ready.store(1, std::memory_order_release);
WakeByAddressSingle(&g_ready);
مسار الزوج في WaitOnAddressمخطّط يبيّن أن جانب التغيير يجهّز البيانات ويكتب العلم بـ release ثم يوقظ بـ WakeByAddressSingle، وأن الجانب المنتظر ينتظر بحلقة while تعيد القراءة بـ acquire استعداداً للعودة المبكرة.الجانب المنتظرعلم ذرّيجانب التغييرالجانب المنتظرعلم ذرّيجانب التغييريقرأ بـ acquireWaitOnAddress حتى يتغيّريكتب بـ releaseWakeByAddressSingleبعد الاستيقاظ يعيد القراءة

الشكل 10: الإيقاظ بـ WakeByAddress، وضمان رؤية القيمة من جانب release وacquire.

6. مع ذلك، مشاهد تستخدم المؤقّت

6.1 عندما يكون الزمن نفسه هو الشرط

بالطبع هناك مشاهد تستخدم المؤقّت.

  • إرسال مقاييس كل 5 ثوانٍ
  • إعادة محاولة بعد 200ms
  • كنس ذاكرة مؤقّتة كل دقيقة
  • الانتظار حتى زمن أجل ثم timeout

هنا ما يُراد انتظاره زمن فعلاً.

6.2 استخدم waitable timer

إن انتظرت «الزمن نفسه» على Windows، فـ waitable timer أوضح معنى من تكديس Sleep فجّاً.

6.3 لا تجعل timeBeginPeriod استعمالاً دائماً أوّل

إن أقلقتك دقّة انتظار المؤقّت القصير، تميل إلى إضافة timeBeginPeriod(1). لكن لا تجعله الاختيار الأوّل للاستعمال الدائم.

الأسباب ثلاثة.

  1. تكلفة في الطاقة / الأداء
  2. السلوك على Windows الحديث أعقد قليلاً
  3. كثيراً ما لا يُصلح السبب الجذري
سبب عدم جعل timeBeginPeriod استعمالاً دائماً أوّلمخطّط يبيّن أنه حتى إن أقلقتك دقّة انتظار المؤقّت القصير لا تجعل timeBeginPeriod الاختيار الأوّل للاستعمال الدائم، لأن فيه تكلفة طاقة وأداء، والسلوك على Windows الحديث أعقد، وكثيراً ما لا يُصلح السبب الجذري.الدقّة مقلقةتميل إلى إضافة timeBeginPeriodلا تجعله اختياراً أوّل دائماًفيه تكلفةالسلوك أعقد قليلاًلا يُصلح السبب الجذري

الشكل 11: قبل رفع الدقّة تذكّر الأسباب الثلاثة لعدم جعله استعمالاً دائماً.

7. قائمة تحقّق عند المراجعة

  • هل صنعت حلقة تحسّس بـ Sleep(1) / Thread.Sleep(1) / Task.Delay(1)
  • هل تعمل polling بمؤقّت بينما تنتظر في الحقيقة وصول طابور أو اكتمال I/O أو طلب إيقاف
  • هل التصميم يتيح الإشارة من جانب المنتج / الاكتمال
  • هل يمكن جمع stop و work في انتظار واحد
  • هل يمكن كتابة تغيّر قيمة داخل العملية بـ WaitOnAddress
  • في مواضع استخدام المؤقّت، هل ما يُراد انتظاره فعلاً «زمن»

8. الخلاصة

تصميم «أتحسّس كل فترة» بانتظار مؤقّت قصير على Windows يتأثّر لا محالة بدقّة المؤقّت وscheduler. لذلك Sleep(1) والـ timeout القصير ليسا انتظاراً دقيقاً كما يبدو.

بالمقابل، إن كان ما تريد انتظاره فعلاً «واقعة» مثل وصول عمل أو اكتمال I/O أو طلب إيقاف أو تغيّر حالة، فانتظار الحدث أطبيعي.

الخلاصة سطر واحد.

إن انتظرت زمناً فمؤقّت، وإن انتظرت واقعة فحدث.

اتّضاح هذا الخط وحده ينفع كالتالي.

  • يصير زمن التأخير مقروءاً
  • يقلّ الاستيقاظ الدوري الضائع
  • يصير أفضل في الطاقة أيضاً
  • يصير قصد الكود أوضح
الزمن مؤقّت والواقعة حدثمخطّط يبيّن أن اتّضاح خط «الزمن مؤقّت والواقعة حدث» يجعل زمن التأخير مقروءاً، ويقلّل الاستيقاظ الدوري الضائع، ويحسّن الطاقة، ويوضّح قصد الكود.زمنواقعةما يُراد انتظاره أيّهماانتظر بمؤقّتانتظر بحدثيتّضح الخطيصير زمن التأخير مقروءاًيقلّ الاستيقاظ الضائع

الشكل 12: الالتزام بسطر الفصل وحده يغيّر التأخير والطاقة وسهولة قراءة قصد الكود.

روابط مرجعية

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

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

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

لماذا لا يستيقظ Sleep(1) في Windows بعد 1 ميلي ثانية بدقّة؟
دقّة timeout في الانتظار المؤقّت على Windows تعتمد على system clock resolution، وفي الإعداد العادي كثيراً ما تكون منصة timer resolution من رتبة 15.6 ميلي ثانية هي الافتراض. ثم حتى بعد انتهاء الانتظار يصير الخيط ready فقط، بلا ضمان أن يأخذ CPU ويجري فوراً. يتأثّر بخيوط أخرى والأولويّة وحالة خمول CPU وDPC/ISR وتنافس الأقفال. أي أن انتظار المؤقّت القصير فيه درجتان على الأقل من عدم اليقين: انجذاب حكم timeout إلى دقّة المؤقّت، وبدء التنفيذ بعد timeout بحسب scheduler.
ما مشكلة تصميم يتحسّس الطابور بـ Sleep(1) أو Task.Delay(1)؟
المشاكل ثلاث: الاستيقاظ دورياً حتى والطابور فارغ، وانجذاب زمن التأخير إلى دقّة المؤقّت، والخسارة في الطاقة. حلقة await Task.Delay(1) في C# تبدو هادئة لكن جوهر التصميم polling. الإصلاح أن يستدعي المنتج SetEvent فور وضع عنصر في الطابور، وأن ينتظر المستهلك بـ WaitForSingleObject أو WaitForMultipleObjects. جمع حدث الإيقاف وحدث العمل في انتظار واحد يردّ على طلب الإيقاف فوراً أيضاً.
كيف تفرّق بين انتظار مؤقّت وانتظار حدث؟
الخط: إن كنت تنتظر زمناً فمؤقّت، وإن كنت تنتظر واقعة فحدث. إرسال مقاييس كل 5 ثوانٍ شرطه الزمن نفسه، فهو عمل waitable timer. وصول عمل إلى الطابور يناسبه event أو semaphore، واكتمال I/O حدث overlapped I/O أو IOCP، وطلب الإيقاف حدث إيقاف أو cancellation، وتغيّر قيمة داخل العملية نفسها WaitOnAddress. إن اتّضح هذا الخط صار زمن التأخير مقروءاً، وقلّ الاستيقاظ الدوري الضائع، وصار قصد الكود أوضح.
ألا يُحل الأمر برفع دقّة المؤقّت بـ timeBeginPeriod(1)؟
لا تجعله الاختيار الأوّل للاستعمال الدائم. الأسباب ثلاثة: تكلفة في الطاقة والأداء، وتعقيد السلوك قليلاً على Windows الحديث، وكثيراً ما لا يُصلح السبب الجذري. إن كان ما تريد انتظاره فعلاً واقعة مثل وصول عمل أو اكتمال I/O، فتغيير التصميم إلى أحداث يُشير فيها الجانب الذي وقع أوضح لزمن التأخير ولـ CPU وللطاقة من رفع دقّة المؤقّت.

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

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

غو كومورا

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

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

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