מה Not Responding באמת אומר — איך Windows מחליט שיש hang, ואיך לתכנן אפליקציות שלא נתקעות

· עודכן בתאריך: · · Windows, פיתוח Windows, חקירת תקלות, multithreading, WinForms, WPF, Win32 API, עיצוב UI

היסטוריית עדכונים (גרסה ראשונה, פורסמה בתאריך 22 Aug 2026)
פרסום ראשון
לצטט את המאמר הזה(DOI (ארכיון רשום): 10.5281/zenodo.22176667)

מזהי ה־DOI שלהלן מפנים לגרסאות שנשמרו בעבר בארכיון, ועשויים שלא להתאים לטקסט הנוכחי. להפניה לטקסט הנוכחי, השתמשו בכתובת של דף זה.

Go Komura (2026). מה Not Responding באמת אומר — איך Windows מחליט שיש hang, ואיך לתכנן אפליקציות שלא נתקעות. KomuraSoft LLC. https://comcomponent.com/he/blog/windows-app-not-responding-hang-mechanism/

DOI (ארכיון רשום)
10.5281/zenodo.22176667
DOI (הגרסה האחרונה שנרשמה)
10.5281/zenodo.22176668

“המסך הופך לבן באמצע עיבוד ובשורת הכותרת כתוב Not Responding.” “משתמשים אומרים שזה נתקע מדי פעם, אבל זה אף פעם לא משתחזר במחשב פיתוח.” אלה תלונות נפוצות על אפליקציות עסקיות ב-Windows.

הדבר הראשון להבין הוא שמי שמציג Not Responding הוא Windows, לא האפליקציה עצמה. Windows מזהה שעיבוד ה-messages של ה-window נעצר, ומחליף את המסך המקורי ב-window חלופי. המפתח למעקב אחרי הסיבה הוא מה ה-UI thread שמחזיק את ה-window הזה עושה, כך שהוא לא יכול לעבד את ה-message הבא.12

המאמר מיועד למפתחים שכותבים אפליקציות עסקיות ב-Windows ולאנשי IT שמקבלים כרטיסים על אפליקציות שנתקעות. הוא מתקדם בסדר שיפוט ה-OS → ה-message loop → בידוד הסיבה לפי קטגוריה → תכנונים שלא נתקעים → הליך חקירה. אם צריך לחקור עכשיו אפליקציה שנתקעה, קודם קוראים את פרק 7.

1. קודם כל, המסקנות: לפנות את ה-UI thread, לא להסתיר את התצוגה

המנגנון מאחורי Not Responding, איך מתקנים אותו ואיך חוקרים אותו נהיים ברורים יותר כשמחלקים כך.

מה רוצים לדעת או במה מתקשים הדבר הראשון להבין פירוט
במה Windows מסתכל כדי להחליט שאפליקציה לא מגיבה? הוא מסתכל על ה-window ועל עיבוד ה-messages של ה-GUI thread שמחזיק אותו. השיפוט אינו לפי-process פרק 2
למה כפתורים וציור מחדש נעצרים? ה-UI thread לא יכול לחזור מ-event handler או מהמתנה, ולכן לא יכול לשלוף את ה-message הבא פרקים 3 ו-4
לא רוצים שהמסך יקפא בפעולה ארוכה מעבירים עבודת CPU ו-APIs סינכרוניים בלבד ל-worker; ל-I/O משתמשים ב-APIs אסינכרוניים פרק 5
אפשר פשוט להימנע מהתצוגה עם DoEvents או הגדרה? זה מזמין באגי reentrancy או רק מסתיר את התצוגה. לא תיקון שורש פרק 6
רוצים לברר למה זה נתקע מדי פעם לוקחים dump ברגע ה-hang, לפני יציאה או restart פרק 7

עיקרון התיקון הוא לא לחכות הרבה ולא לחשב בכבדות על ה-UI thread. ה-UI thread מטפל בקלט, בציור, בתצוגת התקדמות ובקבלת ביטול, ומנותק מעבודה שלוקחת זמן.3

גם, הזמן עד שמופיע Not Responding וזמן התגובה שמרגיש נוח הם שני דברים שונים. להישאר מתחת ל-5 שניות אינו מספיק. סעיף 4.5 מסביר את ההבדל הזה.

2. Not Responding הוא שיפוט של ה-OS: כלל 5 השניות והמסך החלופי

2.1 יחידת השיפוט היא ה-window וה-thread שמחזיק אותו, לא כל ה-process

התיעוד של Microsoft ל-IsHungAppWindow מתייחס ל-window כלא מגיב כשהוא מקיים את התנאים הבאים.1

