「沒有回應」的真面目 ── Windows 如何判定應用程式卡住,以及不卡住的設計

· · Windows, Windows 開發, 故障調查, 多執行緒, WinForms, WPF, Win32 API, UI 設計

更新紀錄(僅初版,2026年08月22日 發布)
初次發布
引用本文(DOI(已登錄存檔): 10.5281/zenodo.22176691)

以下 DOI 指向先前登錄的存檔,內容可能與目前正文不同。引用目前正文時,請使用本頁網址。

Go Komura(2026)。〈「沒有回應」的真面目 ── Windows 如何判定應用程式卡住,以及不卡住的設計〉。小村軟體有限公司。 https://comcomponent.com/zh-TW/blog/windows-app-not-responding-hang-mechanism/

DOI(已登錄存檔)
10.5281/zenodo.22176691
DOI(上次登錄版本)
10.5281/zenodo.22176692

「處理到一半畫面變白,標題列出現『沒有回應』」「使用者說『偶爾會卡住』,但開發機上重現不出來」── 這是 Windows 業務應用程式常見的困擾。

首先要掌握的是:顯示「沒有回應」的不是應用程式自己,而是 Windows。Windows 偵測到視窗的訊息處理停了下來,就把原本的畫面換成替身視窗。追查原因的關鍵在於:負責該視窗的 UI 執行緒正在做什麼,以致無法處理下一則訊息。12

本文寫給在 Windows 上開發業務應用程式的工程師,以及接到「應用程式卡住」回報的 IT 人員,依照作業系統的判定 → 訊息迴圈 → 依原因釐清 → 不卡住的設計 → 調查程序的順序整理。如果你現在就要調查一個卡住的應用程式,請先讀第 7 章。

1. 先講結論:不是「消掉顯示」,而是「把 UI 執行緒空出來」

把「沒有回應」的機制、修法與查法分成下面幾塊,整件事會清楚很多。

想知道的事、遇到的困擾 要先掌握的重點 詳細說明
Windows 看什麼來判定沒有回應 看的是視窗與擁有它的 GUI 執行緒的訊息處理,不是對整個處理程序的判定 第 2 章
為什麼按鈕與重繪都停住 UI 執行緒回不到事件處理或等待之外,取不出下一則訊息 第 3、4 章
不想讓耗時處理把畫面卡住 CPU 運算與只有同步版本的 API 分離到工作者,I/O 改用非同步 API 第 5 章
可以用 DoEvents 或設定只規避顯示嗎 會招來重入錯誤,或只是把顯示藏起來。不要當成根本對策 第 6 章
想調查偶爾卡住的原因 在結束或重新啟動之前,先取下卡住當下的傾印 第 7 章

對策的原則是不要在 UI 執行緒上久等,也不要做重運算。UI 執行緒負責輸入、繪製、顯示進度與接受取消,和耗時的工作切開。3

還有一點:顯示「沒有回應」之前的時間,和操作起來舒適的回應時間,是兩回事。不是低於 5 秒就沒問題。這個差別在 4.5 說明。

2. 「沒有回應」是作業系統的判定:5 秒規則與替身畫面

2.1 判定的單位不是整個處理程序,而是視窗與擁有它的執行緒

Microsoft 對 IsHungAppWindow 的說明中,滿足下列條件的視窗會被視為沒有回應。1

條件 內容
不是在等輸入 並非處於等待輸入的狀態
不在啟動處理中 不是應用程式的啟動處理期間
沒有取出訊息 在內部逾時的 5 秒之間沒有呼叫 PeekMessage

作業系統看的不是應用程式在算什麼,而是訊息迴圈有沒有在轉。所謂訊息迴圈,就是依序取出輸入與重繪要求並加以處理的機制。具體的運作在第 3 章說明。

同一份文件也明白寫著,5 秒這個值未來有可能改變。它是沒有回應判定的內部逾時,不是「UI 可以停到 5 秒」的設計基準。1

判定不是以處理程序為單位。在擁有多條 UI 執行緒的應用程式裡,就算某個視窗卡住了,由另一條執行緒負責的視窗仍可能正常運作。調查時也一樣,不能只看處理程序,必須找出卡住視窗的擁有者執行緒。

