「操作到一半應用程式變白,顯示(沒有回應)。」「我們接到偶爾卡住的單據,但開發機上從來重現不了。」── 對 Windows 業務應用程式來說,這個「沒有回應」是最常見的抱怨之一。而且出奇少人知道:把「沒有回應」顯示放上去的不是卡住的應用程式本身──是作業系統。
Windows 怎麼知道應用程式「卡住了」?那個結霜白的視窗是什麼?本文以在 Windows 上撰寫業務應用程式的開發者、以及承接應用程式卡住單據的 IT 人員為對象,從訊息迴圈的基礎走完「沒有回應」判定,並依一次資訊整理無回應的經典原因、不卡住的設計,以及調查卡住當下的程序。
1. 先講結論
- 「沒有回應」是作業系統的判定。當視窗(以及擁有它的 GUI 執行緒)沒有在等輸入、不在啟動序列中,且 5 秒沒有取出訊息(
PeekMessage)時,作業系統把它當成沒有回應。判定不是每個處理程序一次。1 - 變白的視窗是「幽靈視窗」。作業系統隱藏了原本的視窗,換進相同位置、大小與外觀的假貨。你能做的只有移動、最小化或關閉;內容並沒有在跑。附加了偵錯工具時不會建立幽靈視窗。2
- 無回應的原因幾乎總是收斂成一件事。本該幫浦訊息迴圈的 UI 執行緒被重工作或等待擋住。同步 I/O、網路呼叫、鎖等待,以及跨執行緒的
SendMessage是經典。3 - 設計原則是「不要在 UI 執行緒上等待或計算」。把重工作移到工作者執行緒(C# 用
async/await+Task.Run),讓 UI 執行緒專心繪製、進度與接受取消。4 DoEvents與手動幫浦訊息迴圈是重入錯誤的溫床。「沒有回應」顯示消失了,但結構現在允許任意事件在工作中途插入。正確做法是分離,不是躲避。- 調查從捕捉卡住當下的狀態開始。取傾印並看 UI 執行緒的堆疊,幾乎總能找出它在等什麼。
2. 前提:Windows 應用程式由訊息驅動
要理解「沒有回應」,先要接受 Windows GUI 應用程式是事件驅動的。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 秒規則與幽靈視窗
那麼作業系統怎麼知道「這個應用程式卡住了」?準則有官方記載。當視窗沒有在等輸入、不在啟動序列中,且 5 秒沒有呼叫 PeekMessage(取出訊息)時,作業系統把它當成沒有回應。1 換句話說,作業系統像把脈一樣看「訊息迴圈是否真的在轉」,5 秒沒有脈搏就判定視窗沒有回應(文件寫明這個 5 秒值未來可能改變)。判定單位是視窗與擁有它的 GUI 執行緒;有數條 UI 執行緒的應用程式裡,一條卡住不代表另一條上的視窗死了。傾印裡該看的執行緒是卡住視窗的擁有者。
被判定的頂層視窗接下來發生什麼,也有記載。作業系統隱藏原本的視窗,換成具有相同 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 當看起來像「偵錯工具下從來不會沒有回應,發行版卻會」時,卡住本身可以相同,只有顯示不同。
還有一個 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: 若工作者執行緒向 UI 執行緒視窗送出 SendMessage,而 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 執行緒等工作者,工作者經由 SendMessage 等 UI 執行緒」是經典死結。
COM 套間牽連。對 STA 物件的呼叫以視窗訊息傳遞,因此 UI 執行緒(STA)被擋住時,其他執行緒的 COM 呼叫也會連帶被擋住。那個結構在 COM STA/MTA 文章裡說明。
「只是一瞬間」的堆積。即使是 50 ms 的同步呼叫,在迴圈裡叫 100 次就是 5 秒。沒有回應的門檻是 5 秒,但體感「遲鈍」大約從 100 ms 開始。設計經驗法則是「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 第三,工作執行期間停用按鈕,並以其他方式從設計上殺死重入。
原生 Win32 的畫面相同:把工作交給工作者執行緒,用 PostMessage 以自訂訊息通知 UI 執行緒完成,在視窗程序裡更新 UI。PostMessage 只把訊息放上佇列並立刻返回,因此工作者側也不被擋住。6 等待工作者執行緒本身,用條件變數文章所談的紀律來寫。
flowchart TB
accTitle: 不卡住應用程式裡的角色分工
accDescr: UI 執行緒只負責接受輸入、顯示進度與接受取消;工作者執行緒跑重工作,並經由 PostMessage 或 await 接續把完成交回 UI 執行緒
ui["UI 執行緒:輸入、進度、取消"] -->|"交出工作"| w["工作者執行緒:重工作"]
w -->|"PostMessage / await 接續"| ui
ui -.-> ng["UI 執行緒上沒有同步 I/O 或長計算"]
圖 6: 把 UI 執行緒當「櫃檯」,重工作一律交給工作者,只收下完成通知。
要避開的技巧,是在重工作區塊之間插入 Application.DoEvents() 或 PeekMessage 迴圈,只為了讓顯示活著。你躲過了沒有回應,但任意事件處理常式會在工作中途重入。第二次按按鈕、處理期間關閉表單、計時器觸發──任何一個都可能弄壞仍在處理的資料,而且錯誤依賴時機、難以重現。把手動幫浦訊息迴圈留在強制回應進度對話方塊這類有限結構裡,原則上用分離來解決。
sequenceDiagram
accTitle: DoEvents 造成重入錯誤的時間軸
accDescr: 在重工作中途呼叫 DoEvents,讓已排入佇列的點擊之事件處理常式插入並執行、改寫仍在處理的資料,然後原本的工作再繼續,造成依賴時機的資料損壞
participant U as UI 執行緒
U->>U: 重工作開始(資料正在處理)
U->>U: DoEvents(處理已排入的訊息)
Note over U: 按鈕再按的處理常式插入
U->>U: 插入的工作改寫資料
U->>U: 原本的工作繼續(資料已不一致)
圖 7: DoEvents 抹掉「沒有回應」,代價是邀請任意事件進入工作中途。
對長時間工作,設計裡也要包含進度顯示與取消。用 IProgress<T> 把進度送給 UI,用 CancellationToken 傳達中斷,使用者就能看見「它在做事」,不會伸手去強制結束(那常常是資料損壞的原因)。
flowchart TB
accTitle: 長時間工作的進度與取消流程
accDescr: 工作者執行緒經由 IProgress 把進度送給 UI 執行緒;UI 上的取消動作經由 CancellationToken 到達工作者;工作者在方便的邊界停下並清理
w3["工作者:長時間工作"] -->|"經由 IProgress 的進度"| ui2["UI:進度與停止按鈕"]
ui2 -->|"CancellationToken"| w3
w3 --> stop2["在邊界停下並清理"]
圖 8: 進度是「工作者 → UI」;取消是「UI → 工作者」。從一開始就把這條細的雙向通道寫進設計。
6. 調查卡住的當下
調查「偶爾卡住」時,最有價值的是正好卡住那一刻的執行緒狀態。重開,證據就沒了。
取傾印。在工作管理員的詳細資料索引標籤,對目標處理程序按右鍵 →「建立傾印檔」。光這樣就得到帶有每條執行緒堆疊的完整傾印。只要告訴承接單據的 IT 人員「卡住時,關掉之前先做這個」,調查成功率就差很多。建立收集機制見當機傾印收集文章。
看 UI 執行緒的堆疊。在 WinDbg 打開傾印,看正在幫浦訊息迴圈的執行緒(通常是執行緒 0)的堆疊。同步 I/O 顯示為 ReadFile 或網路 API,鎖等待顯示為 WaitFor… 家族呼叫,跨執行緒 SendMessage 顯示為在 SendMessage 裡等待──原樣如此。怎麼讀在 WinDbg 入門文章裡說明。
即時看。用 Process Explorer 可以當場檢查執行緒清單與堆疊。持續遲鈍時,取 WPR 追蹤並分析 UI 執行緒隨時間的等待(WPR/WPA 實戰)。
flowchart TB
accTitle: 調查沒有回應的基本程序
accDescr: 在卡住當下取傾印,看 UI 執行緒的堆疊,辨識它是停在同步 I/O、鎖等待,還是跨執行緒 SendMessage,並連到對應的設計修正
hang["卡住的當下"] --> dump["取傾印(關掉之前)"]
dump --> stack["看 UI 執行緒的堆疊"]
stack --> io["同步 I/O 或網路等待"]
stack --> lock["鎖等待"]
stack --> sm["跨執行緒 SendMessage"]
io -.-> fix["把該處分離到工作者"]
lock -.-> fix
sm -.-> fix
圖 9: 調查的主角是「卡住當下的傾印」;UI 執行緒的堆疊本身就是原因分類。
也可以把從症狀做第一次切割的方式標準化。若某個操作一定卡住,先懷疑該處理常式裡的同步 I/O。若很少卡住且與操作無關,懷疑鎖順序或跨執行緒的 SendMessage 死結,並在傾印裡對上兩條執行緒的等待目標。若只有某個環境卡住,懷疑網路磁碟機、Proxy 或防毒軟體這類環境因素造成的逾時。
7. 總結
- 「沒有回應」是作業系統判定應用程式 5 秒沒有取出訊息後換進幽靈視窗的機制。把顯示放上去的是作業系統,不是應用程式。
- 卡住的原因是單一一點:「UI 執行緒無法回到訊息迴圈」。同步 I/O、網路、鎖等待,以及跨執行緒 SendMessage 是經典。
- 對策是把重工作移出 UI 執行緒。C# 用
async/await+Task.Run;Win32 用工作者執行緒 +PostMessage。執行期間用停用按鈕等方式從設計上防止重入。 - 用
DoEvents躲避,換來的是重入錯誤。DisableProcessWindowsGhosting只拿掉顯示。兩者都不是根本修正。 - 調查時,「卡住當下」的傾印最重要。原因幾乎總是原樣寫在 UI 執行緒的堆疊上。
從使用者角度看「沒有回應」是「壞掉了」,但一旦知道機制,就能把它翻譯成精確的句子「UI 執行緒 5 秒沒回來」。從那一句往回推,候選原因、修正與調查程序都會自然掉出來。
相關文章
- 虛假喚醒 ── 條件變數為何「沒被通知也會醒來」,以及 Windows 上正確的等待方式
- 多執行緒的實務最佳實踐 .NET篇 ── 在增加執行緒之前先決定的事
- COM STA/MTA 基礎 - 執行緒模型與避免 Hang 的思考方式
- 用 WinDbg + SOS 解讀當機傾印檔 ── 收集之後的實務分析入門
- Process Explorer / Handle / VMMap 實戰 ── 從「此刻」的狀態追查無回應、洩漏、「檔案使用中」
- Windows Shutdown as Seen from Your App — Surviving Exit Notifications, Restarts, and Power Loss Correctly
相關諮詢領域
小村軟體有限公司承接「偶爾卡住」或變成「沒有回應」的業務應用程式根本原因調查(傾印分析與追蹤分析)、把充滿同步工作的舊 UI 程式碼重構成 async/await 與工作者執行緒分離,以及不凍結的 UI 設計審查。即使還沒有重現程序,也可以從如何收集證據的設計開始幫忙。
參考連結
-
Microsoft Learn, IsHungAppWindow function (winuser.h). 關於判定準則:應用程式「沒有在等輸入、不在啟動序列中,且內部逾時 5 秒沒有呼叫 PeekMessage」時被當成沒有回應;關於這個 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 初始化什麼都別做」真正的理由
為什麼不能從 DllMain 呼叫 LoadLibrary,也不能和其他執行緒同步。本文依一次資訊說明載入器鎖定如何序列化每個 DLL 通知、結構上必定成立的死結場景、延遲初始化的正確設計,以及無回應的調查方式。
委外・委託開發 Windows 應用程式前該整理的事項
在委外・委託開發 Windows 應用程式之前,整理既有軟體改版、設備整合、COM/ActiveX、發布與更新、維護等應留意的重點。
Win32 執行緒集區 API ── 用 CreateThreadpoolWork 做「不自己建立執行緒」的並行
原生程式碼裡到處呼叫 CreateThread 嗎?本文依一次資訊解說 Vista 重新設計的 Win32 執行緒集區 API──work、timer、wait、io 四種物件、清理群組,以及回呼裡不該做的事。
從睡眠恢復就壞掉的應用程式 ── Windows 電源事件的機制,以及耐得住恢復的業務應用寫法
打開筆電,業務應用程式的連線卻斷了──原因是設計從未把睡眠算進去。本文依一次資訊整理 WM_POWERBROADCAST 的通知流程、Modern Standby 的行為、斷線/重連設計、睡眠抑制,以及調查指令。
虛假喚醒 ── 條件變數為何「沒被通知也會醒來」,以及 Windows 上正確的等待方式
條件變數的等待即使沒有通知到來也可能返回(虛假喚醒)。本文從 Windows 實作說明規格為何允許它,並展示 Win32、C++ 與 C# 裡用 while 迴圈與述語正確等待的寫法。
相關主題
與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。
Windows 技術主題
彙整 KomuraSoft LLC 關於 Windows 開發、故障調查與既有資產活用文章的主題中心。
UI 執行緒 & 計時器
整理 WPF / WinForms UI 執行緒、非同步流程、Dispatcher 使用、計時器判斷的主題頁面。
故障調查 & 長期運行故障
整理間歇性故障、通訊診斷、長期運行當機、失敗路徑測試基礎的主題頁面。
與本主題相關的服務
本文連結到以下服務頁面,歡迎從最接近的入口查看。
Windows 應用程式開發
支援包含常駐處理、設備連動、運作日誌與可維護結構的 Windows 桌面應用程式。
故障調查 & 根本原因分析
調查難以重現的故障、長時間執行後的問題、記憶體洩漏、通訊停滯等棘手的正式環境問題。
常見問題
整理諮詢這個主題時常見的問題。
- 什麼條件下會出現「沒有回應」?
- 當有視窗的應用程式沒有在等輸入、不在啟動序列中,且 5 秒沒有取出訊息(PeekMessage)時,作業系統判定該視窗無回應。被判定無回應的頂層視窗會被隱藏,換成相同位置、大小與外觀的「幽靈視窗」。「(沒有回應)」標題列文字與結霜白的外觀屬於這個幽靈視窗,它只讓你移動、最小化或關閉。換句話說,「沒有回應」不是應用程式自己顯示的──是作業系統替應用程式放上去的畫面。
- 有沒有設定能在工作進行中不讓「沒有回應」出現?
- 呼叫 DisableProcessWindowsGhosting 會對該處理程序停用換成幽靈視窗。不過那只是讓無回應比較不被使用者看見──視窗仍不回應輸入,從使用者角度看是完全凍結、無法移動或關閉。真正的修正不是抑制顯示,而是把重工作移到工作者執行緒,讓 UI 執行緒連十分之一秒都不要被擋,更別說五秒。另外注意,附加了偵錯工具時作業系統不會建立幽靈視窗,因此偵錯期間看起來可能「從來不會沒有回應」。
- 用 DoEvents(手動幫浦訊息迴圈)來避開「沒有回應」可以嗎?
- 不建議。在重工作中間轉動 DoEvents 或 PeekMessage 迴圈會躲過無回應視窗判定,但任何事件處理常式都可能重入──第二次按按鈕、關閉視窗、計時器等等。另一個處理常式改寫仍在處理的資料,或碰到本該關閉的表單而丟出例外,會產生依賴時機、難以重現的重入錯誤──比「沒有回應」本身更糟。正確做法是用 Task.Run 等把工作本身移到工作者執行緒,讓 UI 執行緒只負責進度顯示與接受取消。
- 要怎麼從工作者執行緒更新 UI(控制項)?
- WinForms 控制項與 WPF 元素只能從建立它們的執行緒(通常是 UI 執行緒)碰觸。從工作者執行緒直接碰會造成例外或未定義行為。在 C# 裡,async/await 是最輕鬆的路:await 之後的接續回到呼叫端 UI 執行緒,因此 await 之後可以正常更新控制項。要明確切換時,WinForms 用 Control.Invoke/BeginInvoke,WPF 用 Dispatcher.InvokeAsync。原生 Win32 的既定做法是工作者執行緒用 PostMessage 把自訂完成訊息貼到 UI 執行緒,由視窗程序更新 UI。
- 要怎麼調查應用程式為什麼顯示「沒有回應」?
- 重要的是在卡住的「當下本身」捕捉狀態。先從工作管理員的詳細資料索引標籤用「建立傾印檔」取完整傾印,再在 WinDbg 看 UI 執行緒(跑訊息迴圈的那條)的堆疊。它是卡在同步 I/O、網路等待、鎖等待,還是經由 SendMessage 等另一條執行緒,堆疊上原樣可見。要看即時處理程序,Process Explorer 的執行緒清單與堆疊檢視有用;要長期追蹤,擷取 WPR 追蹤有效。也請見本站的 WinDbg 入門文章、Process Explorer 實戰,以及 WPR/WPA 實戰。