תנאי משמעות
לא ממתין לקלט ה-window אינו במצב של המתנה לקלט
לא ברצף startup האפליקציה אינה ברצף ה-startup שלה
לא שולפת messages היא לא קראה ל-PeekMessage במשך ה-timeout הפנימי של 5 שניות

ה-OS לא מסתכל על מה האפליקציה מחשבת; הוא מסתכל על האם ה-message loop מסתובב. ה-message loop הוא המנגנון ששולף בקשות קלט וציור מחדש לפי הסדר ומעבד אותן. פרק 3 מסביר איך זה באמת עובד.

אותו תיעוד גם קובע שערך 5 השניות עשוי להשתנות בעתיד. זה ה-timeout הפנימי של שיפוט Not Responding, לא כלל תכנון שאומר “מותר לחסום את ה-UI עד 5 שניות.”1

השיפוט אינו לפי-process. באפליקציה עם כמה UI threads, window אחד יכול להיות תקוע בזמן ש-windows שמחזיקים threads אחרים ממשיכים לעבוד. גם בחקירה צריך לעבור מעבר להסתכלות על ה-process ולזהות את ה-thread שמחזיק את ה-window שנתקע.

2.2 המסך הלבן-קפוא הוא “ghost window”

כש-top-level window נשפט כלא מגיב, Windows מסתיר את ה-window המקורי ומחליף אותו ב-ghost window עם אותו Z-order, מיקום, גודל ומראה. ה-“(Not Responding)” בכותרת והמראה הלבן-קפוא תחת ערכת Aero באים מהמסך החלופי הזה, לא מהאפליקציה שנתקעה.2

מצב ה-window מה המשתמש רואה
ה-window המקורי של האפליקציה לא יכול לעבד messages; לא מגיב ללחיצות או לציור מחדש
ה-ghost window שה-OS מספק פעולות מוגבלות כמו הזזה, שינוי גודל, מזעור וסגירה

היכולת להזיז את ה-window החלופי אינה אומרת שתוכן האפליקציה התחיל לעבוד שוב. ה-OS רק ממלא מקום בפעולות המינימום.24

גם פנייה ש”התצוגה מופיעה מוקדם מדי” צריך לטפל בה קודם כבעיה של UI thread שלא חוזר לעיבוד messages זמן רב. חקירת העיבוד שעצר באה לפני דיכוי התצוגה.

2.3 לא לראות את זה תחת debugger אינו אומר שזה לא תקוע

כל עוד מחובר debugger, ה-OS לא יוצר ghost windows. לכן גם כשיש הבדל כמו “זה לא מופיע בהרצה תחת debugger, אבל בריצה רגילה זה הופך ל-Not Responding,” ה-UI thread יכול להיות חסום בדיוק באותו אופן, ורק התצוגה שונה.2

יש גם API שמבטל את ההחלפה ל-ghost window לפי-process, אבל זה אינו API שמתקן את ה-hang עצמו. סעיף 6.2 מסביר את השימוש המיועד בנפרד.

3. למה המסך נעצר: ה-UI thread וה-message loop

3.1 קלט וציור מחדש מטופלים באותו thread

אפליקציות GUI ב-Windows הן event-driven. הן מקבלות messages לעכבר, למקלדת, לבקשות ציור מחדש, ל-timers וכן הלאה, ומריצות את העיבוד שמתאים לכל אחד. כל thread שיוצר window יש לו message queue ומריץ לולאה כמו הבאה.5

MSG msg;
while (GetMessage(&msg, NULL, 0, 0) > 0) {
    TranslateMessage(&msg);
    DispatchMessage(&msg);   // נקראת ה-window procedure
}

GetMessage שולף message מהתור, ו-DispatchMessage קורא ל-window procedure של ה-window. ה-window procedure היא הפונקציה שמבצעת את העיבוד ל-message. טיפול בלחיצת כפתור, ציור מחדש, ו-event handlers של WinForms ו-WPF כולם מתחברים חזרה לעיבוד ה-messages הזה על ה-UI thread.6

המבנה הבסיסי של ה-message loopה-OS שם עכבר, מקלדת וקלט אחר ב-message queue של ה-thread; לולאת ה-UI thread שולפת עם GetMessage וקוראת ל-window procedure עם DispatchMessage, וכשהעיבוד מסתיים היא חוזרת לראש הלולאהOS (קלט, בקשות ציור מחדש, timers)Message queue של ה-threadשליפה עם GetMessageDispatchMessageעיבוד ב-window procedure

איור 1: שולפים message, מעבדים אותו ב-window procedure, וחוזרים ללולאה. המחזור הזה הוא מה שמחזיק קלט וציור עובדים.

עכשיו, מה קורה כשקוראים לפעולה ארוכה מתוך click handler? עד שהפעולה הזו חוזרת, ה-UI thread לא יכול לשלוף את ה-message הבא. לחיצות ובקשות ציור מחדש שמגיעות אחר כך כבר לא ניתנות לעיבוד. זה המבנה הבסיסי של “מסך קפוא.”

