מה Not Responding באמת אומר — איך Windows מחליט שיש hang, ואיך לתכנן אפליקציות שלא נתקעות
· עודכן בתאריך: · Go Komura · 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
flowchart TB
accTitle: המבנה הבסיסי של ה-message loop
accDescr: ה-OS שם עכבר, מקלדת וקלט אחר ב-message queue של ה-thread; לולאת ה-UI thread שולפת עם GetMessage וקוראת ל-window procedure עם DispatchMessage, וכשהעיבוד מסתיים היא חוזרת לראש הלולאה
os["OS (קלט, בקשות ציור מחדש, timers)"] --> q["Message queue של ה-thread"]
q --> gm["שליפה עם GetMessage"]
gm --> dm["DispatchMessage"]
dm --> wp["עיבוד ב-window procedure"]
wp --> gm
איור 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
sequenceDiagram
accTitle: Deadlock מ-SendMessage בין threads
accDescr: כש-worker thread שולח SendMessage ל-window של ה-UI thread בזמן שה-UI thread חסום בהמתנה לתוצאת ה-worker, כל אחד ממתין שהשני יסיים והתוצאה היא deadlock
participant U as UI thread
participant W as Worker thread
U->>U: המתנה שה-worker יסיים (חסום)
W->>U: SendMessage (לא חוזר עד שהעיבוד מסתיים)
Note over U: לא יכול לעבד messages (ממתין)
Note over W: לא יכול לחזור מ-SendMessage
Note over U,W: ממתינים זה לזה: deadlock
איור 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
flowchart TB
accTitle: חלוקת תפקידים באפליקציה שלא נתקעת
accDescr: UI thread מטפל רק בקבלת קלט, בהצגת התקדמות ובקבלת ביטול; worker thread מריץ את העבודה הכבדה ומחזיר השלמה ל-UI thread דרך PostMessage או המשך await
ui["UI thread: קלט, התקדמות, ביטול"] -->|"מסירת העבודה"| w["Worker thread: עבודה כבדה"]
w -->|"PostMessage / המשך await"| ui
ui -.-> ng["אין 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.
flowchart TB
accTitle: הליך בסיסי לחקירת Not Responding
accDescr: לוקחים dump ברגע ה-hang, מסתכלים על ה-stack של ה-UI thread, מזהים אם הוא נעצר ב-I/O סינכרוני, בהמתנת lock או ב-SendMessage בין threads, ומחברים זאת לתיקון התכנון המתאים
hang["רגע ה-hang"] --> dump["לוקחים dump (לפני הסגירה)"]
dump --> stack["מסתכלים על ה-stack של ה-UI thread"]
stack --> io["I/O סינכרוני או המתנת רשת"]
stack --> lock["המתנת lock"]
stack --> sm["SendMessage בין threads"]
io -.-> fix["מפרידים את המקום ל-worker"]
lock -.-> fix
sm -.-> fix
איור 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,” ומקומות הבדיקה ותיקוני התכנון מתחברים.
מאמרים קשורים
- Spurious wakeup — למה condition variable מתעורר בלי notify, ואיך לחכות נכון ב-Windows
- שיטות עבודה מומלצות ל-multithreading בפועל: מהדורת .NET — מה להחליט לפני שמוסיפים threads
- STA ו-MTA ב-COM — threading model, ואיך נמנעים מ-hang
- קריאת crash dump ב-WinDbg + SOS — מבוא מעשי לניתוח אחרי האיסוף
- Process Explorer / Handle / VMMap בפועל — מעקב אחרי hangs, leaks ו-“הקובץ בשימוש” ממצב הרגע הזה
- Shutdown ב-Windows מנקודת המבט של האפליקציה — exit notifications, restart ו-power loss
תחומי ייעוץ קשורים
KomuraSoft LLC מטפלת בחקירת שורש (ניתוח dump וניתוח trace) של אפליקציות עסקיות ש”נתקעות מדי פעם” או הופכות ל-Not Responding, ב-refactoring של קוד UI ישן מלא בעיבוד סינכרוני להפרדה ל-async/await ול-worker thread, ובסקירות של עיצובי UI שלא קופאים. גם לפני שנוהל השחזור ידוע, אפשר לעזור החל מתכנון איך אוספים את הראיות.
קישורים
-
Microsoft Learn, IsHungAppWindow function (winuser.h). על הקריטריון שאפליקציה נחשבת לא מגיבה כשהיא “לא ממתינה לקלט, אינה ברצף startup, ולא קראה ל-PeekMessage במשך ה-timeout הפנימי של 5 שניות”; על כך שקריטריון 5 השניות הזה עשוי להשתנות; ועל כך שהפונקציה תמיד מחזירה TRUE ל-ghost window. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, GetMessage function (winuser.h). על כך שהמערכת מתייחסת ל-top-level window כלא מגיב כשהוא מפסיק להגיב ל-messages כמה שניות ומחליפה אותו ב-ghost window עם אותו Z-order, מיקום, גודל ומראה; על כך שהמשתמש יכול רק להזיז, לשנות גודל או לסגור; ועל כך שלא נוצר ghost window כשמחובר debugger. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, How to make thread-safe calls to controls (Windows Forms). על כך ש-controls של WinForms אינם בטוחים למגע מכל thread שאינו זה שיצר אותם; על שימוש ב-Invoke/BeginInvoke לעדכונים מ-thread אחר; ועל דפוסים אסינכרוניים בטוחים עם async/await או BackgroundWorker. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, DisableProcessWindowsGhosting function (winuser.h). על היכולת לבטל, ל-GUI process הקורא, את תכונת ה-ghost window שהופכת window לא מגיב לניתן למזעור, להזזה ולסגירה; ועל כך שהביטול נמשך לכל משך חיי ה-process. ↩ ↩2
-
Microsoft Learn, About Messages and Message Queues. על כך שאפליקציות Windows הן event-driven ושה-window procedure מעבדת messages; על ההבחנה בין messages שבתור ל-messages שנשלחים ישירות; על החלפת window לא מגיב ב-ghost window; ועל הסעיף שעוסק ב-deadlock מ-threads ששולחים messages זה לזה. ↩ ↩2 ↩3
-
Microsoft Learn, Using Messages and Message Queues. על מימוש message loop סטנדרטי עם GetMessage, TranslateMessage ו-DispatchMessage, ועל איך בודקים message queue. ↩
-
Microsoft Learn, SendMessage function (winuser.h). על כך ש-SendMessage קורא ל-window procedure של ה-window שצוין ולא חוזר עד שהעיבוד מסתיים; על כך ששליחה ל-window ב-thread אחר גורמת לשולח להמתין עד שאותו thread מעבד את ה-message; ועל ההבדל מ-PostMessage, ששם את ה-message בתור בלי להמתין לתשובה. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, SendMessageTimeout function (winuser.h). על היכולת לשלוח message עם timeout; ועל דגל (SMTO_ABORTIFHUNG) שחוזר בלי להמתין כשה-window לא מגיב (נשפט כ-hung). ↩
מאמרים קשורים
מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.
סיום התמיכה ב-printer drivers של Windows — איך אפליקציות עסקיות מתכוננות להדפסת דוחות ותוויות
Microsoft מפסיקה בהדרגה printer drivers מסוג v3/v4. מה Windows protected print mode מסיר, ואיך עושים inventory ומכינים הדפסת דוחות ותוויו...
Win32 Thread Pool API — מקביליות בלי CreateThread, דרך CreateThreadpoolWork
מפזרים קריאות CreateThread בכל הקוד ה-native? המאמר מסביר את ה-Win32 Thread Pool API שעוצב מחדש ב-Vista: ארבעת האובייקטים work, timer, wa...
DllMain ו-loader lock — הסיבה האמיתית שאומרים לא לעשות כלום ב-initialization של DLL
למה אסור לקרוא ל-LoadLibrary או להסתנכרן עם threads מתוך DllMain. המאמר מסביר ממקורות ראשוניים איך loader lock מסדר כל DLL notification, ...
Spurious wakeup — למה condition variable מתעורר בלי notify, ואיך לחכות נכון ב-Windows
Wait של condition variable יכול להתעורר בלי notify (spurious wakeup). למה Windows מתיר זאת, וצורת ה-wait הנכונה עם while ו-predicate ב-Wi...
איך clipboard ו-drag-and-drop עובדים — טיפול נכון ב-OLE data transfer באפליקציות עסקיות
למה הדבקת Excel מתפרקת ולמה paste נכשל אחרי סגירת המקור: clipboard formats, delayed rendering, OLE drag-and-drop, ומדיניות clipboard hist...
נושאים קשורים
העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.
נושאים טכניים ב-Windows
שער לנושאי פיתוח Windows, חקירת תקלות וניצול נכסים קיימים.
תהליכון ה-UI וטיימרים
תהליכון ה-UI של WPF / WinForms, זרימות אסינכרוניות, Dispatcher ותכנון טיימרים.
שירותים הקשורים לנושא הזה
המאמר קשור ישירות לשירותים הבאים.
פיתוח יישומי Windows
יישומים עסקיים, חיבור התקנים וכלי תקשורת, מהגדרת הדרישות ועד הפיתוח.
שאלות נפוצות
שאלות נפוצות בפניות בנושא המאמר.
- באילו תנאים מופיע 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 בפועל באתר.