2.2 泛白模糊的畫面是「幽靈視窗」

頂層視窗被判定為沒有回應時,Windows 會隱藏原本的視窗,換成具有相同 Z 順序、位置、大小與外觀的幽靈視窗。標題上的「(沒有回應)」,以及 Aero 佈景主題下泛白模糊的外觀,都不是卡住的應用程式畫出來的,而是來自這個替身畫面。2

畫面的狀態 使用者看得到的事
原本的應用程式視窗 無法處理訊息,對點擊與重繪都沒有反應
作業系統準備的幽靈視窗 可以做移動、調整大小、最小化、關閉等有限的操作

就算替身視窗能移動,也不代表應用程式的內容重新開始運作了。那只是作業系統代為提供最低限度的操作而已。24

就算收到「顯示出現得太早」這種回報,也要先當成 UI 執行緒長時間回不到訊息處理的問題來看待。比起抑制顯示,先調查停住的處理才是正事。

2.3 偵錯時沒有顯示出來,不代表沒有卡住

附加偵錯工具時,作業系統不會建立幽靈視窗。因此即使出現「偵錯執行時不會出現,一般執行時就變成沒有回應」的差異,也可能是 UI 執行緒同樣被擋住,只有顯示不同而已。2

雖然也有以處理程序為單位停用幽靈視窗替換的 API,但它並不是修好卡住本身的 API。用途上的區別在 6.2 說明。

3. 畫面為什麼會停住:UI 執行緒與訊息迴圈

3.1 輸入與重繪都由同一條執行緒處理

Windows 的 GUI 應用程式是事件驅動的。它接收滑鼠、鍵盤、重繪要求、計時器等訊息,並執行各自對應的處理。每條建立了視窗的執行緒都擁有訊息佇列,並轉動像下面這樣的迴圈。5

MSG msg;
while (GetMessage(&msg, NULL, 0, 0) > 0) {
    TranslateMessage(&msg);
    DispatchMessage(&msg);   // 呼叫視窗程序
}

GetMessage 從佇列取出訊息,DispatchMessage 呼叫該視窗的視窗程序。視窗程序就是針對訊息執行對應處理的函式。按鈕的點擊處理、重繪,以及 WinForms、WPF 的事件處理常式,全都連到這條 UI 執行緒上的訊息處理。6

訊息迴圈的基本結構作業系統把滑鼠與鍵盤等輸入放進執行緒的訊息佇列,UI 執行緒的迴圈用 GetMessage 取出後以 DispatchMessage 呼叫視窗程序,處理結束後回到迴圈開頭OS(輸入、重繪要求、計時器)執行緒的訊息佇列用 GetMessage 取出DispatchMessage在視窗程序中處理

圖 1: 取出訊息、在視窗程序中處理、回到迴圈。這個轉動支撐著輸入與繪製。

那麼,如果在點擊處理常式裡呼叫一段很長的處理會怎樣?在那段處理返回之前,UI 執行緒取不出下一則訊息。後來送達的點擊與重繪要求都無法處理。這就是「畫面卡住」的基本結構。

3.2 PostMessage 與 SendMessage 返回的時機不同

訊息的傳遞有兩條路徑:放上佇列的路徑,以及等待處理完成的路徑。57

API 動作 呼叫端返回的時機
PostMessage 把訊息放上佇列,由接收端取出後處理 放上佇列就返回,不等處理完成
SendMessage 把訊息送給視窗程序,讓它執行處理 視窗程序處理完之前不返回

特別是把 SendMessage 送到另一條執行緒的視窗時,傳送端會被迫等到接收端有辦法處理訊息為止。這個差異直接通往 4.3 的死結。7

4. 依原因分類:擋住 UI 執行緒的五種模式

外觀同樣是「沒有回應」,擋住它的東西卻各不相同。先把候選分開,最後再用第 7 章的傾印或追蹤確認。

