لماذا يُفضَّل انتظار الأحداث على 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 وللطاقة.
flowchart TB
accTitle: طريقة الانتظار المدفوع بالأحداث
accDescr: مخطّط يبيّن أنه إن كان ما تريد انتظاره واقعة لا زمناً، فالأوضح لزمن التأخير ولـ CPU وللطاقة ألا تنظر على فترات ثابتة، بل أن يُشير الجانب الذي وقع وينتظر الجانب الآخر الحدث.
d1["ما يُراد انتظاره واقعة"] --> d2["الجانب الذي وقع يُشير"]
d2 --> d3["الجانب المنتظر ينتظر الحدث"]
d3 -.-> d4["أوضح لزمن التأخير ولـ 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
flowchart TB
accTitle: خط الفصل بين المؤقّت والحدث
accDescr: مخطّط يبيّن أن المؤقّت يُستخدم فقط عندما يكون الزمن نفسه هو الشرط، وأن واقعة مثل وصول عمل أو اكتمال I/O أو طلب إيقاف تُنتظر بحدث.
q1{"ما يُراد انتظاره زمن أم واقعة"}
q1 -->|"الزمن نفسه"| t1["عمل مؤقّت"]
q1 -->|"واقعة"| e1["عمل انتظار حدث"]
e1 -.-> rei["وصول عمل / اكتمال 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 ليس حلاً جذرياً.
flowchart LR
accTitle: خريطة معرفة polling بالمؤقّت مقابل الانتظار المدفوع بالأحداث
accDescr: مخطّط يبيّن أن انتظار المؤقّت القصير يحمل عدم يقين مزدوجاً من system clock resolution وتأخير الجدولة، وكيف يعالج الانتظار المدفوع بالأحداث وصول العمل إلى الطابور واكتمال I/O وطلب الإيقاف وتغيّر قيمة داخل العملية نفسها، وكيف يُفرَّق استخدام waitable timer وWaitOnAddress
timer_polling["polling بمؤقّت (حلقة تحسّس)"]
event_driven_wait["تصميم انتظار مدفوع بالأحداث"]
system_clock_resolution["system clock resolution (platform timer resolution)"]
scheduler_latency["تأخير الجدولة"]
queue_arrival_wait["انتظار وصول عمل إلى الطابور"]
windows_event_object["كائن حدث Windows"]
wait_functions["دوال الانتظار في Windows (Wait Functions)"]
overlapped_io["Overlapped I/O"]
io_completion_wait["انتظار اكتمال I/O"]
iocp["منفذ اكتمال الإدخال/الإخراج (IOCP)"]
stop_request_wait["انتظار طلب الإيقاف"]
waitonaddress["WaitOnAddress API"]
same_process_value_change_wait["انتظار تغيّر قيمة داخل العملية نفسها"]
data_race["سباق بيانات (data race)"]
waitable_timer["waitable timer (مؤقّت قابل للانتظار)"]
time_based_wait["انتظار شرطه الزمن نفسه"]
timebeginperiod["timeBeginPeriod (طلب دقّة المؤقّت)"]
getsystemtimeadjustment["GetSystemTimeAdjustment"]
interrupt_processing_delay["عامل تأخير مرتبط بمعالجة المقاطعة (ISR/DPC)"]
timer_polling -->|"يشترط"| system_clock_resolution
timer_polling -.->|"قد يسبّب"| scheduler_latency
event_driven_wait -.->|"قد يسبّب"| scheduler_latency
timer_polling -->|"غير موصى به لـ"| queue_arrival_wait
windows_event_object -->|"موصى به لـ"| queue_arrival_wait
wait_functions -->|"يستخدم"| windows_event_object
overlapped_io -->|"موصى به لـ"| io_completion_wait
iocp -->|"موصى به لـ"| io_completion_wait
timer_polling -->|"غير موصى به لـ"| io_completion_wait
windows_event_object -->|"موصى به لـ"| stop_request_wait
timer_polling -->|"غير موصى به لـ"| stop_request_wait
wait_functions -->|"موصى به لـ"| stop_request_wait
waitonaddress -->|"موصى به لـ"| same_process_value_change_wait
waitonaddress -.->|"قد يسبّب"| data_race
waitable_timer -->|"موصى به لـ"| time_based_wait
timebeginperiod -->|"غير موصى به لـ"| queue_arrival_wait
system_clock_resolution -->|"يُتحقّق بـ"| getsystemtimeadjustment
interrupt_processing_delay -.->|"قد يسبّب"| scheduler_latency
iocp -->|"يستخدم"| overlapped_io
wait_functions -->|"يستخدم"| waitable_timer
timer_polling -->|"غير موصى به لـ"| same_process_value_change_wait
في المخطّط، يشير الخطّ المتّصل إلى علاقة قائمة دائماً، ويشير الخطّ المتقطّع إلى علاقة مشروطة (شروط قيامها مذكورة في شرح كلّ علاقة في الصفحة التفصيليّة). القائمة الكاملة للعلاقات (المجموع 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.
flowchart TB
accTitle: طريقتان لفحص الدقّة الخام
accDescr: مخطّط يبيّن أن دقّة مؤقّت بيئتك تُفحص باستدعاء GetSystemTimeAdjustment لرؤية فاصل التحديث بوحدة 100 نانوثانية، أو بتشغيل ClockRes من Sysinternals الذي يستدعي الواجهة نفسها داخلياً.
g1["أريد دقّة الجهاز"] --> g2["استدع GetSystemTimeAdjustment"]
g1 --> g3["شغّل ClockRes"]
g2 --> g4["يُرجع فاصل تحديث بوحدة 100 نانوثانية"]
g3 -.-> g5["يستدعي الواجهة نفسها داخلياً"]
الشكل 3: طريقتان، استدعاء في كود أو ClockRes، لفحص الدقّة الخام للبيئة.
2.2 حلول الأجل لا يعني التنفيذ فوراً
الأعقد أن الخيط لا يُنفَّذ فوراً لحظة مرور timeout.
كما في شرح Sleep، بعد انتهاء الانتظار يصير الخيط ready، لكن لا ضمان أن يأخذ CPU ويجري الآن.
يتأثّر بخيوط أخرى والأولويّة وحالة خمول CPU وDPC / ISR وتنافس الأقفال.
أي أن انتظار المؤقّت القصير فيه درجتان على الأقل من عدم اليقين.
- حكم timeout نفسه ينجذب إلى دقّة المؤقّت
- بعد timeout أيضاً بدء التنفيذ بحسب scheduler
flowchart TB
accTitle: درجتا عدم اليقين في انتظار المؤقّت القصير
accDescr: مخطّط يبيّن أن انتظار المؤقّت القصير يحمل درجتين من عدم اليقين: انجذاب حكم timeout إلى دقّة المؤقّت، ثم صيرورة الخيط ready فقط بعد timeout وبدء التنفيذ بحسب scheduler.
s1["بدء انتظار مؤقّت قصير"] --> s2["حكم timeout يعتمد على دقّة المؤقّت"]
s2 --> s3["الخيط يصير ready فقط"]
s3 --> s4["بدء التنفيذ بحسب scheduler"]
s3 -.-> s5["أثر خيوط أخرى وDPC / ISR"]
الشكل 4: ينحرف زمن الانتظار المعيّن عند موضعين: حكم timeout وبدء التنفيذ.
2.3 Sleep(1) لا يعني دورة 1ms
رؤية Sleep(1) توحي بحلقة «تدور كل 1ms».
لكن لا تُقرأ كذلك.
while (!g_stop)
{
Step();
Sleep(1);
}
حقيقة هذه الحلقة كالتالي.
- زمن تنفيذ
Step()يُضاف كل مرّة - زمن انتظار
Sleep(1)نفسه ينجذب إلى الدقّة - حتى بعد الاستيقاظ لا ضمان الجري فوراً
flowchart TB
accTitle: حقيقة حلقة Sleep(1)
accDescr: مخطّط يبيّن أن حلقة Sleep(1) لا تعني دورة 1 ميلي ثانية لأن زمن تنفيذ Step يُضاف كل مرّة، وزمن الانتظار ينجذب إلى الدقّة، ولا ضمان الجري فوراً بعد الاستيقاظ.
p1["يُضاف زمن تنفيذ Step"] --> p4["لا تصير دورة 1 ميلي ثانية"]
p2["الانتظار ينجذب إلى الدقّة"] --> p4
p3["لا ضمان الجري فوراً بعد الاستيقاظ"] --> p4
الشكل 5: ثلاثة انحرافات تتراكب، فلا يصير Sleep(1) دورة كل 1 ميلي ثانية.
3. لماذا انتظار الحدث أوفر
3.1 شرط نهاية الانتظار يصير «إشارة» لا «انتهاء مهلة»
فضل انتظار الحدث أن معنى الانتظار يتغيّر.
انتظار المؤقّت كالتالي.
- حتى إن لم يقع شيء بعد
- يستيقظ عند حلول زمن ثابت
- ثم يفحص «هل وقع شيء»
انتظار الحدث كالتالي.
- الجانب الذي وقع يُشير
- تُلبّى الانتظار عند الإشارة
- عند الاستيقاظ السبب موجود أصلاً
في شكل، يظهر أن طريقة انتهاء الانتظار نفسها مختلفة.
flowchart TB
subgraph TimerWait["انتظار مؤقّت: يستيقظ لأن الزمن حل"]
T1["ينتظر"] --> T2["يستيقظ بدقّة المؤقّت"]
T2 --> T3{"هل وقع شيء؟"}
T3 -- "لا" --> T1
T3 -- "نعم" --> T4["يعالج"]
end
subgraph EventWait["انتظار حدث: يُوقَظ لأن شيئاً وقع"]
E1["ينتظر"] --> E2["الجانب الذي وقع يُشير"]
E2 --> E3["عند الاستيقاظ السبب ثابت"]
E3 --> E4["يعالج"]
end
الشكل 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 المؤقّت التالي».
flowchart TB
accTitle: ما يزيله انتظار الحدث وما يبقى من أثر
accDescr: مخطّط يبيّن أن انتظار الحدث يبقى متأثّراً بـ scheduler latency وأولويّة الخيط وDPC / ISR ونحوها، لكنه يزيل انتظار النوم حتى tick المؤقّت التالي.
e1["انتظار بحدث"] --> e2["أثر scheduler ونحوه يبقى"]
e1 --> e3["انتظار النوم حتى tick المؤقّت يُزال"]
e2 -.-> e4["الإشارة ليست تأخيراً صفراً فورياً"]
الشكل 7: الحدث ليس سحراً، لكن الحاجة للاستيقاظ بدقّة المؤقّت تزول يقيناً.
4. أنماط سيئة شائعة
4.1 polling للطابور بـ Sleep(1)
الأكثر مشاهدة هذا.
for (;;)
{
if (g_stop)
{
break;
}
WorkItem item;
if (TryPop(item))
{
Process(item);
continue;
}
Sleep(1);
}
الكتابة تبدو بسيطة، لكن المشاكل ثلاث.
- يستيقظ دورياً حتى والطابور فارغ
- زمن التأخير ينجذب إلى دقّة المؤقّت
- خسارة في الطاقة أيضاً
flowchart TB
accTitle: المشاكل الثلاث في polling بـ Sleep(1)
accDescr: مخطّط يبيّن أن polling الطابور بـ Sleep(1) فيه ثلاث مشاكل: الاستيقاظ دورياً حتى والطابور فارغ، وانجذاب زمن التأخير إلى دقّة المؤقّت، والخسارة في الطاقة.
a1["تحسّس الطابور بـ Sleep(1)"] --> b1["يستيقظ دورياً حتى وهو فارغ"]
a1 --> b2["زمن التأخير ينجذب إلى الدقّة"]
a1 --> b3["خسارة في الطاقة أيضاً"]
الشكل 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)
sequenceDiagram
accTitle: شكل إشارة المنتج
accDescr: مخطّط يبيّن أن المنتج يستدعي SetEvent فور وضع عنصر في الطابور، وأن المستهلك ينتظر بـ WaitForSingleObject ونحوه، وعند الاستيقاظ يفرّغ الطابور.
participant P as المنتج
participant Q as الطابور
participant C as المستهلك
P->>Q: يضع عنصراً
P->>C: SetEvent فوراً
C->>C: يستيقظ من الانتظار
C->>Q: يفرّغ الطابور
الشكل 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 |
ثلاث نقاط لا تريد إسقاطها عند الاستخدام.
- استخدمه حتماً زوجاً مع
WakeByAddressSingleأوWakeByAddressAll. إن لم يستدع الجانب الذي غيّر القيمة هذا، لا يستيقظ الخيط المنتظر. واحد فقط فـ Single، والكل فـ All - قد يعود
WaitOnAddressدون إشارة. الوثائق تنصّ على احتمال استيقاظ مبكر في حالة ذاكرة منخفضة ونحوها. عند العودة اقرأ القيمة حتماً مرّة أخرى في حلقة while وتتأكّد أنها تغيّرت فعلاً - الحجم القابل للانتظار أحد 1 / 2 / 4 / 8 بايت
- اجعل العلم ذرّياً.
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);
sequenceDiagram
accTitle: مسار الزوج في WaitOnAddress
accDescr: مخطّط يبيّن أن جانب التغيير يجهّز البيانات ويكتب العلم بـ release ثم يوقظ بـ WakeByAddressSingle، وأن الجانب المنتظر ينتظر بحلقة while تعيد القراءة بـ acquire استعداداً للعودة المبكرة.
participant W as جانب التغيير
participant F as علم ذرّي
participant S as الجانب المنتظر
S->>F: يقرأ بـ acquire
S->>S: WaitOnAddress حتى يتغيّر
W->>F: يكتب بـ release
W->>S: WakeByAddressSingle
S->>F: بعد الاستيقاظ يعيد القراءة
الشكل 10: الإيقاظ بـ WakeByAddress، وضمان رؤية القيمة من جانب release وacquire.
6. مع ذلك، مشاهد تستخدم المؤقّت
6.1 عندما يكون الزمن نفسه هو الشرط
بالطبع هناك مشاهد تستخدم المؤقّت.
- إرسال مقاييس كل 5 ثوانٍ
- إعادة محاولة بعد 200ms
- كنس ذاكرة مؤقّتة كل دقيقة
- الانتظار حتى زمن أجل ثم timeout
هنا ما يُراد انتظاره زمن فعلاً.
6.2 استخدم waitable timer
إن انتظرت «الزمن نفسه» على Windows، فـ waitable timer أوضح معنى من تكديس Sleep فجّاً.
6.3 لا تجعل timeBeginPeriod استعمالاً دائماً أوّل
إن أقلقتك دقّة انتظار المؤقّت القصير، تميل إلى إضافة timeBeginPeriod(1).
لكن لا تجعله الاختيار الأوّل للاستعمال الدائم.
الأسباب ثلاثة.
- تكلفة في الطاقة / الأداء
- السلوك على Windows الحديث أعقد قليلاً
- كثيراً ما لا يُصلح السبب الجذري
flowchart TB
accTitle: سبب عدم جعل timeBeginPeriod استعمالاً دائماً أوّل
accDescr: مخطّط يبيّن أنه حتى إن أقلقتك دقّة انتظار المؤقّت القصير لا تجعل timeBeginPeriod الاختيار الأوّل للاستعمال الدائم، لأن فيه تكلفة طاقة وأداء، والسلوك على Windows الحديث أعقد، وكثيراً ما لا يُصلح السبب الجذري.
t1["الدقّة مقلقة"] --> t2["تميل إلى إضافة timeBeginPeriod"]
t2 --> t3["لا تجعله اختياراً أوّل دائماً"]
t3 -.-> r1["فيه تكلفة"]
t3 -.-> r2["السلوك أعقد قليلاً"]
t3 -.-> r3["لا يُصلح السبب الجذري"]
الشكل 11: قبل رفع الدقّة تذكّر الأسباب الثلاثة لعدم جعله استعمالاً دائماً.
7. قائمة تحقّق عند المراجعة
- هل صنعت حلقة تحسّس بـ
Sleep(1)/Thread.Sleep(1)/Task.Delay(1) - هل تعمل polling بمؤقّت بينما تنتظر في الحقيقة وصول طابور أو اكتمال I/O أو طلب إيقاف
- هل التصميم يتيح الإشارة من جانب المنتج / الاكتمال
- هل يمكن جمع
stopوworkفي انتظار واحد - هل يمكن كتابة تغيّر قيمة داخل العملية بـ
WaitOnAddress - في مواضع استخدام المؤقّت، هل ما يُراد انتظاره فعلاً «زمن»
8. الخلاصة
تصميم «أتحسّس كل فترة» بانتظار مؤقّت قصير على Windows يتأثّر لا محالة بدقّة المؤقّت وscheduler.
لذلك Sleep(1) والـ timeout القصير ليسا انتظاراً دقيقاً كما يبدو.
بالمقابل، إن كان ما تريد انتظاره فعلاً «واقعة» مثل وصول عمل أو اكتمال I/O أو طلب إيقاف أو تغيّر حالة، فانتظار الحدث أطبيعي.
الخلاصة سطر واحد.
إن انتظرت زمناً فمؤقّت، وإن انتظرت واقعة فحدث.
اتّضاح هذا الخط وحده ينفع كالتالي.
- يصير زمن التأخير مقروءاً
- يقلّ الاستيقاظ الدوري الضائع
- يصير أفضل في الطاقة أيضاً
- يصير قصد الكود أوضح
flowchart TB
accTitle: الزمن مؤقّت والواقعة حدث
accDescr: مخطّط يبيّن أن اتّضاح خط «الزمن مؤقّت والواقعة حدث» يجعل زمن التأخير مقروءاً، ويقلّل الاستيقاظ الدوري الضائع، ويحسّن الطاقة، ويوضّح قصد الكود.
m1{"ما يُراد انتظاره أيّهما"}
m1 -->|"زمن"| m2["انتظر بمؤقّت"]
m1 -->|"واقعة"| m3["انتظر بحدث"]
m2 --> m4["يتّضح الخط"]
m3 --> m4
m4 -.-> m5["يصير زمن التأخير مقروءاً"]
m4 -.-> m6["يقلّ الاستيقاظ الضائع"]
الشكل 12: الالتزام بسطر الفصل وحده يغيّر التأخير والطاقة وسهولة قراءة قصد الكود.
روابط مرجعية
- Sleep function (Win32)
- Wait Functions
- WaitForSingleObject function
- WaitForMultipleObjects function
- Event Objects (Synchronization)
- Using Event Objects
- WaitOnAddress function
- WakeByAddressSingle function
- WakeByAddressAll function
- GetSystemTimeAdjustment function
- ClockRes - Sysinternals
- timeBeginPeriod function
- CreateWaitableTimerExW function
- SetWaitableTimer function
- Thread.Sleep Method (.NET)
- Results for the Idle Energy Efficiency Assessment
مقالات ذات صلة
أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.
جدول قرار: إنهاء أم استمرار بعد استثناء غير متوقّع
عند وقوع استثناء غير متوقّع، هل تُنهي التطبيق أم تستمر؟ المقالة ترتّب الحكم من تلف الحالة والآثار الجانبيّة الخارجيّة والخيوط والحدود الأ...
لماذا نستخدم Generic Host و BackgroundService في تطبيق سطح المكتب
تنظيم البدء والمعالجة الدوريّة والإيقاف والسجلات والإعدادات و DI في أدوات Windows والتطبيقات المقيمة باستخدام Generic Host و BackgroundSe...
واجهة مجمع مؤشّرات الترابط Win32 ── تزامن دون إنشاء مؤشّرات ترابط، عبر CreateThreadpoolWork
هل تنثر استدعاءات CreateThread في شيفرتك الأصليّة؟ يشرح هذا المقال واجهة مجمع مؤشّرات الترابط Win32 التي أُعيد تصميمها في Vista ── كائنات...
الأنابيب المسمّاة عمليّاً ── IPC القياسيّ في Windows من التصميم إلى الأمان
دليل عمليّ للأنابيب المسمّاة، وسيلة الاتّصال القياسيّة بين العمليّات في Windows. ينظّم المقال، من المصادر الأوّليّة، الاختيار بين وضع الب...
تطبيقات تتعطّل عند الاستئناف من السكون ── كيف تعمل أحداث الطاقة في Windows وكيف تبني تطبيقات أعمال تصمد أمامها
فتحت الحاسوب المحمول فوجدت اتّصالات تطبيق الأعمال ميّتة ── السبب تصميم لم يحسب حساب السكون. يغطّي المقال تدفّق إشعار WM_POWERBROADCAST، و...
الأسئلة الشائعة
أسئلة شائعة حول موضوع هذه المقالة.
- لماذا لا يستيقظ 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 وللطاقة من رفع دقّة المؤقّت.
الملف الشخصي للمؤلف
صفحة الملف الشخصي لمؤلف المقالة.
غو كومورا
مؤسّس شركة كومورا سوفت ذ.م.م.
يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.