3.2 PostMessage ו-SendMessage חוזרים בזמנים שונים

למסירת messages יש שני נתיבים: אחד ששם את ה-message בתור, ואחד שממתין לסיום העיבוד.57

API התנהגות מתי הקורא חוזר
PostMessage שם את ה-message בתור; המקבל שולף ומעבד חוזר ברגע שה-message נכנס לתור. לא ממתין לסיום העיבוד
SendMessage שולח את ה-message ל-window procedure ומעבד אותו לא חוזר עד שה-window procedure מסיים לעבד

במיוחד, כש-SendMessage נשלח ל-window ב-thread אחר, גם השולח ממתין עד שהמקבל יכול לעבד את ה-message. ההבדל הזה מוביל ישירות ל-deadlock בסעיף 4.3.7

4. סיבות לפי קטגוריה: חמישה דפוסים שחוסמים את ה-UI thread

תצוגת Not Responding נראית אותו דבר, אבל מה שחוסם שונה. קודם מפרידים את המועמדים, ואז מאשרים עם ה-dumps וה-traces של פרק 7.

מועמד לסיבה מה קורה על ה-UI thread הופעה טיפוסית
I/O סינכרוני או קריאת רשת ממתין לקובץ, ל-DB, ל-Web API או דומה שיסתיים מהיר במחשב הפיתוח, אבל נתקע בפרודקשן או בסביבות מסוימות
המתנת lock לא מצליח לקחת lock ש-worker מחזיק וממתין נתקע לעיתים נדירות, לפי איך פעולות חופפות
SendMessage בין threads ממתין ש-thread אחר יעבד את ה-message נתפס בהמתנות הדדיות או ב-broadcasts
קריאה ל-COM STA ממתין לעיבוד ה-messages של ה-STA היעד UI thread שעצר מתפשט לקריאות COM מ-threads אחרים
הצטברות פעולות קצרות כל אחת קצרה, אבל הרצה ברצף אף פעם לא חוזרת ללולאה הפעולות נהיות כבדות רק כשמספר הפריטים גדל

4.1 I/O סינכרוני וקריאות רשת

הדפוס הנפוץ ביותר הוא לבצע באופן סינכרוני, בתוך click handler של כפתור, קריאה או כתיבה של קובץ גדול, שאילתת מסד נתונים, קריאת Web API, או גישה לכונן רשת.

מה שמסתיים בשבריר שנייה במחשב הפיתוח יכול להפוך להמתנה של עשרות שניות תחת השהיית רשת בפרודקשן או בעיה בשרת קבצים. לכונני רשת יש timeouts ארוכים כשהחיבור נופל, וזה מחמיר את התסמין. “זה היה מהיר במחשב הפיתוח” אינו סיבה להמתין על ה-UI thread.

4.2 ה-UI thread ממתין ל-lock ש-worker מחזיק

גם locks שמגנים על נתונים משותפים יכולים לעצור את ה-UI. כש-worker מחזיק lock זמן רב וה-UI thread גם מנסה לקחת את אותו lock, ה-UI thread נשאר ממתין עד שהוא יכול לרכוש אותו.

גם אחרי שמעבירים את העבודה ל-worker, המסך לא מתפנה אם ה-UI thread ממתין שהעבודה תסתיים או שה-lock ישוחרר. משמעת locks מכוסה בפירוט בסדרת multithreading בפועל.

4.3 המתנה להשלמה הדדית דרך SendMessage

ה-deadlock הקלאסי הוא הצירוף שבו ה-UI thread ממתין ש-worker יסיים, ואותו worker שולח SendMessage ל-UI וממתין. ה-UI ממתין ולא יכול לעבד messages, וה-worker לא יכול לחזור מ-SendMessage, כך שאף אחד לא יכול להתקדם.75

Deadlock מ-SendMessage בין threadsכש-worker thread שולח SendMessage ל-window של ה-UI thread בזמן שה-UI thread חסום בהמתנה לתוצאת ה-worker, כל אחד ממתין שהשני יסיים והתוצאה היא deadlockWorker threadUI threadWorker threadUI threadלא יכול לעבד messages (ממתין)לא יכול לחזור מ-SendMessageממתינים זה לזה: deadlockהמתנה שה-worker יסיים (חסום)SendMessage (לא חוזר עד שהעיבוד מסתיים)

איור 2: העברת עבודה ל-worker לבדה אינה מספיקה. כשה-UI וה-worker ממתינים להשלמה זה של זה, התוצאה היא deadlock.