可能的原因 UI 執行緒上正在發生的事 常見的呈現方式
同步 I/O 與網路呼叫 等待檔案、資料庫、Web API 等完成 開發機上很快,正式環境或特定環境才卡住
等待鎖 取不到工作者握著的鎖而等待 依處理重疊的方式,偶爾才卡住
跨執行緒的 SendMessage 等待另一條執行緒處理訊息 被互相等待或廣播牽連
對 COM STA 的呼叫 等待被呼叫端 STA 的訊息處理 UI 執行緒停住會波及其他執行緒的 COM 呼叫
短處理的累積 單次雖短,連續執行就回不到迴圈 只有件數變多時操作才變慢

4.1 同步 I/O 與網路呼叫

最常遇到的,是在按鈕的點擊處理常式裡以同步方式讀寫大檔案、執行資料庫查詢、呼叫 Web API,或存取網路磁碟機的模式。

就算在開發機上零點幾秒就跑完,正式環境的網路延遲或檔案伺服器狀況不佳,也可能變成數十秒的等待。網路磁碟機在連線中斷時的逾時很長,會讓症狀更嚴重。「開發機上很快」不能當成可以在 UI 執行緒上等待的理由。

4.2 UI 執行緒等待工作者握住的鎖

保護共享資料的鎖,也會成為讓 UI 停住的原因。工作者長時間握著鎖,而 UI 執行緒也要取得同一把鎖時,UI 執行緒就會一直等到取得為止。

即使把工作移給工作者,只要結構上是 UI 執行緒在等那份工作結束或等鎖釋放,畫面就空不出來。鎖的紀律在多執行緒實務系列有詳細討論。

4.3 用 SendMessage 互相等待對方完成

經典的死結,是UI 執行緒在等工作者結束,而那個工作者又送 SendMessage 給 UI 並等待的組合。UI 正在等待因而無法處理訊息,工作者也回不到 SendMessage 之後,於是雙方都動不了。75

跨執行緒 SendMessage 造成的死結UI 執行緒因為等待工作者執行緒的結果而被擋住時,工作者執行緒又向 UI 執行緒的視窗送出 SendMessage,雙方互相等待對方完成而形成死結工作者執行緒UI 執行緒工作者執行緒UI 執行緒無法處理訊息(等待中)回不到 SendMessage 之後互相等待而死結等待工作者完成(被擋住)SendMessage(處理完成前不返回)

圖 2: 只把工作分離給工作者並不夠。UI 與工作者互相等待對方處理完成時,就會變成死結。

送往 HWND_BROADCAST 也要小心。只要有一個視窗不回應,傳送端就會被牽連進去。在等不起的場合,可以考慮設有逾時的 SendMessageTimeout,或不等處理完成的 PostMessage。8

4.4 COM 的 STA 停住,連其他執行緒也被牽連

從其他執行緒對 STA 物件的呼叫,是以視窗訊息傳遞的。因此 UI 執行緒(STA)被擋住時,送往它的 COM 呼叫也會被擋住。這個結構會讓不只 UI、連呼叫端都陷入等待狀態。

套間與訊息處理的關係,在 COM STA/MTA 的文章有說明。

4.5 「單次只是一瞬間」連做一百次

就算單次只有 50 ms 的同步處理,連做 100 次就是 5 秒。不要只量個別函式就判斷它很短,而要用UI 執行緒回到訊息迴圈為止的總時間來思考。

體感上的「遲鈍」大約從 100 ms 開始。設計時不要把沒有回應判定的 5 秒當成目標,而要把可以擋住 UI 執行緒的時間想成毫秒等級。

5. 不卡住的設計:把工作的執行與畫面的更新分開

5.1 區分運算與只有同步 API 的處理,以及非同步 I/O

對策的原則是把耗時的工作趕出 UI 執行緒。不過,並不是所有東西都用同一種方式搬走。

工作的種類 C# 的基本處理方式 UI 執行緒的角色
CPU 負擔重的運算 用 Task.Run 移到工作者執行緒 非同步地等待完成,並顯示結果
只有同步 API 的處理 分離到工作者執行緒 不因為同步等待完成而擋住 UI
有非同步 API 的 I/O 對 GetStringAsync 等使用 await 等待 I/O 期間回到訊息處理
輸入、繪製、進度、取消 由 UI 執行緒負責 不混入長運算或同步 I/O

