UI僵死分析

緣由剖析

UI僵死無非只是由於UI線程因繁忙而沒法去接受用戶的響應。詳細說來內在緣由有如下兩個:html

  1. 正常的業務代碼寫在UI線程中執行,業務代碼的任務繁重致使UI線程沒法分身去接受用戶的界面輸入
  2. UI控件在非UI線程中建立。緣由以下如述:
    1. 每個UI控件建立後都向SystemEvents註冊UserPreferenceChanged事件,而且建立了控件的線程會被自動安裝WindowsFormsSynchronizationContext做爲其同步上下文
    2. 系統默認在UI線程裏建立一個隱藏窗口「.NET-BroadcastEventWindow」來獲取SystemEvents相關的系統消息
    3. 此隱藏窗口獲取到消息後經過系統的PostMessage方法向註冊了此事件的各UI控件發送通知並等待(注意不是經過SendMessage)
    4. 若某UI控件未建立在UI線程上,由於其建立控件的線程不會監視和獲取本線程的消息隊列中的消息,因此UI線程的PostMessage方法會一直等待,UI呈現僵死狀態

PostMessagewindows

  • 將消息送至目標window所在線程(可經過系統API獲取控件的句柄所屬的線程)的「Posted Message Queue」消息隊列,消息稱爲列隊型(queued)型消息
  • Control.Invoke與Control.BeginInvoke都調用PostMessage(相比SendMessage可防止死鎖),區別是前者會使用WaitForWaitHandle來等待消息處理完畢


SendMessageapi

  • 將消息送至目標window所在線程的「Sent Message Queue」消息隊列,但消息稱爲非列隊型(Non-queued)消息
  • 發送線程調用SendMessage後會掛起並等待返回,若是期間有其餘線程發消息給這個發送線程,它能夠響應,但僅限於非隊列型(Non-queued)消息
  • WH_CALLWNDPROC鉤子用於監視SendMessage調用

 

異常發生後如何診斷

診斷的目的是要肯定引起了UI線程繁忙的緣由。安全

  • 如果由於上述第1點緣由,即由於正常的業務代碼在UI線程中跑的話,直接用VS等調試工具看一下UI線程的堆棧便可;
  • 若若由於上述第2點緣由,即由於在非UI線程中建立了UI控件的話,那得先找出此控件。UI線程裏此控件由於觸發了SynchronizationContext.Send方法而凍結。
    • 使用spy++能夠直接查看活動的後臺線程上是否有控件。不過若線程將控件建立出來放在堆內存上後線程就消亡了的話,那就沒法看到了。
    • 使用Windbg:
      • 獲取UI線程堆棧一看便知控件的類名,對照着Windbg的「!dso」命令結果找到此控件的地址,再查找其引用。若UI線程中顯示的控件類型因其爲內部的子控件且爲通用類型而沒法直接定位代碼的話,那麼能夠嘗試追溯找到此控件的父控件。
      • 可獲取讓UI凍結的WindowsFormSynchronizationContext,再經過如下方式找出目標託管線程的ID。不過由於託管線程在消亡後的ID能夠被重複使用,因此經過此方式找到的託管線程ID多是已經消亡的線程的ID,因此以後要找到目標Thread對象再比較其Thread.m_ExecutionContext._syncContext是否不爲空且正爲讓UI凍結的WindowsFormSynchronizationContext。
      1. 使用「!do <synchronizationContext對象地址>」命令顯示其數據結構,從成員destinationThreadRef獲取指定了建立控件的目標線程的WeakReference對象地址
      2. 使用「!dumpobject <WeakReference對象地址>」顯示其數據結構,從成員m_handle獲取建立控件的目標線程的句柄
      3. 使用「dd <目標線程的句柄> L1」命令顯示此句柄中包含的線程地址
      4. 使用「!dumpobject <目標線程地址>」命令顯示目標線程的數據結構,從成員m_ManagedThreadId獲取目標線程的託管線程號
      5. 使用「?0n<託管線程號>」命令獲取託管線程號的16進制數

 

 防患於未然數據結構

寫代碼時應該遵循這條原則:保證線程安全,尤爲是不應在非UI線程上直接進行UI操做,包括控件的建立。app

不過團隊水平良莠不齊,即便是一些老手也不免犯錯。函數

因此若是可以攔截控件建立的過程,那麼就能夠經過Windows API根據此控件的句柄獲取其在運行的線程號,看是否就是主UI線程號來輸出日誌,以在調試階段解決問題。工具

有如下兩種途徑:ui

攔截Winodws Message。經過建立Global Hook攔截全部線程的窗口建立消息。spa

攔截Windows API。經過攔截各線程對窗口建立的API的調用。

使用windbg拉截windows api的調用前不要忘了爲其加載符號。如:srv*c:\symbols*http://msdl.microsoft.com/download/symbols

 本人經過EasyHook開源庫使用了第2種方法,即攔截對WindowsAPI的調用完成了工具的建立,截圖以下:

 

參考

Debugging Windows Forms Application Hangs During SystemEvents.UserPreferenceChanged

Windows Forms application freezes when system settings are changed or the workstation is locked

Mysterious Hang or The Great Deception of InvokeRequired

細說UI線程和Windows消息隊列

理解Windows窗體和WPF中的跨線程調用

WinForm二三事(三)Control.Invoke&Control.BeginInvoke

一千個是什麼 - Windows消息機制(Windows Messaging)

Invoke and BeginInvoke

Windows 應用程序交互過程

PostMessage與SendMessage

Windows 應用程序交互過程

Using Window Messages to Implement Global System Hooks in C#

PInvoke.net

Windows API函數大全

Deviare API Hook Overview

EasyHook

HOOK API 函數跳轉詳解

Windows下Hook API技術 inline hook

Change C# Class object to System.IntPtr

GCHandle.Alloc 方法 (Object)

如何得到指定進程的主窗口

EnumWindows function

Control.InvokeRequired 屬性

線程句柄

相關文章
相關標籤/搜索