גם שליחה ל-HWND_BROADCAST דורשת זהירות. window אחד שלא מגיב מספיק כדי לגרור את השולח. במקומות שאי אפשר להרשות לעצמכם להמשיך לחכות, שוקלים SendMessageTimeout, שמציב timeout, או PostMessage, שלא ממתין לסיום העיבוד.8

4.4 COM STA נעצר וגורר threads אחרים איתו

קריאות מ-threads אחרים לאובייקט STA נמסרות כ-window messages. לכן כשה-UI thread (STA) חסום, גם קריאות COM אליו נחסמות. זה מבנה שבו לא רק ה-UI אלא גם הקוראים נשארים ממתינים.

הקשר בין apartments לעיבוד messages מוסבר במאמר COM STA/MTA.

4.5 לחזור על “רק רגע” 100 פעמים

גם פעולה סינכרונית של 50 ms מצטברת ל-5 שניות כשחוזרים עליה 100 פעמים. אל תשפטו לפי מדידת פונקציות בודדות ומציאתן קצרות; תחשבו במונחי הזמן הכולל עד שה-UI thread חוזר ל-message loop.

כבדות נתפסת מתחילה בסביבות 100 ms. אל תכוונו לסף 5 השניות של Not Responding; תכננו מתוך הנחה שה-UI thread מותר לחסום רק למילישניות.

5. תכנונים שלא נתקעים: להפריד בין הרצת העבודה לעדכון המסך

5.1 להפריד חישוב ו-APIs סינכרוניים מ-I/O אסינכרוני

עיקרון התיקון הוא לדחוף עבודה שלוקחת זמן מחוץ ל-UI thread. עם זאת, לא הכול מועבר באותו אופן.

סוג העבודה טיפול בסיסי ב-C# תפקיד ה-UI thread
חישוב כבד על CPU מעבירים ל-worker thread עם Task.Run ממתינים להשלמה באופן אסינכרוני ומציגים את התוצאה
עיבוד עם API סינכרוני בלבד מפרידים ל-worker thread לא חוסמים את ה-UI בהמתנה סינכרונית להשלמה
I/O עם API אסינכרוני await ל-GetStringAsync או דומה חוזרים לעיבוד messages בזמן שה-I/O ממתין
קלט, ציור, התקדמות, ביטול מטפלים ב-UI thread לא מערבבים חישוב ארוך או I/O סינכרוני

ב-I/O אסינכרוני אין צורך לתפוס thread רק כדי להמתין. גם דוגמת הקוד למטה משתמשת ב-Task.Run לחישוב וב-API האסינכרוני המקורי לתקשורת HTTP.3

5.2 ב-C#, משתמשים ב-async/await כדי לחזור ל-UI ולעדכן אותו

הדוגמה הבאה מפרידה עבודה כבדה מעדכוני UI ב-click handler של WinForms. כי הדוגמה הזו מתחילה מ-UI event handler ושומרת על execution context של ה-UI, ההמשך אחרי await חוזר ל-UI thread, שם אפשר לעדכן controls.3

private async void RunButton_Click(object sender, EventArgs e)
{
    runButton.Enabled = false;
    try
    {
        // עבודה כבדה על ה-CPU ו-APIs שהם סינכרוניים בלבד הולכים ל-worker דרך Task.Run
        var result = await Task.Run(() => HeavyCalculation(input));

        // ל-I/O משתמשים ב-API אסינכרוני מקורי (גם הוא אינו צורך thread)
        var data = await httpClient.GetStringAsync(url);

        // אחרי await אנחנו שוב על ה-UI thread, כך שאפשר לגעת ב-controls ישירות
        resultLabel.Text = result + data.Length.ToString();
    }
    catch (Exception ex)
    {
        // חריגה שדולפת מ-handler מסוג async void מפילה את האפליקציה. תופסים אותה כאן
        MessageBox.Show($"הפעולה נכשלה: {ex.Message}");
    }
    finally
    {
        runButton.Enabled = true;
    }
}

יש ארבע נקודות לקחת ממנה.

איפה הכוונה
המתנה להשלמה עם await מחזירה את ה-UI thread ל-message loop בכל פעם שיש המתנה
עדכון המסך אחרי await לא נוגע ב-controls מה-worker
השבתת הכפתור בזמן ריצה מונעת מאותה פעולה להתחיל שוב ושוב
catch ו-finally מונעת מחריגות לדלוף מ-handler ה-async void ומחזירה את מצב הכפתור

זו דוגמת קוד שמראה את חלוקת התפקידים; המימוש של HeavyCalculation ודומים, טיפול בסגירת טופס, והתקדמות וביטול מושמטים. ההתקדמות והביטול שעבודה ארוכה צריכה מתווספים בנפרד, כמו בסעיף 5.4.