在非同步 I/O 中,不需要只為了等待就占用一條執行緒。後面的程式碼範例也一樣,運算用 Task.Run,HTTP 通訊則使用原生的非同步 API,兩者分開使用。3

5.2 C# 用 async/await 回到 UI 執行緒更新

下面是在 WinForms 的點擊處理常式中,把重工作與 UI 更新分開的例子。在這個從 UI 事件處理常式啟動、並保有 UI 執行內容的例子裡,await 之後的接續會回到 UI 執行緒,因此可以更新控制項。3

private async void RunButton_Click(object sender, EventArgs e)
{
    runButton.Enabled = false;
    try
    {
        // CPU 負擔重的處理、只有同步版本的 API,用 Task.Run 交給工作者
        var result = await Task.Run(() => HeavyCalculation(input));

        // I/O 使用原生就是非同步的 API(也不會占用執行緒)
        var data = await httpClient.GetStringAsync(url);

        // await 之後已經回到 UI 執行緒,所以可以直接碰控制項
        resultLabel.Text = result + data.Length.ToString();
    }
    catch (Exception ex)
    {
        // 例外從 async void 處理常式漏出去會讓應用程式當掉。在這裡接住
        MessageBox.Show($"處理失敗:{ex.Message}");
    }
    finally
    {
        runButton.Enabled = true;
    }
}

要讀懂的重點有下面四個。

位置 用意
用 await 等待完成 產生等待時讓 UI 執行緒回到訊息迴圈
在 await 之後更新畫面 不從工作者直接碰控制項
執行期間停用按鈕 防止同一項處理被重複啟動
catch 與 finally 不讓例外從 async void 處理常式漏出,並把按鈕狀態復原

這只是示範角色分工的程式碼範例,HeavyCalculation 等的實作、表單關閉時的處理,以及進度與取消處理都省略了。長時間處理所需的進度與中斷,另外照 5.4 的方式加入。

WinForms 的控制項與 WPF 的元素,要從建立它們的執行緒來操作。從工作者直接碰會導致例外或不確定的行為。要明確地切回 UI 執行緒時,WinForms 使用 Control.Invoke / BeginInvoke,WPF 使用 Dispatcher.InvokeAsync。3

5.3 Win32 用 PostMessage 通知完成

原生 Win32 的角色分工也一樣。把工作交給工作者,完成後用 PostMessage 送出自訂的完成訊息,由 UI 執行緒的視窗程序更新畫面。PostMessage 放上佇列就返回,因此工作者不會等待 UI 處理完成。7

不卡住的應用程式中的角色分工UI 執行緒只負責接受輸入、顯示進度與接受取消,重工作由工作者執行緒執行,並透過 PostMessage 或 await 的接續把完成交回 UI 執行緒交出工作PostMessage / await 的接續UI 執行緒:輸入、進度、取消工作者執行緒:重處理禁止在 UI 執行緒做同步 I/O 與長運算

圖 3: 重運算與同步處理交給工作者,畫面的更新則交回 UI 執行緒。非同步 I/O 如 5.1 所述,使用非同步 API 來等待。

等待工作者本身這件事,要依照條件變數的文章所談的紀律來寫。UI 執行緒不要又同步地回頭等待工作者,這一點同樣重要。

5.4 從一開始就設計進度與取消

就算畫面沒有卡住,如果長時間什麼都不變,使用者也分不出程式到底有沒有在動。因此,要把顯示進度與接受取消也當成處理的一部分來設計。

傳遞方向 使用的機制 角色
工作者 → UI IProgress<T> 把進展狀況傳給畫面
UI → 工作者 CancellationToken 傳達中止的要求

工作者在告一段落的地方中斷,並做好善後。有了進度與中止的手段,就能減少使用者依賴強制結束的場面。因為強制結束有時會造成資料損毀。

6. 兩種看似規避手段的做法,以及它們的極限

6.1 DoEvents 會把別的事件請進處理的中途

在重處理之間插入 Application.DoEvents() 或 PeekMessage 迴圈,就能處理訊息並避開沒有回應的顯示。可是這麼一來,原本的處理還沒結束,別的事件處理常式就會在中途被執行。這就是重入。

