原因剖析

UI僵死无非只是因为UI线程因繁忙而无法去接受用户的响应。详细说来内在原因有以下两个:

  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呈现僵死状态

PostMessage

  • 将消息送至目标window所在线程(可通过系统API获取控件的句柄所属的线程)的“Posted Message Queue”消息队列,消息称为列队型(queued)型消息
  • Control.Invoke与Control.BeginInvoke都调用PostMessage(相比SendMessage可防止死锁),区别是前者会使用WaitForWaitHandle来等待消息处理完毕

SendMessage

  • 将消息送至目标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操作,包括控件的创建。

不过团队水平参差不齐,即使是一些老手也难免犯错。

所以如果能够拦截控件创建的过程,那么就可以通过Windows API根据此控件的句柄获取其在运行的线程号,看是否就是主UI线程号来输出日志,以在调试阶段解决问题。

有以下两种途径:

拦截Winodws Message。通过创建Global Hook拦截所有线程的窗口创建消息。

拦截Windows API。通过拦截各线程对窗口创建的API的调用。

使用windbg拉截windows api的调用前不要忘了为其加载符号。如:srv*c:\symbols*http://msdl.microsoft.com/download/symbols

 本人通过EasyHook开源库使用了第2种方法,即拦截对WindowsAPI的调用完成了工具的创建,截图如下:

Request sent和Waiting 慢_Windows