一个超经典 WinForm,WPF 卡死问题的终极反思
一:背景
1. 讲故事
写这篇文章起源于训练营里一位朋友最近在微信聊到他对这个问题使用了一种非常切实可行,简单粗暴的方式,并且也成功解决了公司里几个这样的卡死dump,如今在公司已是灵魂级人物,让我也尝到了什么叫反哺!对,这个东西叫 Harmony
, github网址: https://github.com/pardeike/Harmony
,一个非常牛逼的C#程序函数修改器。
二:卡死问题的回顾
1. 故障成因
为了方便讲述,先把 WinForm/WPF 程序故障的调用堆栈给大家呈现一下。
0:000:x86> !clrstack
OS Thread Id: 0x4eb688 (0)
Child SP IP Call Site
002fed38 0000002b [HelperMethodFrame_1OBJ: 002fed38] System.Threading.WaitHandle.WaitOneNative(System.Runtime.InteropServices.SafeHandle, UInt32, Boolean, Boolean)
002fee1c 5cddad21 System.Threading.WaitHandle.InternalWaitOne(System.Runtime.InteropServices.SafeHandle, Int64, Boolean, Boolean)
002fee34 5cddace8 System.Threading.WaitHandle.WaitOne(Int32, Boolean)
002fee48 538d876c System.Windows.Forms.Control.WaitForWaitHandle(System.Threading.WaitHandle)
002fee88 53c5214a System.Windows.Forms.Control.MarshaledInvoke(System.Windows.Forms.Control, System.Delegate, System.Object[], Boolean)
002fee8c 538dab4b [InlinedCallFrame: 002fee8c]
002fef14 538dab4b System.Windows.Forms.Control.Invoke(System.Delegate, System.Object[])
002fef48 53b03bc6 System.Windows.Forms.WindowsFormsSynchronizationContext.Send(System.Threading.SendOrPostCallback, System.Object)
002fef60 5c774708 Microsoft.Win32.SystemEvents+SystemEventInvokeInfo.Invoke(Boolean, System.Object[])
002fef94 5c6616ec Microsoft.Win32.SystemEvents.RaiseEvent(Boolean, System.Object, System.Object[])
002fefe8 5c660cd4 Microsoft.Win32.SystemEvents.OnUserPreferenceChanged(Int32, IntPtr, IntPtr)
002ff008 5c882c98 Microsoft.Win32.SystemEvents.WindowProc(IntPtr, Int32, IntPtr, IntPtr)
...
这个程序之所以被卡死,底层原因到底大概是这样的。
- 程序在t1时间,有非主线程创建了控件。
- 程序在t2时间,用户主动或被动做了 远程连接,Windows主题色刷新 等操作,这种系统级操作Windows需要同步刷新给所有UI控件。
- 那些非主线程控件由于没有 MessageLoop 机制,导致主线程给这些UI发消息时得不到响应,最终引发悲剧。
t2时间的卡死是由于t1时间的错误创建导致,要想在dump中反向追溯目前是无法做到的,所以要想找到祸根需要监控t1,即MarshalingControl
到底是谁创建的,为此我也写过两篇文章来仔细分析此事。
- (一个超经典 WinForm 卡死问题的再反思 )[https://www.cnblogs.com/huangxincheng/p/16868486.html]
- (一个超经典 WinForm 卡死问题的最后一次反思)[https://www.cnblogs.com/huangxincheng/p/17654394.html]
第一种方式是启动 windbg 对 System_Windows_Forms_ni System.Windows.Forms.Application+MarshalingControl..ctor
进行拦截,说实话这种方式很多程序员搞不定,原因在于windbg的使用门槛较高,现实中很多程序员连CURD都没摸明白,所以可想而知了。。。
第二种方式是启动 perfview 对 winform/wpf 程序进行监控,直到程序出现卡死停止收集。最后在录播中寻找 MarshalingControl..ctor
的调用栈,这种方式也有不可行的时候,如果说卡死发生在程序启动的10天后,那这个录播文件将会超级超级大,或者有更极端的情况发生。
所以这两种方案都有各自的优缺点,现实可行性虽然有,但不高。。。今天作为终结篇,必须把这个问题安排掉,继续提供两种切实可行的方案。
三:两种修改方案
1. 使用 Harmony 注入
Harmony作为一款运行时C#方法修改器,借助它我完全可以将一些逻辑注入到 MarshalingControl..ctor
中,比如记录下初始化该方法的 堆栈信息
,是不是就可以轻松找到这个非主线程控件到底是谁?对不对,有了思路,我们在 nuget 上引用 Lib.Harmony
,上代码说话。
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
var harmony = new Harmony("一线码农聊技术");
Type applicationType = typeof(Application);
Type marshalingControlType = applicationType.GetNestedType("MarshalingControl", BindingFlags.NonPublic);
ConstructorInfo constructor = marshalingControlType.GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, Type.EmptyTypes, null);
var prefix = typeof(HookMarshalingControl).GetMethod("OnActionExecuting");
harmony.Patch(constructor, new HarmonyMethod(prefix));
}
private void Form1_Load(object sender, EventArgs e)
{
}
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
Button btn = new Button();
var query = btn.Handle;
}
private void button1_Click(object sender, EventArgs e)
{
backgroundWorker1.RunWorkerAsync();
}
}
/// <summary>
/// Hook MarshalingControl 的描述类
/// </summary>
public class HookMarshalingControl
{
/// <summary>
/// 原生方法之前执行的 action
/// </summary>
public static void OnActionExecuting()
{
Console.WriteLine("----------------------------");
Console.WriteLine($"控件创建线程:{Thread.CurrentThread.ManagedThreadId}");
Console.WriteLine(Environment.StackTrace);
Console.WriteLine("----------------------------");
}
}
卦中的代码逻辑我就不详述了,核心就是将 OnActionExecuting
方法注入到 MarshalingControl..ctor
构造函数里,把程序运行起来后观察 output 窗口,截图如下:
终于是一个卧槽,祸根居然是一个 tid=3
的线程初始化了 new Button()
控件。。。
2. 使用 DnSpy
Harmony 作为一款修改器,它对程序的侵入性是非常高的,目前还是有一些bug,比如对 .NET7 的支持还不是很好,但相对 perfview
和 windbg
的方式已经非常轻量级了,极大的降低了使用门槛。
问题来了,那有没有一种对程序无侵入,可行性超高的方式呢?当然是有的,dnspy 此时可以闪亮登场,用过 dnspy 的朋友应该知道它是一款轻量级,免安装绿色的调试器,当然除了调试器功能,它还是一款程序集修改器,可以实现 Harmony 的所有功能,在实践中我们可以将 dnspy copy 到客户机使用 启动调试
或者 附加进程
的方式对程序进行干预。
如何使用 dnspy 对 MarshalingControl..ctor
进行干预呢?可以使用 断点日志
的功能,日志信息如下:
控件创建线程:{Environment.CurrentManagedThreadId} \n $CALLSTACK
有些人可能要问了 $CALLSTACK
是什么东西?很显然是堆栈信息,除了这个关键词还有很多,具体可以看后面的 问号面板
。
接下来把程序跑起来,观察 output面板。
从面板中可以清楚的看到,原来有个 tid=3 的线程创建了一个 Button
控件,这就是我们要找的祸根。
到这里,可能有些人要说,dnspy 启动 exe 的方式因为各种原因在我们这边行不通,有没有其他的方式呢? 当然是有的,我们还可以在程序启动之后以 进程附加
的方式注入,同样也是一种非常可行且低侵入的方式。
为了能够更早的介入,可以在 Form1 初始化之前弹一个MessageBox,有更好的方式大家也可以说一下,感谢。参考代码如下:
public partial class Form1 : Form
{
public Form1()
{
MessageBox.Show("开启你的注入吧...");
InitializeComponent();
}
}
弹框之后,使用 dnspy 的进程附加。
附加好了之后关闭弹框让程序继续运行,点击 buttton 按钮,可以看到 output 上的输出。
11:20:01.548 控件创建线程:<<<当线程位于不安全状态时无法对表达式进行求值。按步调试或运行直到触发断点。>>>
11:20:01.550 System.Windows.Forms.Application.MarshalingControl.MarshalingControl
11:20:01.551 System.Windows.Forms.Application.ThreadContext.MarshalingControl.get
11:20:01.552 System.Windows.Forms.WindowsFormsSynchronizationContext.WindowsFormsSynchronizationContext
11:20:01.553 System.Windows.Forms.WindowsFormsSynchronizationContext.InstallIfNeeded
11:20:01.553 System.Windows.Forms.Control.Control
11:20:01.554 System.Windows.Forms.ButtonBase.ButtonBase
11:20:01.554 System.Windows.Forms.Button.Button
11:20:01.554 WindowsFormsApp1.Form1.backgroundWorker1_DoWork
11:20:01.555 System.ComponentModel.BackgroundWorker.OnDoWork
11:20:01.555 System.ComponentModel.BackgroundWorker.WorkerThreadStart
11:20:01.556 System.Runtime.Remoting.Messaging.StackBuilderSink.AsyncProcessMessage
11:20:01.556 System.Threading.ExecutionContext.RunInternal
11:20:01.557 System.Threading.ExecutionContext.Run
11:20:01.557 System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem
11:20:01.557 System.Threading.ThreadPoolWorkQueue.Dispatch
11:20:01.558 [本机到托管的转换]
11:20:01.558
这里稍微提醒一下,tid 在这里没有显示出来,大家可以换成问号面板
上的关键词 $TID
即可,不过TID不是最重要的,最重要的是调用栈给弄出来了。
四:总结
作为一名专业的 .NET高级调试师
,在这个经典卡死的问题溯源上一直没有提供非常好的解决方案,还是有些内疚的,在我的高级调试之旅中还是会不间断的收到类似dump,相信这篇文章之后,不再有人被它所困扰!
一个超经典 WinForm,WPF 卡死问题的终极反思的更多相关文章
- 一个超经典 WinForm 卡死问题的再反思
一:背景 1.讲故事 这篇文章起源于昨天的一位朋友发给我的dump文件,说它的程序出现了卡死,看了下程序的主线程栈,居然又碰到了 OnUserPreferenceChanged 导致的挂死问题,真的是 ...
- Winform/WPF中内嵌BeetleX的HTTP服务
在新版本的BeetleX.FastHttpApi加入了对netstandard2.0支持,如果程序基于.NetFramework4.6.1来构建WinForm或WPF桌面程序的情况下可以直接把Beet ...
- 网络采集软件核心技术剖析系列(7)---如何使用C#语言搭建程序框架(经典Winform界面,顶部菜单栏,工具栏,左边树形列表,右边多Tab界面)
一 本系列随笔概览及产生的背景 自己开发的豆约翰博客备份专家软件工具问世3年多以来,深受广大博客写作和阅读爱好者的喜爱.同时也不乏一些技术爱好者咨询我,这个软件里面各种实用的功能是如何实现的. 该软件 ...
- MFC,QT与WinForm,WPF简介
编程语言的组成编程语言做为一种语言自然和英语这些自然语言有类似的地方.学英语时我们知道要先记26个字母,然后单词及其发音,接下来就是词组,句子.反正简单的说就是记单词,熟悉词法,句法.接下来就是应用了 ...
- 后续来啦:Winform/WPF中快速搭建日志面板
后续来啦:Winform/WPF中快速搭建日志面板 继昨天发文ASP.NET Core 可视化日志组件使用(阅读文章,查看视频)后,视频下有朋友留言 "Winform客户端的程序能用它不?& ...
- 在VS中手工创建一个最简单的WPF程序
如果不用VS的WPF项目模板,如何手工创建一个WPF程序呢?我们来模仿WPF模板,创建一个最简单的WPF程序. 第一步:文件——新建——项目——空项目,创建一个空项目. 第二步:添加引用,Presen ...
- 腾讯出品的一个超棒的 Android UI 库
腾讯出品的一个超棒的 Android UI 库 相信做 Android 久了大家都会有种体会,那就是 Android 开发相对于前端开发来说统一的 UI 开源库比较少.造成这种现象的原因一方面是大多数 ...
- 提供PPT嵌入Winform/WPF解决方案,Winform/WPF 中嵌入 office ppt 解决方案
Winform/WPF 中嵌入 office ppt(powerpoint)解决方案示: 1. 在winform中操作ppt,翻页.播放.退出:显示 总页数.当前播放页数 2. 启动播放ppt时录制视 ...
- scala 入门Eclipse环境搭建及第一个入门经典程序HelloWorld
scala 入门Eclipse环境搭建及第一个入门经典程序HelloWorld 学习了: http://blog.csdn.net/wangmuming/article/details/3407911 ...
- 优雅的在WinForm/WPF/控制台 中使用特性封装WebApi
优雅的在WinForm/WPF/控制台 中使用特性封装WebApi 说明 在C/S端作为Server,建立HTTP请求,方便快捷. 1.使用到的类库 Newtonsoft.dll 2.封装 HttpL ...
随机推荐
- Nginx增加网页认证功能
Nginx增加网页认证功能 增加认证功能模块 ngx_http_auth_basic_module 模块实现让访问者,只有输入正确的用户密码才允许访问web内容.web上的一些内容不想被其他人知道,但 ...
- 一些有用的shell命令组合
1.找出Linux系统中磁盘占用最大的10个文件 1)CentOS7 和 busybox 1.30.1 验证可用 find / -type f -print0 | xargs -0 du | sort ...
- toFullScreen:全屏------exitFullscreen:退出全屏
toFullScreen:全屏 function toFullScreen(){ let elem = document.body; elem.webkitRequestFullScreen ? el ...
- php open_basedir的使用
今天跨省问为什么file_exists检测一个相对路径的文件无法获取到true,文件明明有,但是获取不到,我看了一下,感觉可能是因为这个文件是软链接过来的有关系. 然后他找了找发现是和这么一个文件.u ...
- 人形机器人-强化学习算法-PPO算法的实现细节是否会对算法性能有大的影响.
PPO算法是强化学习算法中目前应用最广的算法,虽然这个算法是2017年发表的,但是至今在整个AI领域下的agent子领域中这个算法都是最主要的强化学习算法(至少目前还没有之一),这个算法尤其在Chat ...
- Linux中的文件属性和 文件类型
文件类型及属性 文件属性 每列的含义 [root@oldboyedu ~]# ll -i 33575029 -rw-r--r--. 1 root root 337 Nov 2 10:26 ho ...
- laravel框架之ORM操作
Laravel 支持原生的 SQL 查询.流畅的查询构造器 和 Eloquent ORM 三种查询方式: 流畅的查询构造器(简称DB),它是为创建和运行数据库查询提供的一个接口,支持大部分数据库操作, ...
- 理解Hive 不同组件的功能
Hive功能 通过将SQL转换成MR.Spark等任务,来计算HDFS中数据的工具. Hive是基于Hadoop之上的数仓工具.通过HDFS存储真实的数据,通过YARN运行计算任务(MR.Spark等 ...
- JDBC基础知识
常见连接数据库工具: 图形化工具:点击.拖拽就可以操作数据库,对用户友好,简单对数据操作,复杂数据库操作爱莫能助 JDBC(驱动程序):调用jar包接口 窗口(命令行):输入完整SQL语句对复杂数据库 ...
- vue 路由的代码实现(转)
https://juejin.cn/post/6844904051679870984 需要的使用到的知识 地址变化事件监控 vue插件机制 构造地址和组件的映射关系 定义route-view 组件 当 ...