舉例來說,在加工資料的途中呼叫 DoEvents,於是累積起來的按鈕再點擊被處理了。那個處理常式改寫了同一份資料之後,原本的處理才恢復。在這個順序下,原本的處理所預期的狀態已經消失了。

會重入的不只有按鈕。關閉表單的操作與計時器也會進來。處理中的資料被破壞、碰到已關閉的表單而發生例外,這類錯誤依賴時機、難以重現,往往比沒有回應更難調查。

原則是分離工作,而不是用手去轉幫浦。手動的訊息處理只保留在強制回應的進度對話方塊這類受限的結構裡。一般的長時間處理,請採用第 5 章的分離與重入防止。

6.2 停用幽靈視窗,操作也回不來

DisableProcessWindowsGhosting 是用來對呼叫端處理程序停用幽靈視窗替換的 API。停用會持續到處理程序結束為止。4

它是為了資訊站終端機這類特殊用途而存在的:你不希望作業系統擅自拿出一個看起來可以操作的替身視窗。訊息處理停住的事實並沒有改變,使用者還會連幽靈視窗提供的移動與結束手段都失去。這不是一般應用程式拿來當沒有回應對策的東西。

做法 會改變的事 留下的問題
用 DoEvents 等手動轉動幫浦 在處理的中途也會處理其他訊息 任意事件會重入,狀態可能被破壞
DisableProcessWindowsGhosting 停止作業系統換成替身視窗 UI 執行緒依然被擋住
把耗時的工作從 UI 分離出去 讓 UI 執行緒能回到輸入與繪製 進度、取消、重入防止也要一併設計

7. 調查程序:關掉之前,先把卡住的當下留下來

7.1 先取得傾印

在調查「偶爾卡住」時,最有價值的是卡住那一刻的執行緒狀態。一旦結束或重新啟動,那個狀態就沒了。

在工作管理員的「詳細資料」索引標籤對目標處理程序按右鍵,選擇「建立傾印檔」。這樣就能取得包含所有執行緒堆疊的完整傾印。也要把「卡住時,關掉之前先取傾印」這個步驟,分享給接收回報的那一方。

收集機制的建立,請參考當機傾印收集的文章。

7.2 看擁有卡住視窗的那條執行緒

用 WinDbg 開啟傾印,確認 UI 執行緒的堆疊。轉動訊息迴圈的執行緒通常是執行緒 0,但也有多條 UI 執行緒的應用程式,所以不要只憑編號決定,要看卡住視窗的擁有者執行緒。

堆疊上看得到的東西 接下來要查的對象
停在 ReadFile 或網路 API 的等待 檔案存取、同步 I/O、等待網路回應
停在 WaitFor… 系列的等待 鎖或同步對象。在等什麼完成,而對方又在做什麼
停在 SendMessage 內部的等待 目的地執行緒能不能處理訊息、雙方是否互相等待

UI 執行緒的堆疊會顯示出它在等什麼而停住。懷疑是死結時,不要只看單邊,要把兩條執行緒的等待對象對照起來看。具體的讀法在 WinDbg 入門文章有說明。

調查沒有回應的基本程序在卡住的瞬間取得傾印,查看 UI 執行緒的堆疊,判定它是停在同步 I/O、等待鎖,還是跨執行緒的 SendMessage,並連到對應的設計修正卡住的瞬間取得傾印(關掉之前)查看 UI 執行緒的堆疊同步 I/O、等待網路等待鎖跨執行緒的 SendMessage把該處分離到工作者

圖 4: 從卡住瞬間的傾印追查 UI 執行緒在等什麼。若是 I/O 就改成非同步或分離;若是鎖或 SendMessage,就一路確認到互相等待的結構。

7.3 即時確認與時間序列分析分開使用

除了傾印之外,還有當場查看執行中處理程序的方法,以及記錄時間流動的方法。

想調查的事 方法
保存卡住的瞬間,事後再查 取得傾印並用 WinDbg 分析
當場查看執行中處理程序的執行緒與堆疊 使用 Process Explorer
依時間序列追蹤常態性的遲鈍或 UI 執行緒的等待 用 WPR 擷取追蹤,再用 WPA 分析

時間序列的擷取方式,在 WPR/WPA 實戰中討論。

