ما الذي يعنيه «لا يستجيب» حقّاً ── كيف يقرِّر Windows أنّ تطبيقاً قد تجمّد، وكيف تصمِّم تطبيقات لا تتجمّد

· · Windows, تطوير Windows, استكشاف الأخطاء, تعدّد مؤشّرات الترابط, WinForms, WPF, Win32 API, تصميم الواجهة

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

كيف يعلم Windows أنّ تطبيقاً قد «تجمّد»؟ ما تلك النافذة البيضاء المتجمِّدة؟ موجَّه إلى المطوِّرين الذين يكتبون تطبيقات أعمال على Windows وإلى موظّفي تقنيّة المعلومات الذين يأخذون تذاكر عن تطبيقات تتجمّد، يعمل هذا المقال عبر حكم «لا يستجيب» من أساسيّات حلقة الرسائل، وينظِّم الأسباب الكلاسيكيّة للتعليق، والتصاميم التي لا تتجمّد، وإجراء التحقيق في لحظة التعليق ── كلّها مؤسَّسة على المصادر الأوّليّة.

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

  • «لا يستجيب» حكم نظام التشغيل. عندما تكون نافذة (ومؤشّر ترابط الواجهة الذي يملكها) لا تنتظر إدخالاً، وليست في تسلسل بدئها، ولم تسترجع رسالة (PeekMessage) لمدّة 5 ثوانٍ، يعاملها نظام التشغيل غير مستجيبة. الحكم ليس لكلّ عمليّة.1
  • النافذة المبيَّضة «نافذة شبح». أخفى نظام التشغيل النافذة الأصليّة واستبدل مزيّفاً بالموضع والحجم والمظهر نفسه. كلّ ما يمكنك فعله النقل أو التصغير أو الإغلاق؛ المحتويات لا تعمل. لا تُنشأ نافذة شبح بينما مصحِّح مرفق.2
  • سبب التعليق يؤول شبه دائماً إلى شيء واحد. مؤشّر ترابط الواجهة الذي ينبغي أن يضخّ حلقة الرسائل محجوب على عمل ثقيل أو انتظار. الإدخال/الإخراج المتزامن، واستدعاءات الشبكة، وانتظارات القفل، وSendMessage عبر مؤشّرات الترابط هي الكلاسيكيّات.3
  • مبدأ التصميم «لا تنتظر ولا تحسب على مؤشّر ترابط الواجهة». انقل العمل الثقيل إلى مؤشّر عامل (async/await + Task.Run في C#) واترك مؤشّر الواجهة مكرَّساً للرسم والتقدّم وقبول الإلغاء.4
  • DoEvents وضخّ حلقة الرسائل يدوياً مرتع لأخطاء إعادة الدخول. عرض «لا يستجيب» يختفي، لكنّ البنية الآن تتيح لأحداث اعتباطيّة المقاطعة في وسط العمل. الفصل، لا التهرّب، هو النهج السليم.
  • التحقيق يبدأ بالتقاط الحالة عند لحظة التعليق. خذ تفريغاً وانظر إلى مكدّس مؤشّر الواجهة، ويمكنك شبه دائماً تحديد ما ينتظره.

2. مقدّمة لازمة: تطبيقات Windows تُقاد بالرسائل

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

لكلّ مؤشّر ترابط يُنشئ نافذة طابور رسائل ويشغِّل حلقة رسائل كهذه.

MSG msg;
while (GetMessage(&msg, NULL, 0, 0) > 0) {
    TranslateMessage(&msg);
    DispatchMessage(&msg);   // the window procedure is called
}

GetMessage يسترجع رسالة من الطابور، وDispatchMessage يستدعي إجراء النافذة لتلك النافذة (دالّة معالجة الرسائل). معالجة نقرة الزرّ، وإعادة الرسم، ومعالجات أحداث WinForms أو WPF كلّها، عندما تغليها، تعمل داخل دورة واحدة من هذه الحلقة.5

البنية الأساسيّة لحلقة الرسائليضع نظام التشغيل إدخال الفأرة ولوحة المفاتيح وغيرهما في طابور رسائل مؤشّر الترابط؛ تسترجعه حلقة مؤشّر الواجهة بـ GetMessage، وتستدعي إجراء النافذة بـ DispatchMessage، وتعود إلى رأس الحلقة عندما تنتهي المعالجةنظام التشغيل(إدخال، إعادة رسم، مؤقِّتات)طابور رسائل مؤشّر الترابطاسترجع بـ GetMessageDispatchMessageعالج في إجراء النافذة

الشكل 1: قلب تطبيق الواجهة هو حلقة الرسائل؛ كلّ معالج حدث يعمل كدورة واحدة من هذه الحلقة.

لهذه البنية عاقبة مهمّة واحدة. إن فعلت عملاً يستغرق وقتاً داخل إجراء النافذة (معالج حدث)، لا تستطيع الحلقة استرجاع الرسالة التالية في تلك الأثناء. لا تستطيع التفاعل لا مع النقرات ولا مع طلبات إعادة الرسم ── هذا ما هو «التعليق» حقّاً.

يجدر أيضاً استيعاب أنّ الرسائل تُسلَّم بمسارين. PostMessage يضع الرسالة على الطابور ويعود فوراً، وتسترجع الحلقة الرسائل وتعالجها بالترتيب. SendMessage، من جهة أخرى، يستدعي إجراء النافذة مباشرةً ولا يعود إلى المستدعي حتّى تكتمل المعالجة.36 ذلك الفرق ينتقل مباشرةً إلى نقاش الجمود في الفصل 4.

مسارا تسليم الرسائليضع PostMessage الرسالة على الطابور ويعود فوراً؛ تسترجع حلقة الرسائل وتعالج بالترتيب. يستدعي SendMessage الإجراء مباشرةً ولا يعود إلى المستدعي حتّى تكتمل المعالجةPostMessageضَع على الطابور(يعود فوراً)الحلقة تسترجع وتعالج بالترتيبSendMessageاستدعِ الإجراء مباشرةًلا يعود حتّى تكتمل المعالجة

الشكل 2: رغم أنّ كليهما «يرسل رسالة»، فإنّ Post في الطابور وSend الذي ينتظر الاكتمال طبيعتان مختلفتان تماماً.

3. كيف يُحكَم على «لا يستجيب» ── قاعدة الثواني الخمس ونافذة الشبح

فكيف يعلم نظام التشغيل أنّ «هذا التطبيق قد تجمّد»؟ المعيار موثَّق رسميّاً. يعامل نظام التشغيل نافذة غير مستجيبة عندما تكون لا تنتظر إدخالاً، وليست في تسلسل بدئها، ولم تستدعِ PeekMessage (استرجاع رسالة) لمدّة 5 ثوانٍ.1 بمعنى آخر، يراقب نظام التشغيل ما إذا كانت «حلقة الرسائل تدور فعليّاً» كما تأخذ نبضاً، وإن لم يكن ثمّة نبض لمدّة 5 ثوانٍ يحكم أنّ النافذة غير مستجيبة (تنصّ الوثائق أنّ قيمة الثواني الخمس هذه قد تتغيّر في المستقبل). وحدة الحكم هي النافذة ومؤشّر ترابط الواجهة الذي يملكها؛ في تطبيق بعدّة مؤشّرات واجهة، تعليق مؤشّر واحد لا يعني أنّ نوافذ مؤشّر آخر ميّتة. المؤشّر الذي تنظر إليه في التفريغ هو مالك النافذة المعلَّقة.

ما يحدث لنافذة عليا حُكِم عليها موثَّق أيضاً. نظام التشغيل يخفي النافذة الأصليّة ويستبدلها بـ«نافذة شبح» لها ترتيب Z والموضع والحجم والمظهر نفسه. كلّ ما يستطيع المستخدم فعله بها النقل أو تغيير الحجم أو الإغلاق (قسراً). التطبيق في الداخل لا يستجيب فعليّاً، لذا لا تعمل عمليّات أخرى.2

حكم النافذة المعلَّقة والاستبدال بنافذة شبحعندما يُحجَب مؤشّر الواجهة على عمل ثقيل ويتوقّف استرجاع الرسائل لمدّة 5 ثوانٍ، يحكم نظام التشغيل أنّ النافذة غير مستجيبة، ويخفي الأصليّة، ويستبدل نافذة شبح بالمظهر نفسه، ويقدِّم للمستخدم النقل والتصغير والإغلاق فقطلانعممؤشّر الواجهة محجوب على عمل ثقيلاسترجاع الرسائل يتوقّفانقضت 5 ثوانٍ؟استبدل بنافذة شبحالعنوان يعرض(لا يستجيب)أبيض متجمِّد؛ النقل والإغلاق فقط

الشكل 3: نصّ «لا يستجيب» والشاشة البيضاء كلاهما ينتميان إلى نافذة الشبح التي استبدلها نظام التشغيل، لا إلى التطبيق المعلَّق.

سلسلة «(لا يستجيب)» التي تظهر على شريط العنوان، والمظهر الأبيض المتجمِّد تحت سمة Aero، كلاهما ينتميان إلى نافذة الشبح هذه. تتبع عاقبتان عمليّتان.

  • بحلول وقت عرض «لا يستجيب»، لم يكن مؤشّر الترابط الذي يملك تلك النافذة يعالج رسائل لمدّة 5 ثوانٍ على الأقلّ. ليس أنّ «العرض جاء مبكّراً جداً» ── مؤشّر الواجهة محجوب قطعاً.
  • لا تُنشأ نافذة شبح بينما مصحِّح مرفق.2 عندما يبدو كأنّ «لا يصير غير مستجيب تحت المصحِّح، لكنّه يفعل في الإصدار»، يمكن أن يكون التعليق نفسه هو نفسه والعرض وحده مختلفاً.

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

4. لماذا تتجمّد التطبيقات ── أنماط كلاسيكيّة تحجب مؤشّر الواجهة

السبب، بعد غليه، نقطة واحدة ── «مؤشّر الواجهة لا يعود إلى حلقة الرسائل» ── لكنّ الأشكال التي تقابلها عمليّاً تقع في كلاسيكيّات قليلة.

تصنيف الأسباب الكلاسيكيّة التي تحجب مؤشّر الواجهةالعائلات الكلاسيكيّة الأربع ── الإدخال/الإخراج المتزامن واستدعاءات الشبكة، وانتظارات القفل، وSendMessage عبر مؤشّرات الترابط، وتدخّل COM STA ── كلّها تؤول إلى النقطة نفسها أنّ مؤشّر الواجهة لا يستطيع العودة إلى حلقة الرسائلأيّ سبب كلاسيكيّ؟إدخال/إخراج أم قفل؟SendMessage أم COM؟إدخال/إخراج متزامن وشبكةانتظارات قفلSendMessageعبر مؤشّرات الترابطتدخّل COM STAالواجهة لا تستطيع العودةفحص لا يستجيب

الشكل 4: العَرَض المرئيّ واحد، لكنّ الجاني الذي يحجب المؤشّر يقع في أربع عائلات، والإجراء المضادّ يختلف لكلّ منها.

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

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

SendMessage عبر مؤشّرات الترابط. SendMessage لا يعود حتّى ينهي إجراء النافذة الوجهة المعالجة.6 عندما ترسله إلى نافذة على مؤشّر آخر، يُجعَل المرسِل ينتظر حتّى يكون ذلك المؤشّر في حالة يستطيع فيها معالجة الرسائل. إن كان مؤشّر الوجهة نفسه ينتظر شيئاً، لديك جمود رسائل ينتظر فيه كلّ جانب الآخر.3 الإرسال إلى HWND_BROADCAST خصوصاً سيجرّك حالما تكون نافذة واحدة غير مستجيبة. عندما لا تستطيع تحمّل الانتظار، انظر في SendMessageTimeout أو PostMessage، الذي لا ينتظر ردّاً.8

جمود يسبّبه SendMessage عبر مؤشّرات الترابطإذا أرسل مؤشّر عامل SendMessage إلى نافذة مؤشّر الواجهة بينما مؤشّر الواجهة محجوب ينتظر نتيجة العامل، ينتظر كلّ جانب انتهاء الآخر ويكون لديك جمودمؤشّر عاملمؤشّر الواجهةمؤشّر عاملمؤشّر الواجهةلا يستطيع معالجة الرسائل(محجوب)لا يستطيع العودة من SendMessageينتظر أحدهما الآخر ── جمودينتظر انتهاء العامل(محجوب)SendMessage(لا يعود حتّى تُعالَج)

الشكل 5: «مؤشّر الواجهة ينتظر العامل، والعامل ينتظر مؤشّر الواجهة عبر SendMessage» جمود كلاسيكيّ.

تدخّل شقّة COM. استدعاءات كائن STA تُسلَّم كرسائل نافذة، لذا عندما يكون مؤشّر الواجهة (STA) محجوباً، تُحجَب استدعاءات COM من مؤشّرات أخرى كضرر جانبيّ أيضاً. تلك البنية مشروحة في مقال COM STA/MTA.

تراكم «إنّه لحظة فحسب». حتّى استدعاء متزامن بـ50 ملي ثانية، يُستدعى 100 مرّة في حلقة، هو 5 ثوانٍ. عتبة «لا يستجيب» 5 ثوانٍ، لكنّ «البطء» المُدرَك يبدأ حول 100 ملي ثانية. قاعدة إبهام تصميميّة هي «يجوز حجب مؤشّر الواجهة لميلي ثوانٍ فقط».

5. تصاميم لا تتجمّد ── نقل العمل الثقيل خارج مؤشّر الواجهة

مبدأ التصميم شيء واحد: انقل العمل الذي يستغرق وقتاً خارج مؤشّر الواجهة. في C# (WinForms/WPF)، async/await أقصر نهج سليم.

private async void RunButton_Click(object sender, EventArgs e)
{
    runButton.Enabled = false;
    try
    {
        // CPU-heavy work, or APIs that are only synchronous, go to a worker via Task.Run
        var result = await Task.Run(() => HeavyCalculation(input));

        // For I/O, use APIs that are natively async (they do not consume a thread either)
        var data = await httpClient.GetStringAsync(url);

        // After await you are back on the UI thread, so you can touch controls directly
        resultLabel.Text = result + data.Length.ToString();
    }
    catch (Exception ex)
    {
        // An exception leaking from an async void handler will take the app down. Catch it here
        MessageBox.Show($"The operation failed: {ex.Message}");
    }
    finally
    {
        runButton.Enabled = true;
    }
}

ثمّة ثلاث نقاط. الأولى، بينما await ينتظر، يكون مؤشّر الواجهة عائداً في حلقة الرسائل، لذا لا تصير غير مستجيب. الثانية، الاستمرار بعد await يعود إلى مؤشّر الواجهة، لذا يمكنك لمس العناصر بشكل عاديّ بعدها (لمس عنصر مباشرةً من مؤشّر عامل محظور؛ إن احتجت، استخدم Control.Invoke / Dispatcher.InvokeAsync).4 الثالثة، عطِّل الزرّ أثناء سير العمل، وإلا اقتل إعادة الدخول بالتصميم.

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

تقسيم الأدوار في تطبيق لا يتجمّدمؤشّر الواجهة مسؤول فقط عن قبول الإدخال وعرض التقدّم وقبول الإلغاء؛ يشغِّل مؤشّر عامل العمل الثقيل ويعيد الاكتمال إلى مؤشّر الواجهة عبر PostMessage أو استمرار awaitسلِّم العملPostMessage / استمرار awaitمؤشّر الواجهة: إدخال، تقدّم، إلغاءمؤشّر عامل: عمل ثقيللا إدخال/إخراج متزامن ولا حساب طويل على الواجهة

الشكل 6: أبقِ مؤشّر الواجهة «مكتب الاستقبال»، وسلِّم العمل الثقيل دائماً إلى عامل، وخذ إشعار الاكتمال فقط.

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

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

الشكل 7: يمحو DoEvents «لا يستجيب» مقابل دعوة أحداث اعتباطيّة إلى وسط العمل.

للعمل طويل الأمد، ضمِّن عرض التقدّم والإلغاء في التصميم أيضاً. أرسل التقدّم إلى الواجهة بـIProgress<T> وتواصل المقاطعة بـCancellationToken، ويمكن للمستخدم أن يرى أنّ «العمل يجري» ولن يصل إلى إنهاء قسريّ (الذي غالباً سبب لفساد البيانات).

تدفّق التقدّم والإلغاء للعمل طويل الأمديرسل مؤشّر العامل التقدّم إلى مؤشّر الواجهة عبر IProgress؛ يصل فعل إلغاء على الواجهة إلى العامل عبر CancellationToken؛ يتوقّف العامل عند حدّ مناسب وينظِّفتقدّم عبر IProgressCancellationTokenعامل: عمل طويل الأمدالواجهة: تقدّم وزرّ إيقافتوقّف عند حدّ ونظِّف

الشكل 8: التقدّم «عامل → واجهة»؛ الإلغاء «واجهة → عامل». ضمِّن هذه القناة الرقيقة ثنائيّة الاتّجاه في التصميم من البداية.

6. التحقيق في لحظة التعليق

في تحقيق «يتجمّد أحياناً»، أثمن شيء هو حالة مؤشّر الترابط عند اللحظة الدقيقة لتعليقه. أعد التشغيل، والدليل ذهب.

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

انظر إلى مكدّس مؤشّر الواجهة. افتح التفريغ في WinDbg وانظر إلى مكدّس المؤشّر الذي يضخّ حلقة الرسائل (عادةً المؤشّر 0). الإدخال/الإخراج المتزامن يظهر كـReadFile أو واجهة شبكة، وانتظار القفل كاستدعاء عائلة WaitFor…، وSendMessage عبر مؤشّرات الترابط كانتظار داخل SendMessage ── كما هو. كيفيّة قراءته مشروحة في مقال WinDbg التمهيديّ.

انظر إليه حيّاً. بـProcess Explorer يمكنك فحص قائمة مؤشّرات الترابط والمكدّسات في الحال. عندما يكون بطيئاً بثبات، خذ تتبّع WPR وحلِّل انتظارات مؤشّر الواجهة عبر الزمن (WPR/WPA عمليّاً).

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

الشكل 9: نجم التحقيق «تفريغ لحظة التعليق»؛ مكدّس مؤشّر الواجهة نفسه تصنيف السبب.

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

7. الخلاصة

  • «لا يستجيب» آليّة يحكم فيها نظام التشغيل أنّ تطبيقاً لم يسترجع رسالة لمدّة 5 ثوانٍ ويستبدل نافذة شبح. الشيء الذي يعرض الشاشة هو نظام التشغيل، لا التطبيق.
  • سبب التعليق نقطة واحدة: «مؤشّر الواجهة لا يستطيع العودة إلى حلقة الرسائل». الإدخال/الإخراج المتزامن، والشبكة، وانتظارات القفل، وSendMessage عبر مؤشّرات الترابط هي الكلاسيكيّات.
  • الإجراء المضادّ نقل العمل الثقيل خارج مؤشّر الواجهة. في C#، async/await + Task.Run؛ في Win32، مؤشّر عامل + PostMessage. امنع إعادة الدخول أثناء التنفيذ بالتصميم، كتعطيل الزرّ.
  • التهرّب بـDoEvents مقابل أخطاء إعادة دخول. DisableProcessWindowsGhosting يزيل العرض فقط. لا هذا ولا ذاك إصلاح جذريّ.
  • للتحقيق، تفريغ «لحظة التعليق» أهمّ شيء. السبب شبه دائماً مكتوب على مكدّس مؤشّر الواجهة كما هو.

من منظور المستخدم «لا يستجيب» هو «إنّه معطوب»، لكن متى عرفت الآليّة يمكنك ترجمته إلى الجملة الدقيقة «مؤشّر الواجهة لم يَعُد لمدّة 5 ثوانٍ». بالعمل إلى الوراء من تلك الجملة الواحدة، تسقط مرشّحات السبب والإصلاح وإجراء التحقيق طبيعيّاً.

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

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

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

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

  1. Microsoft Learn, IsHungAppWindow function (winuser.h). حول معيار الحكم أنّ تطبيقاً يُعامَل غير مستجيب عندما يكون «لا ينتظر إدخالاً، وليس في تسلسل بدئه، ولم يستدعِ PeekMessage لمدّة المهلة الداخليّة البالغة 5 ثوانٍ»؛ وحول إمكان تغيّر معيار الثواني الخمس هذا؛ وحول إرجاع الدالّة TRUE دائماً لنافذة شبح.  2

  2. Microsoft Learn, GetMessage function (winuser.h). حول معاملة النظام نافذة عليا غير مستجيبة عندما تتوقّف عن الاستجابة للرسائل لعدّة ثوانٍ واستبدالها بنافذة شبح بترتيب Z والموضع والحجم والمظهر نفسه؛ وحول قدرة المستخدم فقط على النقل أو تغيير الحجم أو الإغلاق؛ وحول عدم إنشاء نافذة شبح بينما مصحِّح مرفق.  2 3

  3. Microsoft Learn, About Messages and Message Queues. حول كون تطبيقات Windows مدفوعة بالأحداث ومعالجة إجراء النافذة للرسائل؛ وحول التمييز بين الرسائل في الطابور والرسائل المُرسَلة مباشرةً؛ وحول استبدال نافذة غير مستجيبة بنافذة شبح؛ وحول القسم الذي يغطّي الجمود من إرسال مؤشّرات الترابط رسائل بعضها لبعض.  2 3 4

  4. Microsoft Learn, How to make thread-safe calls to controls (Windows Forms). حول عدم أمان لمس عناصر تحكّم WinForms من أيّ مؤشّر غير الذي أنشأها؛ وحول استخدام Invoke/BeginInvoke للتحديثات من مؤشّر آخر؛ وحول أنماط غير متزامنة آمنة باستخدام async/await أو BackgroundWorker.  2

  5. Microsoft Learn, Using Messages and Message Queues. حول تنفيذ نموذجيّ لحلقة رسائل بـGetMessage وTranslateMessage وDispatchMessage، وحول كيفيّة فحص طابور رسائل. 

  6. Microsoft Learn, SendMessage function (winuser.h). حول استدعاء SendMessage إجراء النافذة المحدَّدة وعدم العودة حتّى تكتمل المعالجة؛ وحول جعل إرسال إلى نافذة على مؤشّر آخر المرسِل ينتظر حتّى يعالج ذلك المؤشّر الرسالة؛ وحول الفرق عن PostMessage، الذي يضع الرسالة على الطابور دون انتظار ردّ.  2 3

  7. Microsoft Learn, DisableProcessWindowsGhosting function (winuser.h). حول إمكان تعطيل، لعمليّة الواجهة المستدعية، ميزة نافذة الشبح التي تجعل نافذة غير مستجيبة قابلة للتصغير والنقل والإغلاق؛ وحول استمرار التعطيل لعمر العمليّة. 

  8. Microsoft Learn, SendMessageTimeout function (winuser.h). حول إمكان إرسال رسالة بمهلة؛ وحول علم (SMTO_ABORTIFHUNG) يعود دون انتظار عندما تكون النافذة غير مستجيبة (حُكِم بتعليقها). 

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

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

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

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

تحت أيّ شروط يظهر «لا يستجيب»؟
يحكم نظام التشغيل أنّ نافذة معلَّقة عندما يكون تطبيق ذو نافذة لا ينتظر إدخالاً، وليس في تسلسل بدئه، ولم يسترجع رسالة (PeekMessage) لمدّة 5 ثوانٍ. تُخفى النافذة العليا المعلَّقة وتُستبدَل بـ«نافذة شبح» بالموضع والحجم والمظهر نفسه. نصّ شريط العنوان «(لا يستجيب)» والمظهر الأبيض المتجمِّد ينتميان إلى نافذة الشبح هذه، التي لا تتيح لك سوى النقل أو التصغير أو الإغلاق. بمعنى آخر، «لا يستجيب» ليس شيئاً يعرضه التطبيق نفسه ── إنّه شاشة يضعها نظام التشغيل نيابةً عن التطبيق.
هل ثمّة إعداد يمنع ظهور «لا يستجيب» أثناء سير العمل؟
استدعاء DisableProcessWindowsGhosting يعطِّل الاستبدال بنافذة شبح لتلك العمليّة. غير أنّ ذلك يجعل التعليق أقلّ ظهوراً للمستخدم فحسب ── النافذة ما زالت لا تتفاعل مع الإدخال، ومن منظور المستخدم تجمّد تامّ بلا سبيل للنقل أو الإغلاق. الإصلاح الحقيقيّ ليس كبح العرض، بل نقل العمل الثقيل إلى مؤشّر ترابط عامل حتّى لا يُحجَب مؤشّر ترابط الواجهة ولو لجزء من الثانية، فضلاً عن خمس ثوانٍ. لاحظ أيضاً أنّ نظام التشغيل لا يُنشئ نافذة شبح بينما مصحِّح مرفق، لذا قد يبدو كأنّ «لا يستجيب» لا يحدث أبداً أثناء التصحيح.
هل يُقبَل تجنّب «لا يستجيب» بـ DoEvents (ضخّ حلقة الرسائل يدوياً)؟
غير مستحسَن. تدوير DoEvents أو حلقة PeekMessage في وسط عمل ثقيل سيتفادى حكم النافذة المعلَّقة، لكن أيّ معالج حدث يمكنه حينها إعادة الدخول ── نقرة ثانية على الزرّ، وإغلاق النافذة، ومؤقِّت، وما شابه. معالج آخر يعيد كتابة بيانات ما زالت تُعالَج، أو يلمس نموذجاً كان يُفترَض إغلاقه ويرمي، يُنتج أخطاء إعادة دخول تعتمد على التوقيت ويصعب إعادة إنتاجها ── أسوأ من «لا يستجيب» نفسه. النهج السليم نقل العمل نفسه إلى مؤشّر ترابط عامل بـ Task.Run أو ما شابه، وترك مؤشّر ترابط الواجهة مسؤولاً فقط عن عرض التقدّم وقبول الإلغاء.
كيف أحدِّث الواجهة (عناصر التحكّم) من مؤشّر ترابط عامل؟
يجوز لمس عناصر تحكّم WinForms وعناصر WPF فقط من مؤشّر الترابط الذي أنشأها (عادةً مؤشّر ترابط الواجهة). لمسها مباشرةً من مؤشّر عامل يسبّب استثناءات أو سلوكاً غير معرَّف. في C#، async/await أقصر طريق: الاستمرار بعد await يعود إلى مؤشّر ترابط الواجهة المستدعي، لذا يمكنك تحديث العناصر بشكل عاديّ بعد الـawait. للتبديل صراحةً، استخدم Control.Invoke/BeginInvoke في WinForms وDispatcher.InvokeAsync في WPF. في Win32 الأصليّ، النمط المستقرّ أن يُرسل مؤشّر العامل PostMessage برسالة اكتمال مخصَّصة إلى مؤشّر الواجهة، وأن يحدِّث إجراء النافذة الواجهة.
كيف أحقّق في سبب عرض تطبيق «لا يستجيب»؟
المهمّ التقاط الحالة عند «اللحظة نفسها» للتعليق. خذ أوّلاً تفريغاً كاملاً من علامة تبويب التفاصيل في مدير المهام بـ«إنشاء ملفّ تفريغ»، ثمّ في WinDbg انظر إلى مكدّس مؤشّر ترابط الواجهة (المؤشّر الذي يشغِّل حلقة الرسائل). ما إذا كان عالقاً في إدخال/إخراج متزامن، أو انتظار شبكة، أو انتظار قفل، أو انتظار مؤشّر آخر عبر SendMessage يظهر على المكدّس كما هو. للنظر إلى عمليّة حيّة، قائمة مؤشّرات الترابط وعرض المكدّس في Process Explorer مفيدان؛ لمتابعته عبر الزمن، التقاط تتبّع WPR فعّال. انظر أيضاً مقال WinDbg التمهيديّ في هذا الموقع، وProcess Explorer عمليّاً، وWPR/WPA عمليّاً.

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

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

غو كومورا

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

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

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