Controls של WinForms ורכיבי WPF מטופלים מה-thread שיצר אותם. מגע ישיר מ-worker מוביל לחריגות או להתנהגות לא מוגדרת. כדי לחזור ל-UI thread במפורש, משתמשים ב-Control.Invoke / BeginInvoke ב-WinForms וב-Dispatcher.InvokeAsync ב-WPF.3

5.3 ב-Win32, מודיעים על השלמה עם PostMessage

חלוקת התפקידים זהה גם ב-Win32 native. מוסרים את העבודה ל-worker, וכשהוא מסיים שולחים completion message מותאם עם PostMessage ומעדכנים את המסך ב-window procedure של ה-UI thread. כי PostMessage חוזר ברגע שה-message נכנס לתור, ה-worker לא ממתין שה-UI יסיים לעבד.7

חלוקת תפקידים באפליקציה שלא נתקעתUI thread מטפל רק בקבלת קלט, בהצגת התקדמות ובקבלת ביטול; worker thread מריץ את העבודה הכבדה ומחזיר השלמה ל-UI thread דרך PostMessage או המשך awaitמסירת העבודהPostMessage / המשך awaitUI thread: קלט, התקדמות, ביטולWorker thread: עבודה כבדהאין I/O סינכרוני או חישוב ארוך על ה-UI thread

איור 3: מוסרים חישוב כבד ועבודה סינכרונית ל-worker, ומחזירים עדכוני מסך ל-UI thread. I/O אסינכרוני ממתין עם API אסינכרוני, כמו בסעיף 5.1.

ההמתנה על ה-worker עצמו נכתבת לפי המשמעת שמכוסה במאמר condition variables. חשוב גם שה-UI thread לא יחזור להמתין באופן סינכרוני ל-worker.

5.4 לתכנן התקדמות וביטול מההתחלה

גם אם המסך לא קופא, אם כלום לא משתנה זמן רב המשתמש לא יכול לדעת אם זה עובד. לכן מתכננים תצוגת התקדמות וביטול כחלק מהעיבוד.

כיוון מנגנון תפקיד
Worker → UI IProgress<T> מדווח התקדמות למסך
UI → worker CancellationToken מעביר בקשת עצירה

ה-worker עוצר בגבול נוח ומנקה. אמצעי התקדמות וביטול מצמצמים את המצבים שבהם משתמשים נסמכים על סיום כפוי. סיום כפוי יכול להוביל לקלקול נתונים.

6. שתי שיטות שנראות כמו פתרונות עוקפים, והמגבלות שלהן

6.1 DoEvents מזמין events אחרים לאמצע העבודה

הכנסת Application.DoEvents() או לולאת PeekMessage במהלך עבודה כבדה מעבדת messages ונמנעת מתצוגת Not Responding. עם זאת, event handler אחר רץ עכשיו בזמן שהעבודה המקורית עדיין לא הסתיימה. זה reentrancy.

למשל, קוראים ל-DoEvents באמצע עיבוד נתונים, ועיבוד לחיצה שנייה על הכפתור שהצטברה בתור. אותו handler משנה את אותם נתונים, ואז העבודה המקורית ממשיכה. בסדר הזה, המצב שהעבודה המקורית הניחה כבר איננו.

כפתורים אינם הדבר היחיד שנכנס מחדש. סגירת הטופס ו-timers נכנסים גם הם. באגים כמו נתונים שעדיין מעובדים שנשברים, או חריגה ממגע בטופס שנסגר, תלויים בתזמון וקשים לשחזור, והם נוטים להיות קשים יותר לחקירה מ-Not Responding.

העיקרון הוא להפריד את העבודה, לא לשאוב את הלולאה ידנית. משאירים עיבוד messages ידני במבנים מוגבלים כמו דיאלוג התקדמות מודאלי. לעבודה ארוכה רגילה, משתמשים בהפרדה ובמניעת reentrancy של פרק 5.

6.2 ביטול ghost windows לא מחזיר שליטה

DisableProcessWindowsGhosting הוא API שמבטל את ההחלפה ל-ghost window ל-process הקורא. הביטול נמשך לכל משך חיי ה-process.4

הוא קיים לשימושים מיוחדים כמו מסופי kiosk, שבהם רוצים למנוע מה-OS לשים window חלופי שניתן לתפעול. העובדה שעיבוד messages נעצר לא משתנה, והמשתמש גם מאבד את האמצעים להזיז או לסגור את ה-window שה-ghost window סיפק. זה לא משהו לשימוש כפתרון Not Responding באפליקציות רגילות.