7.4 從症狀縮小候選,再用證據確認

重現條件也是調查的入口。不過不要只憑症狀就斷定原因,要和傾印或追蹤對照。

症狀 先懷疑的對象 確認的地方
某個操作一定會卡住 該處理常式裡的同步 I/O 等 對應該操作的處理,以及 UI 執行緒的堆疊
偶爾卡住,與操作沒有相關性 鎖的順序、跨執行緒 SendMessage 的死結 互相等待的兩條執行緒的堆疊
只有特定環境會卡住 網路磁碟機、Proxy、防毒軟體等造成的等待或逾時 等待中的 API,以及該環境下的回應時間

8. 總結

「沒有回應」是 Windows 判定視窗的訊息處理停了下來,並拿出替身畫面的機制。判定的單位是視窗與擁有它的 GUI 執行緒,不是整個處理程序。5 秒這個內部逾時是未來可能改變的值,要和舒適的 UI 回應時間區分開來。12

要修的對象不是顯示,而是長時間占用 UI 執行緒的運算與等待。CPU 處理與只有同步版本的 API 交給工作者,非同步 I/O 改用非同步 API,畫面的更新則交回 UI 執行緒。進度、取消、重入防止也要成套設計。用 DoEvents 規避或停用幽靈視窗,都無法取代這些做法。

原因不明時,先從在結束之前取得傾印,查看卡住視窗的擁有者執行緒在等什麼開始。把「看起來壞掉了」換成「UI 執行緒回不到訊息處理」,要查的地方與設計的修法就連得起來了。

相關文章

相關諮詢領域

小村軟體有限公司承接「偶爾卡住」「變成沒有回應」的業務應用程式原因調查(傾印分析與追蹤分析)、把充滿同步處理的舊有 UI 程式碼改寫為 async/await 並分離工作者執行緒,以及不會凍結的 UI 設計審查。即使還在重現步驟不明的階段,也能從如何取證的設計開始協助。

參考連結

  1. Microsoft Learn, IsHungAppWindow function (winuser.h). 關於判定基準:應用程式「不是在等輸入、不在啟動處理中,且在內部逾時的 5 秒之間沒有呼叫 PeekMessage」時會被視為沒有回應;關於這個 5 秒基準可能改變;以及對幽靈視窗一律傳回 TRUE。 ↩ ↩2 ↩3 ↩4

  2. Microsoft Learn, GetMessage function (winuser.h). 關於頂層視窗數秒不回應訊息時,系統把它視為沒有回應,並換成具有相同 Z 順序、位置、大小與外觀的幽靈視窗;關於使用者只能做移動、調整大小與關閉;以及附加偵錯工具時不會建立幽靈視窗。 ↩ ↩2 ↩3 ↩4 ↩5

  3. Microsoft Learn, How to make thread-safe calls to controls (Windows Forms). 關於 WinForms 的控制項無法從建立它們以外的執行緒安全地碰觸;關於從其他執行緒更新時要使用 Invoke/BeginInvoke;以及使用 async/await 或 BackgroundWorker 的安全非同步模式。 ↩ ↩2 ↩3 ↩4

  4. Microsoft Learn, DisableProcessWindowsGhosting function (winuser.h). 關於能對呼叫端的 GUI 處理程序停用幽靈視窗功能(讓沒有回應的視窗可以最小化、移動與關閉);以及停用會持續到處理程序結束為止。 ↩ ↩2

  5. Microsoft Learn, About Messages and Message Queues. 關於 Windows 應用程式是事件驅動、由視窗程序處理訊息的結構;關於經由佇列的訊息與直接送出的訊息之區別;關於沒有回應的視窗被換成幽靈視窗;以及執行緒之間互相送訊息造成死結的章節。 ↩ ↩2 ↩3

  6. Microsoft Learn, Using Messages and Message Queues. 關於以 GetMessage、TranslateMessage 與 DispatchMessage 實作標準訊息迴圈的範例,以及訊息佇列的調查方式。 ↩

  7. Microsoft Learn, SendMessage function (winuser.h). 關於 SendMessage 會呼叫指定視窗的視窗程序,並在處理完成之前不返回;關於送往另一條執行緒的視窗時,要等對方執行緒處理訊息才會返回;以及與不等回應、直接放上佇列的 PostMessage 之間的差異。 ↩ ↩2 ↩3 ↩4

  8. Microsoft Learn, SendMessageTimeout function (winuser.h). 關於能以逾時方式送出訊息;以及備有對不回應的視窗(被判定為卡住的視窗)不等待就返回的旗標(SMTO_ABORTIFHUNG)。 ↩

