מה "לא מגיב" באמת — איך Windows מחליט שיישום נתקע, ואיך לתכנן יישומים שלא
· Go Komura · Windows, פיתוח Windows, חקירת תקלות, ריבוי תהליכונים, WinForms, WPF, Win32 API, עיצוב UI
“היישום הופך לבן באמצע פעולה ומציג (לא מגיב).” “מגיעים כרטיסים שהוא נתקע מדי פעם, אבל זה אף פעם לא משתחזר במחשב פיתוח.” — ליישומים עסקיים ב-Windows, “לא מגיב” הזה הוא אחת התלונות הנפוצות ביותר. ומה שמפתיע כמה מעט יודעים הוא שהדבר ששם את תצוגת “לא מגיב” אינו היישום הנתקע עצמו — זו מערכת ההפעלה.
איך Windows יודע שיישום “נתקע”? מהו החלון הלבן-קפוא הזה? מיועד למפתחות ולמפתחים שכותבים יישומים עסקיים ב-Windows ולאנשי IT שמקבלים כרטיסים על יישומים שנתקעים, המאמר הזה עובר על שיפוט “לא מגיב” מיסודות לולאת ההודעות, ומסדר את הסיבות הקלאסיות לתקיעות, תכנונים שלא נתקעים, והליך לחקירת רגע התקיעה — הכול מעוגן במקורות ראשוניים.
1. השורה התחתונה קודם
- “לא מגיב” הוא שיפוט של מערכת ההפעלה. כשחלון (ותהליכון ה-GUI שמחזיק אותו) אינו ממתין לקלט, אינו ברצף ההפעלה, ולא שלף הודעה (
PeekMessage) במשך 5 שניות, מערכת ההפעלה מתייחסת אליו כלא מגיב. השיפוט אינו לפי-תהליך.1 - החלון המולבן הוא “חלון רפאים”. מערכת ההפעלה הסתירה את החלון המקורי והחליפה בו זיוף באותו מיקום, גודל ומראה. כל מה שאפשר לעשות הוא להזיז, למזער או לסגור; התוכן אינו רץ. חלון רפאים לא נוצר כשמחובר מנפה שגיאות.2
- סיבת התקיעה כמעט תמיד מתכנסת לדבר אחד. תהליכון ה-UI שאמור לשאוב את לולאת ההודעות חסום על עבודה כבדה או המתנה. I/O סינכרוני, קריאות רשת, המתנות נעילה, ו-
SendMessageבין תהליכונים הם הקלאסיקה.3 - עיקרון התכנון הוא “אל תמתינו ואל תחשבו על תהליכון ה-UI”. מעבירים עבודה כבדה לתהליכון עובד (
async/await+Task.Runב-C#) ומשאירים את תהליכון ה-UI מסור לציור, התקדמות וקבלת ביטול.4 DoEventsושאיבת לולאת ההודעות ידנית הם בית גידול לבאגי reentrancy. תצוגת “לא מגיב” נעלמת, אבל המבנה עכשיו נותן לאירועים שרירותיים להפריע באמצע העבודה. הפרדה, לא התחמקות, היא הגישה הנכונה.- חקירה מתחילה בלכידת מצב ברגע התקיעה. לוקחים dump ומסתכלים על המחסנית של תהליכון ה-UI, וכמעט תמיד אפשר לזהות על מה הוא ממתין.
2. תנאי מוקדם: יישומי Windows מונעים על ידי הודעות
כדי להבין “לא מגיב”, קודם צריך לקבל שיישום GUI ב-Windows הוא מונע-אירועים. יישום GUI לא הולך להביא קלט בעצמו; הוא מקבל הודעות שמערכת ההפעלה מוסרת (עכבר, מקלדת, בקשות ציור מחדש, טיימרים וכן הלאה) ופועל לפיהן.3
לכל תהליכון שיוצר חלון יש תור הודעות ומריץ לולאת הודעות כזו.
MSG msg;
while (GetMessage(&msg, NULL, 0, 0) > 0) {
TranslateMessage(&msg);
DispatchMessage(&msg); // the window procedure is called
}
GetMessage שולף הודעה מהתור, ו-DispatchMessage קורא לפרוצדורת החלון של אותו חלון (פונקציית הטיפול בהודעות). טיפול בלחיצת כפתור, ציור מחדש, ומטפלי אירועים של WinForms או WPF כולם, כשמפרקים אותם, רצים בתוך איטרציה אחת של הלולאה הזו.5
flowchart TB
accTitle: המבנה הבסיסי של לולאת ההודעות
accDescr: מערכת ההפעלה שמה עכבר, מקלדת וקלט אחר בתור ההודעות של התהליכון; לולאת תהליכון ה-UI שולפת עם GetMessage, קוראת לפרוצדורת החלון עם DispatchMessage, וחוזרת לראש הלולאה כשהעיבוד מסתיים
os["OS (קלט, בקשות ציור מחדש, טיימרים)"] --> q["תור הודעות התהליכון"]
q --> gm["שליפה עם GetMessage"]
gm --> dm["DispatchMessage"]
dm --> wp["טיפול בפרוצדורת החלון"]
wp --> gm
איור 1: הלב של יישום GUI הוא לולאת ההודעות; כל מטפל אירוע רץ כאיטרציה אחת של הלולאה הזו.
למבנה הזה יש תוצאה חשובה אחת. אם עושים עבודה גוזלת-זמן בתוך פרוצדורת החלון (מטפל אירוע), הלולאה לא יכולה לשלוף את ההודעה הבאה בינתיים. היא לא יכולה להגיב לא ללחיצות ולא לבקשות ציור מחדש — זה מה ש”תקיעה” באמת.
כדאי גם לקבל שהודעות נמסרות בשני נתיבים. PostMessage שם את ההודעה בתור וחוזר מיד, והלולאה שולפת ומעבדת הודעות לפי הסדר. SendMessage, לעומת זאת, קורא לפרוצדורת החלון ישירות ולא חוזר לקורא עד שהעיבוד מסתיים.36 ההבדל הזה נכנס ישר לדיון הקיפאון בפרק 4.
flowchart TB
accTitle: שני נתיבי מסירת הודעות
accDescr: PostMessage שם את ההודעה בתור וחוזר מיד; לולאת ההודעות שולפת ומעבדת לפי הסדר. SendMessage קורא לפרוצדורה ישירות ולא חוזר לקורא עד שהעיבוד מסתיים
pm["PostMessage"] --> q2["שים בתור (חוזר מיד)"]
q2 --> loop["הלולאה שולפת ומעבדת לפי הסדר"]
sm["SendMessage"] --> direct["קריאה ישירה לפרוצדורה"]
direct --> w2["לא חוזר עד שהעיבוד מסתיים"]
איור 2: אף ששניהם “שולחים הודעה”, Post לתור ו-Send שמחכה להשלמה הם טבעות שונות לגמרי.
3. איך “לא מגיב” נשפט — כלל 5 השניות וחלון הרפאים
אז איך מערכת ההפעלה יודעת ש”היישום הזה נתקע”? הקריטריון מתועד רשמית. מערכת ההפעלה מתייחסת לחלון כלא מגיב כשהוא אינו ממתין לקלט, אינו ברצף ההפעלה, ולא קרא ל-PeekMessage (שליפת הודעה) במשך 5 שניות.1 כלומר, מערכת ההפעלה צופה האם “לולאת ההודעות באמת מסתובבת” כמו שלוקחים דופק, ואם אין דופק 5 שניות היא שופטת את החלון כלא מגיב (התיעוד קובע שערך 5 השניות עשוי להשתנות בעתיד). יחידת השיפוט היא החלון ותהליכון ה-GUI שמחזיק אותו; ביישום עם כמה תהליכוני UI, תהליכון אחד שנתקע לא אומר שחלונות על תהליכון אחר מתים. התהליכון להסתכל עליו ב-dump הוא בעל החלון הנתקע.
מה קורה לחלון עליון שנשפט מתועד גם. מערכת ההפעלה מסתירה את החלון המקורי ומחליפה אותו ב”חלון רפאים” עם אותו סדר-Z, מיקום, גודל ומראה. כל מה שהמשתמש יכול לעשות איתו הוא להזיז, לשנות גודל או (בכוח) לסגור. היישום בפנים לא באמת מגיב, ולכן שום פעולה אחרת לא עובדת.2
flowchart TB
accTitle: שיפוט חלון נתקע והחלפת חלון הרפאים
accDescr: כשתהליכון ה-UI חסום על עבודה כבדה ושליפת הודעות נעצרת 5 שניות, מערכת ההפעלה שופטת את החלון כלא מגיב, מסתירה את המקורי, מחליפה בחלון רפאים באותו מראה, ומציעה למשתמש רק הזזה, מזעור וסגירה
busy["תהליכון UI חסום על עבודה כבדה"] --> stop["שליפת הודעות נעצרת"]
stop --> judge{"חלפו 5 שניות?"}
judge -->|"לא"| stop
judge -->|"כן"| ghost["החלפה בחלון רפאים"]
ghost --> u1["הכותרת מציגה (לא מגיב)"]
ghost --> u2["לבן קפוא; רק הזזה וסגירה"]
איור 3: גם הטקסט “לא מגיב” וגם המסך הלבן שייכים לחלון הרפאים שמערכת ההפעלה החליפה, לא ליישום הנתקע.
המחרוזת “(לא מגיב)” שמופיעה בשורת הכותרת, והמראה הלבן-קפוא תחת ערכת Aero, שניהם שייכים לחלון הרפאים הזה. שתי תוצאות מעשיות נובעות מזה.
- עד ש”לא מגיב” מוצג, התהליכון שמחזיק את החלון הזה לא עיבד הודעות לפחות 5 שניות. זה לא ש”התצוגה עלתה מוקדם מדי” — תהליכון ה-UI בהחלט חסום.
- חלון רפאים לא נוצר כשמחובר מנפה שגיאות.2 כשנראה כאילו “זה אף פעם לא הופך ללא-מגיב תחת המנפה, אבל כן ב-release”, התקיעה עצמה יכולה להיות זהה ורק התצוגה שונה.
יש גם API, DisableProcessWindowsGhosting, שמבטל את ההחלפה הזו לכל התהליך.7 הוא מיועד למקרים מיוחדים כמו מסופי קיוסק שבהם לא רוצים שמערכת ההפעלה תגרום לחלון להיראות ניתן להפעלה בעצמה. קריאה אליו עוצרת את תצוגת “לא מגיב” מלהופיע, אבל העובדה שהיישום נתקע לא משתנה. הבינו שזה לא משהו שיישום כללי משתמש בו כנגד-מידה ל”לא מגיב”.
4. למה יישומים נתקעים — דפוסים קלאסיים שחוסמים את תהליכון ה-UI
הסיבה, כשמפרקים אותה, היא נקודה אחת — “תהליכון ה-UI לא חוזר ללולאת ההודעות” — אבל הצורות שפוגשים בפועל נופלות לכמה קלאסיקות.
flowchart TB
accTitle: סיווג הסיבות הקלאסיות שחוסמות את תהליכון ה-UI
accDescr: ארבע המשפחות הקלאסיות — I/O סינכרוני וקריאות רשת, המתנות נעילה, SendMessage בין תהליכונים, ומעורבות COM STA — כולן מתכנסות לאותה נקודה אחת שתהליכון ה-UI לא יכול לחזור ללולאת ההודעות
kind{"איזו סיבה קלאסית?"}
kind --> io{"I/O או נעילה?"}
kind --> other{"SendMessage או COM?"}
io --> c1["I/O סינכרוני ורשת"]
io --> c2["המתנות נעילה"]
other --> c3["SendMessage"]
c3 -.-> c3n["בין תהליכונים"]
other --> c4["מעורבות COM STA"]
c1 --> core["ה-UI לא יכול לחזור"]
c2 --> core
c3 --> core
c4 --> core
core --> ar["בדיקת לא מגיב"]
איור 4: הסימפטום הנראה זהה, אבל האשם שחוסם את התהליכון נופל לארבע משפחות, והנגד-מידה שונה לכל אחת.
I/O סינכרוני וקריאות רשת. זה הנפוץ ביותר. הדפוס של לעשות סינכרונית, בתוך מטפל לחיצת כפתור, קריאה או כתיבה גדולה של קובץ, שאילתת מסד נתונים, קריאת Web API, או גישה לקובץ בכונן רשת. במחשב פיתוח זה מסתיים בשבריר שנייה, אז אף פעם לא שמים לב; השהיית רשת בייצור או תקלה בשרת קבצים הופכת את זה להמתנה של עשרות שניות, ומקבלים כרטיסים ש”זה נתקע מדי פעם”. לכונני רשת יש פקיעות זמן ארוכות כשהחיבור למטה, והן מחמירות את הסימפטום דרמטית.
המתנות נעילה. הדפוס שבו תהליכון ה-UI מנסה לקחת נעילה על נתונים משותפים עם תהליכון עובד, ומסתיים בהמתנה לעובד שמחזיק את הנעילה הזו זמן רב. משמעת נעילות מכוסה בפירוט בסדרת הריבוי-תהליכונים המעשית.
SendMessage בין תהליכונים. SendMessage לא חוזר עד שפרוצדורת חלון היעד סיימה לעבד.6 כששולחים לחלון על תהליכון אחר, השולח מושם להמתין עד שאותו תהליכון במצב שבו הוא יכול לעבד הודעות. אם תהליכון היעד עצמו ממתין למשהו, יש קיפאון הודעות שבו כל צד ממתין לשני.3 שליחה ל-HWND_BROADCAST בפרט תגרור אתכם ברגע שחלון אחד לא מגיב. כשאי אפשר להרשות המתנה, שוקלים SendMessageTimeout או PostMessage, שלא מחכה לתשובה.8
sequenceDiagram
accTitle: קיפאון שנגרם מ-SendMessage בין תהליכונים
accDescr: אם תהליכון עובד שולח SendMessage לחלון של תהליכון UI בזמן שתהליכון ה-UI חסום בהמתנה לתוצאת העובד, כל צד ממתין שהשני יסיים ויש קיפאון
participant U as תהליכון UI
participant W as תהליכון עובד
U->>U: ממתין שהעובד יסיים (חסום)
W->>U: SendMessage (לא חוזר עד שיעובד)
Note over U: לא יכול לעבד הודעות (חסום)
Note over W: לא יכול לחזור מ-SendMessage
Note over U,W: ממתינים זה לזה — קיפאון
איור 5: “תהליכון ה-UI ממתין לעובד, והעובד ממתין לתהליכון ה-UI דרך SendMessage” הוא קיפאון קלאסי.
מעורבות דירת COM. קריאות לאובייקט STA נמסרות כהודעות חלון, ולכן כשתהליכון ה-UI (STA) חסום, קריאות COM מתהליכונים אחרים נחסמות גם כנזק נלווה. המבנה הזה מוסבר במאמר COM STA/MTA.
הצטברות של “זה רק רגע”. גם קריאה סינכרונית של 50 מ״ש, שנקראת 100 פעמים בלולאה, היא 5 שניות. סף לא-מגיב הוא 5 שניות, אבל “כבדות” נתפסת מתחילה סביב 100 מ״ש. כלל אצבע תכנוני הוא “תהליכון ה-UI מותר לחסום רק למילישניות”.
5. תכנונים שלא נתקעים — העברת עבודה כבדה מחוץ לתהליכון ה-UI
עיקרון התכנון הוא דבר אחד: מעבירים עבודה גוזלת-זמן מחוץ לתהליכון ה-UI. ב-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 ממתין, תהליכון ה-UI חזר ללולאת ההודעות, ולכן לא הולכים ללא-מגיב. שנית, ההמשך אחרי await חוזר לתהליכון ה-UI, כך שאפשר לגעת בפקדים כרגיל אחר כך (מגע ישיר בפקד מתהליכון עובד אסור; אם צריך, משתמשים ב-Control.Invoke / Dispatcher.InvokeAsync).4 שלישית, משביתים את הכפתור בזמן שהעבודה רצה, ואחרת הורגים reentrancy בתכנון.
התמונה זהה ב-Win32 מקורי: מוסרים את העבודה לתהליכון עובד, מודיעים לתהליכון ה-UI על השלמה כהודעה מותאמת דרך PostMessage, ומעדכנים את ה-UI בפרוצדורת החלון. PostMessage רק שם את ההודעה בתור וחוזר מיד, כך שגם צד העובד לא חסום.6 כותבים את ההמתנה לתהליכון העובד עצמו עם המשמעת שמכוסה במאמר משתני התנאי.
flowchart TB
accTitle: חלוקת תפקידים ביישום שלא נתקע
accDescr: תהליכון ה-UI אחראי רק לקבלת קלט, הצגת התקדמות וקבלת ביטול; תהליכון עובד מריץ את העבודה הכבדה ומחזיר השלמה לתהליכון ה-UI דרך PostMessage או המשך await
ui["תהליכון UI: קלט, התקדמות, ביטול"] -->|"מסירת העבודה"| w["תהליכון עובד: עבודה כבדה"]
w -->|"PostMessage / המשך await"| ui
ui -.-> ng["אין I/O סינכרוני או חישוב ארוך על תהליכון ה-UI"]
איור 6: משאירים את תהליכון ה-UI כ”דלפק קבלה”, תמיד מוסרים עבודה כבדה לעובד, ולוקחים רק את הודעת ההשלמה.
מה שרוצים להימנע ממנו הוא הטכניקה של להכניס Application.DoEvents() או לולאת PeekMessage בין גושי עבודה כבדה רק כדי להשאיר את התצוגה חיה. מתחמקים מלא-מגיב, אבל מטפלי אירוע שרירותיים נכנסים מחדש באמצע העבודה. לחיצה שנייה על הכפתור, סגירת הטופס במהלך עיבוד, ירי טיימר — כל אחד מהם יכול לקלקל נתונים שעדיין מעובדים, והבאגים תלויי-תזמון וקשים לשחזור. משאירים שאיבה ידנית של לולאת ההודעות בתוך מבנה מוגבל כמו דיאלוג התקדמות מודאלי, וככלל פותרים בהפרדה.
sequenceDiagram
accTitle: ציר הזמן של באג reentrancy שנגרם מ-DoEvents
accDescr: קריאה ל-DoEvents באמצע עבודה כבדה נותנת למטפל אירוע של לחיצה שבתור להפריע ולרוץ, לכתוב מחדש נתונים שעדיין מעובדים, ואז להמשיך את העבודה המקורית, ומייצרת קלקול נתונים תלוי-תזמון
participant U as תהליכון UI
U->>U: עבודה כבדה מתחילה (נתונים בעיבוד)
U->>U: DoEvents (עיבוד הודעות שבתור)
Note over U: מטפל הלחיצה החוזרת מפריע
U->>U: העבודה המפריעה כותבת מחדש את הנתונים
U->>U: העבודה המקורית ממשיכה (הנתונים כבר לא עקביים)
איור 7: DoEvents מוחק “לא מגיב” בתמורה להזמנת אירועים שרירותיים לאמצע העבודה.
לעבודה ארוכה, כוללים גם תצוגת התקדמות וביטול בתכנון. שולחים התקדמות ל-UI עם IProgress<T> ומתקשרים הפרעה עם CancellationToken, והמשתמש יכול לראות ש”זה עובד” ולא יגיע לסיום כפוי (שלעיתים קרובות הוא סיבה לקלקול נתונים).
flowchart TB
accTitle: זרימת התקדמות וביטול לעבודה ארוכה
accDescr: תהליכון העובד שולח התקדמות לתהליכון ה-UI דרך IProgress; פעולת ביטול ב-UI מגיעה לעובד דרך CancellationToken; העובד עוצר בגבול נוח ומנקה
w3["עובד: עבודה ארוכה"] -->|"התקדמות דרך IProgress"| ui2["UI: התקדמות וכפתור עצירה"]
ui2 -->|"CancellationToken"| w3
w3 --> stop2["עצירה בגבול וניקוי"]
איור 8: התקדמות היא “עובד → UI”; ביטול הוא “UI → עובד”. כוללים את הערוץ הדו-כיווני הדק הזה בתכנון מההתחלה.
6. חקירת רגע התקיעה
בחקירה של “זה נתקע מדי פעם”, הדבר היקר ביותר הוא מצב התהליכון ברגע המדויק שהוא נתקע. אתחול מחדש — והראיות נעלמות.
לוקחים dump. בלשונית הפרטים של מנהל המשימות, לחיצה ימנית על תהליך היעד → “Create dump file”. זה לבד נותן dump מלא עם המחסנית של כל תהליכון. עצם האמירה לאנשי ה-IT שמקבלים את הכרטיסים “כשזה נתקע, קחו את זה לפני שסוגרים” משנה מאוד את שיעור ההצלחה של החקירה. לבניית מנגנון איסוף, ראו את מאמר איסוף crash dump.
מסתכלים על המחסנית של תהליכון ה-UI. פותחים את ה-dump ב-WinDbg ומסתכלים על המחסנית של התהליכון ששואב את לולאת ההודעות (בדרך כלל תהליכון 0). I/O סינכרוני מופיע כ-ReadFile או API רשת, המתנת נעילה כקריאת משפחת WaitFor…, ו-SendMessage בין תהליכונים כהמתנה בתוך SendMessage — כפי שהוא. איך לקרוא את זה מוסבר במאמר ה-WinDbg המבוא.
מסתכלים בחי. עם Process Explorer אפשר לבדוק את רשימת התהליכונים והמחסניות במקום. כשזה כבד באופן יציב, לוקחים עקבות WPR ומנתחים את המתנות תהליכון ה-UI לאורך זמן (WPR/WPA בפועל).
flowchart TB
accTitle: הליך בסיסי לחקירת לא מגיב
accDescr: לוקחים dump ברגע התקיעה, מסתכלים על המחסנית של תהליכון ה-UI, מזהים האם הוא נעצר על I/O סינכרוני, המתנת נעילה או SendMessage בין תהליכונים, ומחברים את זה לתיקון התכנון המתאים
hang["רגע התקיעה"] --> dump["לוקחים dump (לפני סגירה)"]
dump --> stack["מסתכלים על המחסנית של תהליכון ה-UI"]
stack --> io["I/O סינכרוני או המתנת רשת"]
stack --> lock["המתנת נעילה"]
stack --> sm["SendMessage בין תהליכונים"]
io -.-> fix["מפרידים את האתר לעובד"]
lock -.-> fix
sm -.-> fix
איור 9: כוכב החקירה הוא “dump של רגע התקיעה”; המחסנית של תהליכון ה-UI עצמה היא סיווג הסיבה.
אפשר גם לתקנן איך לוקחים חיתוך ראשון מהסימפטום. אם זה תמיד נתקע בפעולה מסוימת, קודם חושדים ב-I/O סינכרוני בתוך אותו מטפל. אם זה נתקע לעיתים נדירות ובלי מתאם לפעולה, חושדים בסדר נעילות או בקיפאון SendMessage בין תהליכונים, ומצמידים את יעדי ההמתנה של שני התהליכונים ב-dump. אם זה נתקע רק בסביבה מסוימת, חושדים בפקיעות זמן מגורמים סביבתיים כמו כונן רשת, פרוקסי או תוכנת אנטי-וירוס.
7. סיכום
- “לא מגיב” הוא מנגנון שבו מערכת ההפעלה שופטת שיישום לא שלף הודעה 5 שניות ומחליפה בחלון רפאים. הדבר ששם את התצוגה הוא מערכת ההפעלה, לא היישום.
- סיבת התקיעה היא נקודה אחת: “תהליכון ה-UI לא יכול לחזור ללולאת ההודעות”. I/O סינכרוני, הרשת, המתנות נעילה, ו-SendMessage בין תהליכונים הם הקלאסיקה.
- הנגד-מידה היא להעביר עבודה כבדה מחוץ לתהליכון ה-UI. ב-C#,
async/await+Task.Run; ב-Win32, תהליכון עובד +PostMessage. מונעים reentrancy בזמן ביצוע בתכנון, כמו השבתת הכפתור. - התחמקות עם
DoEventsהיא בתמורה לבאגי reentrancy.DisableProcessWindowsGhostingרק מסיר את התצוגה. אף אחד מהם אינו תיקון שורש. - לחקירה, dump של “רגע התקיעה” הוא הדבר החשוב ביותר. הסיבה כמעט תמיד כתובה על המחסנית של תהליכון ה-UI כפי שהיא.
מנקודת מבט המשתמש “לא מגיב” הוא “זה שבור”, אבל ברגע שמכירים את המנגנון אפשר לתרגם אותו למשפט המדויק “תהליכון ה-UI לא חזר 5 שניות”. עבודה אחורה מהמשפט האחד הזה, המועמדים לסיבה, התיקון והליך החקירה כולם נופלים באופן טבעי.
מאמרים קשורים
- התעוררויות ספונטניות — למה משתני תנאי מתעוררים “בלי שהודיעו להם” ואיך להמתין נכון ב-Windows
- שיטות מומלצות לריבוי תהליכונים בפועל: מהדורת .NET — מה להחליט לפני שמוסיפים עוד תהליכונים
- יסודות COM STA/MTA — מודלי תהליכונים ואיך להימנע מתקיעות
- קריאת crash dumps עם WinDbg + SOS — מדריך מעשי לניתוח אחרי איסוף
- Process Explorer / Handle / VMMap בפועל — מרדף אחרי תקיעות, דליפות ו”קובץ בשימוש” מהמצב עכשיו
- כיבוי Windows כפי שנראה מהיישום — לשרוד נכון הודעות יציאה, אתחולים ואובדן חשמל
תחומי ייעוץ קשורים
KomuraSoft LLC מטפלת בחקירות שורש של יישומים עסקיים ש”נתקעים מדי פעם” או הופכים ל”לא מגיב” (ניתוח dump וניתוח עקבות), ברפקטור של קוד UI ישן מלא בעבודה סינכרונית להפרדת async/await ותהליכון עובד, ובסקירות של תכנוני UI שלא קופאים. גם כשעדיין אין הליך שחזור, אפשר לעזור החל מתכנון איך לאסוף ראיות.
קישורים
-
Microsoft Learn, IsHungAppWindow function (winuser.h). על קריטריון השיפוט שיישום מטופל כלא מגיב כשהוא “אינו ממתין לקלט, אינו ברצף ההפעלה, ולא קרא ל-PeekMessage במשך פקיעת הזמן הפנימית של 5 שניות”; על כך שקריטריון 5 השניות נתון לשינוי; ועל כך שהפונקציה תמיד מחזירה TRUE לחלון רפאים. ↩ ↩2
-
Microsoft Learn, GetMessage function (winuser.h). על כך שהמערכת מתייחסת לחלון עליון כלא מגיב כשהוא מפסיק להגיב להודעות כמה שניות ומחליפה אותו בחלון רפאים באותו סדר-Z, מיקום, גודל ומראה; על כך שהמשתמש יכול רק להזיז, לשנות גודל או לסגור; ועל כך שחלון רפאים לא נוצר כשמחובר מנפה שגיאות. ↩ ↩2 ↩3
-
Microsoft Learn, About Messages and Message Queues. על כך שיישומי Windows מונעי-אירועים ופרוצדורת החלון מעבדת הודעות; על ההבחנה בין הודעות בתור להודעות שנשלחות ישירות; על החלפת חלון לא-מגיב בחלון רפאים; ועל הסעיף שמכסה קיפאון מתהליכונים ששולחים הודעות זה לזה. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, How to make thread-safe calls to controls (Windows Forms). על כך שפקדי WinForms אינם בטוחים למגע מתהליכון אחר מזה שיצר אותם; על שימוש ב-Invoke/BeginInvoke לעדכונים מתהליכון אחר; ועל דפוסים אסינכרוניים בטוחים עם async/await או BackgroundWorker. ↩ ↩2
-
Microsoft Learn, Using Messages and Message Queues. על מימוש טיפוסי של לולאת הודעות עם GetMessage, TranslateMessage ו-DispatchMessage, ועל איך לבדוק תור הודעות. ↩
-
Microsoft Learn, SendMessage function (winuser.h). על כך ש-SendMessage קורא לפרוצדורת החלון שצוינה ולא חוזר עד שהעיבוד מסתיים; על כך ששליחה לחלון על תהליכון אחר גורמת לשולח להמתין עד שאותו תהליכון מעבד את ההודעה; ועל ההבדל מ-PostMessage, ששם את ההודעה בתור בלי לחכות לתשובה. ↩ ↩2 ↩3
-
Microsoft Learn, DisableProcessWindowsGhosting function (winuser.h). על היכולת לבטל, לתהליך ה-GUI הקורא, את תכונת חלון הרפאים שהופכת חלון לא-מגיב לניתן למזעור, הזזה וסגירה; ועל כך שהביטול נמשך לכל חיי התהליך. ↩
-
Microsoft Learn, SendMessageTimeout function (winuser.h). על היכולת לשלוח הודעה עם פקיעת זמן; ועל דגל (SMTO_ABORTIFHUNG) שחוזר בלי לחכות כשהחלון לא מגיב (נשפט כנתקע). ↩
מאמרים קשורים
מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.
DllMain ונעילת הטוען — הסיבה האמיתית שאומרים לכם "לא לעשות כלום באתחול DLL"
למה אסור לקרוא ל-LoadLibrary או להסתנכרן עם תהליכונים אחרים מתוך DllMain. המאמר מסביר ממקורות ראשוניים איך נעילת הטוען מסדרת כל הודעת DLL...
Win32 Thread Pool API — מקביליות בלי ליצור תהליכונים, דרך CreateThreadpoolWork
מפזרים קריאות CreateThread בכל הקוד הנייטיב? המאמר מסביר את ה-API של מאגר התהליכונים של Win32 שעוצב מחדש ב-Vista — ארבעת האובייקטים work,...
יישומים שנשברים בחזרה משינה — איך אירועי צריכת החשמל של Windows עובדים ואיך לבנות יישומים עסקיים ששורדים אותם
פתחתם את המחשב הנייד והחיבורים של היישום העסקי היו מתים — הסיבה היא תכנון שלא חשב על שינה. המאמר מכסה את זרימת ההודעות WM_POWERBROADCAST,...
התעוררויות ספונטניות — למה משתני תנאי מתעוררים "בלי שהודיעו להם" ואיך להמתין נכון ב-Windows
המתנה על משתנה תנאי יכולה לחזור גם כשלא הגיעה הודעה (התעוררות ספונטנית). המאמר מסביר ממימוש Windows למה המפרט מתיר זאת, ומציג את צורת ההמ...
איך לוח ההעתקה וגרירה ושחרור עובדים — טיפול נכון בהעברת נתוני OLE ביישומים עסקיים
מדביקים טבלת Excel והעיצוב מתפרק; סוגרים את יישום המקור ואי אפשר יותר להדביק — שניהם באים מכך שהלוח שם את אותו תוכן בכמה פורמטים בבת אחת....
נושאים קשורים
העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.
נושאים טכניים ב-Windows
שער לנושאי פיתוח Windows, חקירת תקלות וניצול נכסים קיימים.
תהליכון ה-UI וטיימרים
תהליכון ה-UI של WPF / WinForms, זרימות אסינכרוניות, Dispatcher ותכנון טיימרים.
שירותים הקשורים לנושא הזה
המאמר קשור ישירות לשירותים הבאים.
פיתוח יישומי Windows
יישומים עסקיים, חיבור התקנים וכלי תקשורת, מהגדרת הדרישות ועד הפיתוח.
שאלות נפוצות
שאלות נפוצות בפניות בנושא המאמר.
- באילו תנאים מופיע "לא מגיב"?
- מערכת ההפעלה שופטת חלון כנתקע כשיישום עם חלון אינו ממתין לקלט, אינו ברצף ההפעלה, ולא שלף הודעה (PeekMessage) במשך 5 שניות. החלון העליון הנתקע מוסתר ומוחלף ב"חלון רפאים" באותו מיקום, גודל ומראה. טקסט שורת הכותרת "(לא מגיב)" והמראה הלבן-קפוא שייכים לחלון הרפאים הזה, שמאפשר רק להזיז, למזער או לסגור. כלומר, "לא מגיב" אינו משהו שהיישום עצמו מציג — זו מסך שמערכת ההפעלה שמה בשמו.
- יש הגדרה שמונעת מ"לא מגיב" להופיע בזמן שיש עבודה בתהליך?
- קריאה ל-DisableProcessWindowsGhosting מבטלת את ההחלפה לחלון רפאים לתהליך הזה. זה רק הופך את התקיעה לפחות נראית למשתמש — החלון עדיין לא מגיב לקלט, ומנקודת מבט המשתמש זו הקפאה מלאה בלי דרך להזיז או לסגור. התיקון האמיתי אינו דיכוי התצוגה, אלא העברת עבודה כבדה לתהליכון עובד כדי שתהליכון ה-UI לעולם לא ייחסם אפילו לעשירית שנייה, שלא לדבר על חמש. שימו לב גם שמערכת ההפעלה לא יוצרת חלון רפאים כשמחובר מנפה שגיאות, כך שיכול להיראות כאילו "לא מגיב" אף פעם לא קורה בניפוי.
- מקובל להימנע מ"לא מגיב" עם DoEvents (שאיבת לולאת ההודעות ידנית)?
- לא מומלץ. סיבוב DoEvents או לולאת PeekMessage באמצע עבודה כבדה יתחמק משיפוט החלון הנתקע, אבל כל מטפל אירוע יכול אז להיכנס מחדש — לחיצה שנייה על הכפתור, סגירת החלון, טיימר וכן הלאה. מטפל אחר שכותב מחדש נתונים שעדיין מעובדים, או נוגע בטופס שאמור היה להיסגר וזורק, מייצר באגי reentrancy שתלויים בתזמון וקשים לשחזור — גרוע מ"לא מגיב" עצמו. הגישה הנכונה היא להעביר את העבודה עצמה לתהליכון עובד עם Task.Run או דומה, ולהשאיר את תהליכון ה-UI אחראי רק לתצוגת התקדמות ולקבלת ביטול.
- איך מעדכנים את ה-UI (פקדים) מתהליכון עובד?
- פקדי WinForms ורכיבי WPF מותרים למגע רק מהתהליכון שיצר אותם (בדרך כלל תהליכון ה-UI). מגע ישיר מתהליכון עובד גורם לחריגות או להתנהגות לא מוגדרת. ב-C#, async/await הוא הנתיב הקל: ההמשך אחרי await חוזר לתהליכון ה-UI הקורא, כך שאפשר לעדכן פקדים כרגיל אחרי ה-await. למעבר מפורש, משתמשים ב-Control.Invoke/BeginInvoke ב-WinForms וב-Dispatcher.InvokeAsync ב-WPF. ב-Win32 מקורי, הדפוס המקובל הוא שתהליכון העובד שולח PostMessage של הודעת השלמה מותאמת לתהליכון ה-UI, ופרוצדורת החלון מעדכנת את ה-UI.
- איך חוקרים למה יישום מציג "לא מגיב"?
- הדבר החשוב הוא ללכוד מצב ב"רגע עצמו" של התקיעה. קודם לוקחים dump מלא מלשונית הפרטים של מנהל המשימות עם "Create dump file", ואז ב-WinDbg מסתכלים על המחסנית של תהליכון ה-UI (התהליכון שמריץ את לולאת ההודעות). האם הוא תקוע ב-I/O סינכרוני, בהמתנת רשת, בהמתנת נעילה, או בהמתנה לתהליכון אחר דרך SendMessage — זה מופיע במחסנית כפי שהוא. לתהליך חי, רשימת התהליכונים ומבט המחסנית של Process Explorer שימושיים; למעקב לאורך זמן, לכידת עקבות WPR יעילה. ראו גם את מאמר ה-WinDbg המבוא באתר, Process Explorer בפועל, ו-WPR/WPA בפועל.