שיטה מה משתנה מה נשאר
שאיבה ידנית עם DoEvents או דומה Messages אחרים מעובדים באמצע העבודה Events שרירותיים נכנסים מחדש ויכולים לקלקל מצב
DisableProcessWindowsGhosting עוצר את ה-OS מהחלפה ל-window חלופי ה-UI thread נשאר חסום
הפרדת עבודה שלוקחת זמן מה-UI מאפשרת ל-UI thread לחזור לקלט ולציור צריך לתכנן גם התקדמות, ביטול ומניעת reentrancy

7. הליך חקירה: לשמר את רגע ה-hang לפני הסגירה

7.1 קודם לוקחים dump

בחקירה של “זה נתקע מדי פעם,” הדבר היקר ביותר הוא מצב ה-threads ברגע שהוא תקוע. ברגע שיוצאים או עושים restart, המצב הזה אובד.

בלשונית Details של Task Manager, לוחצים לחיצה ימנית על ה-process היעד ובוחרים Create dump file. זה נותן full dump שמכיל את ה-stack של כל thread. משתפים את הנוהל גם עם מי שמקבלים את הכרטיסים: “כשזה נתקע, לוקחים dump לפני שסוגרים.”

לבניית מנגנון איסוף, ראו את מאמר איסוף crash dump.

7.2 להסתכל על ה-thread שמחזיק את ה-window שנתקע

פותחים את ה-dump ב-WinDbg ובודקים את ה-stack של ה-UI thread. ה-thread שמריץ את ה-message loop הוא בדרך כלל thread 0, אבל בחלק מהאפליקציות יש כמה UI threads, לכן לא מחליטים לפי מספר בלבד; מסתכלים על ה-thread שמחזיק את ה-window שנתקע.

מה מופיע ב-stack מה לבדוק אחר כך
המתנה ב-ReadFile או ב-API רשת גישה לקבצים, I/O סינכרוני, המתנה לתגובת רשת
המתנה בפונקציית WaitFor… ה-lock או אובייקט הסנכרון. לאיזו השלמה הוא ממתין, ומה הצד השני עושה
המתנה בתוך SendMessage האם thread היעד יכול לעבד messages. האם הם ממתינים זה לזה

ה-stack של ה-UI thread מראה על מה הוא ממתין. כשחשוד deadlock, מצליבים את יעדי ההמתנה של שני ה-threads, לא רק של אחד. איך קוראים אותם מוסבר במאמר המבוא ל-WinDbg.

הליך בסיסי לחקירת Not Respondingלוקחים dump ברגע ה-hang, מסתכלים על ה-stack של ה-UI thread, מזהים אם הוא נעצר ב-I/O סינכרוני, בהמתנת lock או ב-SendMessage בין threads, ומחברים זאת לתיקון התכנון המתאיםרגע ה-hangלוקחים dump (לפני הסגירה)מסתכלים על ה-stack של ה-UI threadI/O סינכרוני או המתנת רשתהמתנת lockSendMessage בין threadsמפרידים את המקום ל-worker

איור 4: מ-dump של רגע ה-hang עוקבים אחרי מה שה-UI thread ממתין לו. ל-I/O, הופכים לאסינכרוני או מפרידים; ל-locks ול-SendMessage, בודקים גם את מבנה ההמתנה.

7.3 לבחור בין בדיקה חיה לניתוח על ציר זמן

מלבד dumps, יש דרך להסתכל על process רץ במקום ודרך לרשום את זרימת הזמן.

מה רוצים לברר שיטה
לשמר את רגע ה-hang ולבדוק אותו אחר כך לוקחים dump ומנתחים ב-WinDbg
לראות threads ו-stacks של process חי במקום משתמשים ב-Process Explorer
לעקוב אחרי כבדות קבועה או המתנות של UI thread לאורך זמן מצלמים trace ב-WPR ומנתחים ב-WPA

איך מצלמים ציר זמן מכוסה ב-WPR/WPA בפועל.

7.4 לצמצם מועמדים מהתסמין, ואז לאשר בראיות

תנאי שחזור הם גם כניסה לחקירה. אבל לא מחליטים את הסיבה מהתסמין לבדו; מצליבים מול dumps ו-traces.

תסמין חשוד ראשון איפה לבדוק
תמיד נתקע בפעולה מסוימת I/O סינכרוני או דומה בתוך אותו handler העיבוד של אותה פעולה וה-stack של ה-UI thread
נתקע לעיתים נדירות, בלי מתאם לפעולות סדר locks, או deadlock של SendMessage בין threads ה-stacks של שני ה-threads שממתינים זה לזה
נתקע רק בסביבה מסוימת המתנות ו-timeouts שנגרמים מכונני רשת, proxies, אנטי-וירוס וכדומה ה-API שממתינים לו וזמן התגובה שלו באותה סביבה

8. סיכום