共用相同標籤的最新文章。能以相近的主題延伸理解。

與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。

本文連結到以下服務頁面,歡迎從最接近的入口查看。

常見問題

整理諮詢這個主題時常見的問題。

「沒有回應」在什麼條件下會顯示出來?
當一個有視窗的應用程式「不是在等輸入、不在啟動處理中,而且 5 秒內沒有取出訊息(PeekMessage)」時,作業系統就把該視窗判定為沒有回應。被判定的頂層視窗會被隱藏,換成位置、大小、外觀都相同的「幽靈視窗」。標題列上的「(沒有回應)」字樣與泛白模糊的外觀都屬於這個幽靈視窗,使用者只能做移動、最小化、關閉這幾種操作。也就是說,「沒有回應」不是應用程式自己畫出來的,而是作業系統代替它顯示的畫面。
有沒有設定可以讓處理進行中不顯示「沒有回應」?
呼叫 DisableProcessWindowsGhosting,就能對該處理程序停用換成幽靈視窗的行為。但這只是「讓使用者比較看不出來程式卡住」,視窗不回應操作的事實並沒有改變,使用者看到的反而是連移動與關閉都做不到的完全凍結。根本對策不是抑制顯示,而是把重工作移到工作者執行緒,讓 UI 執行緒別說 5 秒,連 0.1 秒都不要被擋住。另外要注意,附加偵錯工具時作業系統不會建立幽靈視窗,因此看起來會像是「偵錯時不會沒有回應」。
可以用 DoEvents(手動轉動訊息幫浦)來規避沒有回應嗎?
不建議。在重工作的中途轉動 DoEvents 或 PeekMessage 迴圈,雖然能避開沒有回應的判定,但按鈕被再次點擊、關閉視窗的操作、計時器等任意事件處理常式,都會在處理進行到一半時重入。處理中的資料被別的處理常式改寫、碰到本該關閉的表單而發生例外,這類重入錯誤依賴時機、難以重現,比沒有回應更棘手。正攻法是用 Task.Run 等方式把處理本身移到工作者執行緒,UI 執行緒只負責顯示進度與接受取消。
要怎麼從工作者執行緒更新畫面(控制項)?
WinForms 的控制項與 WPF 的元素,只能從建立它們的執行緒(通常是 UI 執行緒)碰觸。從工作者執行緒直接碰會造成例外或不確定的行為。在 C# 裡使用 async/await 最為簡單,因為 await 之後的接續會回到呼叫端的 UI 執行緒,所以 await 之後可以照常更新控制項。要明確切換時,WinForms 使用 Control.Invoke/BeginInvoke,WPF 使用 Dispatcher.InvokeAsync。原生 Win32 的標準做法,則是從工作者執行緒用 PostMessage 把自訂的完成訊息送給 UI 執行緒,由視窗程序那一側更新畫面。
要怎麼調查變成「沒有回應」的應用程式的原因?
重點是在卡住的「那個瞬間」取得狀態。先從工作管理員的詳細資料索引標籤用「建立傾印檔」取得完整傾印,再用 WinDbg 查看 UI 執行緒(轉動訊息迴圈的那條執行緒)的堆疊。它是停在同步 I/O、等待網路、等待鎖,還是因為 SendMessage 而等待其他執行緒,堆疊上會原樣呈現。想在程式還活著時查看,可以用 Process Explorer 的執行緒清單與堆疊檢視;想依時間序列追蹤,則用 WPR 擷取追蹤。也歡迎參考本站的 WinDbg 入門、Process Explorer 實戰、WPR/WPA 實戰等文章。

作者檔案

本文作者的個人檔案頁面。

Go Komura

小村軟體有限公司 代表

以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。

回到部落格一覽