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 属性

线程句柄

相关文章
相关标签/搜索