Not Responding הוא מנגנון שבו Windows קובע שעיבוד ה-messages של window נעצר ושם מסך חלופי. יחידת השיפוט היא ה-window וה-GUI thread שמחזיק אותו, לא כל ה-process. ה-timeout הפנימי של 5 שניות הוא ערך שעשוי להשתנות בעתיד, והוא נפרד מזמן תגובת UI נוח.12

מה שצריך לתקן אינו התצוגה אלא החישוב או ההמתנה שתופסים את ה-UI thread זמן רב. שולחים עבודת CPU ו-APIs סינכרוניים בלבד ל-worker, שולחים I/O אסינכרוני ל-APIs אסינכרוניים, ומחזירים עדכוני מסך ל-UI thread. מתכננים התקדמות, ביטול ומניעת reentrancy כסט. התחמקות עם DoEvents או ביטול ghost windows אינה תחליף.

כשהסיבה לא ידועה, מתחילים בלקיחת dump לפני יציאה ובהסתכלות על מה ה-thread שמחזיק את ה-window שנתקע ממתין לו. מחליפים את “זה נראה שבור” ב-“ה-UI thread לא יכול לחזור לעיבוד messages,” ומקומות הבדיקה ותיקוני התכנון מתחברים.

מאמרים קשורים

תחומי ייעוץ קשורים

KomuraSoft LLC מטפלת בחקירת שורש (ניתוח dump וניתוח trace) של אפליקציות עסקיות ש”נתקעות מדי פעם” או הופכות ל-Not Responding, ב-refactoring של קוד UI ישן מלא בעיבוד סינכרוני להפרדה ל-async/await ול-worker thread, ובסקירות של עיצובי UI שלא קופאים. גם לפני שנוהל השחזור ידוע, אפשר לעזור החל מתכנון איך אוספים את הראיות.

קישורים

  1. Microsoft Learn, IsHungAppWindow function (winuser.h). על הקריטריון שאפליקציה נחשבת לא מגיבה כשהיא “לא ממתינה לקלט, אינה ברצף startup, ולא קראה ל-PeekMessage במשך ה-timeout הפנימי של 5 שניות”; על כך שקריטריון 5 השניות הזה עשוי להשתנות; ועל כך שהפונקציה תמיד מחזירה TRUE ל-ghost window. ↩ ↩2 ↩3 ↩4

  2. Microsoft Learn, GetMessage function (winuser.h). על כך שהמערכת מתייחסת ל-top-level window כלא מגיב כשהוא מפסיק להגיב ל-messages כמה שניות ומחליפה אותו ב-ghost window עם אותו Z-order, מיקום, גודל ומראה; על כך שהמשתמש יכול רק להזיז, לשנות גודל או לסגור; ועל כך שלא נוצר ghost window כשמחובר debugger. ↩ ↩2 ↩3 ↩4 ↩5

  3. Microsoft Learn, How to make thread-safe calls to controls (Windows Forms). על כך ש-controls של WinForms אינם בטוחים למגע מכל thread שאינו זה שיצר אותם; על שימוש ב-Invoke/BeginInvoke לעדכונים מ-thread אחר; ועל דפוסים אסינכרוניים בטוחים עם async/await או BackgroundWorker. ↩ ↩2 ↩3 ↩4

  4. Microsoft Learn, DisableProcessWindowsGhosting function (winuser.h). על היכולת לבטל, ל-GUI process הקורא, את תכונת ה-ghost window שהופכת window לא מגיב לניתן למזעור, להזזה ולסגירה; ועל כך שהביטול נמשך לכל משך חיי ה-process. ↩ ↩2

  5. Microsoft Learn, About Messages and Message Queues. על כך שאפליקציות Windows הן event-driven ושה-window procedure מעבדת messages; על ההבחנה בין messages שבתור ל-messages שנשלחים ישירות; על החלפת window לא מגיב ב-ghost window; ועל הסעיף שעוסק ב-deadlock מ-threads ששולחים messages זה לזה. ↩ ↩2 ↩3

  6. Microsoft Learn, Using Messages and Message Queues. על מימוש message loop סטנדרטי עם GetMessage, TranslateMessage ו-DispatchMessage, ועל איך בודקים message queue. ↩

  7. Microsoft Learn, SendMessage function (winuser.h). על כך ש-SendMessage קורא ל-window procedure של ה-window שצוין ולא חוזר עד שהעיבוד מסתיים; על כך ששליחה ל-window ב-thread אחר גורמת לשולח להמתין עד שאותו thread מעבד את ה-message; ועל ההבדל מ-PostMessage, ששם את ה-message בתור בלי להמתין לתשובה. ↩ ↩2 ↩3 ↩4

  8. Microsoft Learn, SendMessageTimeout function (winuser.h). על היכולת לשלוח message עם timeout; ועל דגל (SMTO_ABORTIFHUNG) שחוזר בלי להמתין כשה-window לא מגיב (נשפט כ-hung). ↩

מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.

העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.

המאמר קשור ישירות לשירותים הבאים.

שאלות נפוצות

שאלות נפוצות בפניות בנושא המאמר.

באילו תנאים מופיע Not Responding?
ה-OS שופט window כלא מגיב כשאפליקציה עם window אינה ממתינה לקלט, אינה ברצף ה-startup, ולא שלפה message (PeekMessage) במשך 5 שניות. ה-top-level window ששופט כך מוסתר ומוחלף ב-ghost window באותו מיקום, גודל ומראה. הטקסט "(Not Responding)" בשורת הכותרת והמראה הלבן-קפוא שייכים ל-ghost window הזה, שמאפשר רק להזיז, למזער או לסגור. כלומר Not Responding אינו משהו שהאפליקציה עצמה מציגה — זו מסך שה-OS שם בשמה.
יש הגדרה שמונעת מ-Not Responding להופיע בזמן שיש עבודה בתהליך?
קריאה ל-DisableProcessWindowsGhosting מבטלת את ההחלפה ל-ghost window ל-process הזה. זה רק הופך את ה-hang לפחות נראה למשתמש — ה-window עדיין לא מגיב לקלט, ומנקודת מבט המשתמש זו הקפאה מלאה בלי דרך להזיז או לסגור. התיקון האמיתי אינו דיכוי התצוגה, אלא העברת עבודה כבדה ל-worker thread כדי ש-UI thread לעולם לא ייחסם אפילו לעשירית שנייה, שלא לדבר על חמש. שימו לב גם שה-OS לא יוצר ghost window כשמחובר debugger, כך שיכול להיראות כאילו Not Responding אף פעם לא קורה בניפוי.
מקובל להימנע מ-Not Responding עם DoEvents (שאיבת message loop ידנית)?
לא מומלץ. סיבוב DoEvents או לולאת PeekMessage באמצע עבודה כבדה יתחמק משיפוט ה-hung window, אבל כל event handler יכול אז להיכנס מחדש באמצע העבודה: לחיצה שנייה על הכפתור, סגירת ה-window, timer וכן הלאה. באגי reentrancy כמו handler אחר שכותב מחדש נתונים שעדיין מעובדים, או נוגע בטופס שאמור היה להיסגר וזורק חריגה, תלויים בתזמון וקשים לשחזור — גרוע מ-Not Responding עצמו. הגישה הנכונה היא להעביר את העבודה עצמה ל-worker thread עם Task.Run או דומה, ולהשאיר את ה-UI thread אחראי רק לתצוגת התקדמות ולקבלת ביטול.
איך מעדכנים את המסך (controls) מ-worker thread?
Controls של WinForms ורכיבי WPF מותרים למגע רק מה-thread שיצר אותם (בדרך כלל UI thread). מגע ישיר מ-worker thread גורם לחריגות או להתנהגות לא מוגדרת. ב-C#, async/await הוא הנתיב הקל: ההמשך אחרי await חוזר ל-UI thread הקורא, כך שאפשר לעדכן controls כרגיל אחרי ה-await. למעבר מפורש משתמשים ב-Control.Invoke/BeginInvoke ב-WinForms וב-Dispatcher.InvokeAsync ב-WPF. ב-Win32 native, הדפוס המקובל הוא ש-worker thread שולח PostMessage של completion message מותאם ל-UI thread, ו-window procedure מעדכנת את המסך.
איך חוקרים למה אפליקציה מציגה Not Responding?
הדבר החשוב הוא ללכוד את המצב ב"רגע עצמו" של ה-hang. קודם לוקחים full dump מלשונית Details של Task Manager עם "Create dump file", ואז ב-WinDbg מסתכלים על ה-stack של ה-UI thread (ה-thread שמריץ את ה-message loop). האם הוא תקוע ב-I/O סינכרוני, בהמתנת רשת, בהמתנת lock, או בהמתנה ל-thread אחר דרך SendMessage — זה מופיע ב-stack כפי שהוא. ל-process חי, רשימת ה-threads ומבט ה-stack של Process Explorer שימושיים; למעקב לאורך זמן, לכידת traces של WPR יעילה. ראו גם את מאמרי המבוא ל-WinDbg, Process Explorer בפועל, ו-WPR/WPA בפועל באתר.

פרופיל הכותב

עמוד היכרות עם כותב המאמר.

Go Komura

מנהל KomuraSoft LLC

מתמחה בפיתוח תוכנה עבור Windows, ייעוץ טכני וחקירת תקלות, בעיקר בפרויקטים עם מערכות קיימות ובאגים שקשה לשחזר.

קישורים ציבוריים

חזרה לבלוג