首页 > 其他分享 >一个超经典 WinForm 卡死问题的最后一次反思

一个超经典 WinForm 卡死问题的最后一次反思

时间:2023-08-24 16:25:04浏览次数:33  
标签:btnFreezeEm Windows System private Forms uint 反思 卡死 WinForm

一:背景

1. 讲故事

在我分析的 200+ dump 中,同样会遵循着 28原则,总有那些经典问题总是反复的出现,有很多的朋友就是看了这篇 一个超经典 WinForm 卡死问题的再反思 找到我,说 WinDbg 拦截 System_Windows_Forms_ni System.Windows.Forms.Application+MarshalingControl..ctor 总会有各种各样的问题,而且 windbg 也具有强侵入性,它的附加进程方式让很多朋友望而生畏!

这一篇我们再做一次反思,就是如何不通过 WinDbg 找到那个 非主线程创建的控件,那到底用什么工具的? 对,就是用 Perfview 的墙钟模式。

二:Perview 的墙钟调查

1. 测试案例

我还是用上一篇提到的案例,用 backgroundWorker1 的工作线程去创建一个 Button 控件来模拟这种现象,参考代码如下:


namespace WindowsFormsApp2
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {

        }

        private void button1_Click_1(object sender, EventArgs e)
        {
            backgroundWorker1.RunWorkerAsync();
        }

        private void backgroundWorker1_DoWork_1(object sender, DoWorkEventArgs e)
        {
            Button btn = new Button();
            var query = btn.Handle;
        }
    }
}

一旦控件在工作线程上被创建,代码内部就会实例化 MarshalingControlWindowsFormsSynchronizationContext,这里就用前者来探究。

2. 寻找 MarshalingControl 调用栈

那怎么去寻找这个调用栈呢?在 perfview 中有一个 Thread Time 复选框,它可以记录到 Thread 的活动轨迹,在活动轨迹中寻找我们的目标类 MarshalingControl 即可,有了思路之后说干就干,命令行下的参考代码:


PerfView.exe  "/DataFile:PerfViewData.etl" /BufferSizeMB:256 /StackCompression /CircularMB:500 /KernelEvents:ThreadTime /NoGui /NoNGenRundown collect

当然也可以在 Focus process 中输入你的进程名来减少 Size,启动 prefview 监控之后,我们打开程序,点击 Button 按钮之后,停止 Prefview 监控,稍等片刻之后我们打开 Thread Time Stacks,检索我们要的 MarshalingControl 类, 截图如下:

从卦中可以看到如下三点信息:

  • 当前 prefview 录制了 34.7s
  • MarshalingControl.ctor 有 2 个实例
  • 二次实例化分别在 22.84s 和 24.12s

接下来可以右键选择 Goto -> Goto Item in Callers 看一下它的 Callers 到底都是谁?截图如下:

从卦中可以清晰的看到如下信息:

  • 第一个实例是由 System.Windows.Forms.ScrollableControl..ctor() 触发的。

  • 第二个实例是由 System.Windows.Forms.ButtonBase..ctor() 触发的。

大家可以逐一的去探究,第一个实例是窗体自身的 System.Windows.Forms.Form ,后者就是那个罪魁祸首,卦中信息非常清楚指示了来自于 WindowsFormsApp2.Form1.backgroundWorker1_DoWork_1,是不是非常的有意思?

3. 如何让窗体尽可能早的卡死

所谓的尽早卡死就是尽可能早的让主线程出现如下调用栈。


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 时间的 new button,可能在 t10 时间因为某些操作才会出现程序卡死,所以你会被迫用 prefview 一直监视,而一直监视就会导致生成太多的 etw 事件,总之很搞的。

先感谢下上海的包老师 提供的一段很棒的脚本,也经过了老师实测
让这个问题解决起来更加完美 ❤

这里我用 ILSpy 反编译一下这个执行程序,完整代码如下:


// Freezer.FreezerForm
using System;
using System.ComponentModel;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using Freezer;

public class FreezerForm : Form
{
	private Button btnFreezeEm;

	private Container components = null;

	private const uint WM_SETTINGCHANGE = 26u;

	private const uint HWND_BROADCAST = 65535u;

	private const uint SMTO_ABORTIFHUNG = 2u;

	public FreezerForm()
	{
		InitializeComponent();
	}

	protected override void Dispose(bool disposing)
	{
		if (disposing && components != null)
		{
			components.Dispose();
		}
		base.Dispose(disposing);
	}

	private void InitializeComponent()
	{
		btnFreezeEm = new System.Windows.Forms.Button();
		SuspendLayout();
		btnFreezeEm.Location = new System.Drawing.Point(89, 122);
		btnFreezeEm.Name = "btnFreezeEm";
		btnFreezeEm.Size = new System.Drawing.Size(115, 23);
		btnFreezeEm.TabIndex = 0;
		btnFreezeEm.Text = "Freeze 'em!";
		btnFreezeEm.Click += new System.EventHandler(btnFreezeEm_Click);
		AutoScaleBaseSize = new System.Drawing.Size(6, 15);
		base.ClientSize = new System.Drawing.Size(292, 267);
		base.Controls.Add(btnFreezeEm);
		base.Name = "FreezerForm";
		Text = "Freezer";
		ResumeLayout(false);
	}

	[DllImport("user32.dll")]
	private static extern uint SendMessageTimeout(uint hWnd, uint msg, uint wParam, string lParam, uint flags, uint timeout, out uint result);

	[STAThread]
	private static void Main()
	{
		Application.Run(new FreezerForm());
	}

	private void btnFreezeEm_Click(object sender, EventArgs e)
	{
		try
		{
			Cursor = Cursors.WaitCursor;
			SendMessageTimeout(65535u, 26u, 0u, "Whatever", 2u, 5000u, out var _);
		}
		finally
		{
			Cursor = Cursors.Arrow;
		}
	}
}

这个脚本供大家参考吧,这里要提醒一下,我实测了下需要在运行时需要反复点以及最小最大话可能会遇到一次,不管怎么说还是非常好的宝贵资料。

三:总结

关于对 非主线程创建控件 的问题,这已经是第三篇思考了,希望后续不要再写这个主题了。

图片名称

标签:btnFreezeEm,Windows,System,private,Forms,uint,反思,卡死,WinForm
From: https://www.cnblogs.com/huangxincheng/p/17654394.html

相关文章

  • Winform项目中出现 "已经可见的窗体不能显示为模式对话框。在调用 showDialog 之前应
    1问题描述最近做一个winform项目,启动程序弹出的加载进度窗体时,发生如标题所示的异常。2尝试debug根据异常提示,在进度窗体弹出前添加代码Visable=false;--未解决逐步debug调试发现Form弹框运行了2次,由此查出bug所在。由于我是用的单例模式,在Program.cs中运行的还是new......
  • WinForm窗口拖动
    privatePointmypoint;privatevoidpanel1_MouseDown(objectsender,MouseEventArgse){mypoint=newPoint(-e.X,-e.Y);}privatevoidpanel1_MouseMove(objectsender,MouseEventArgse){......
  • winform双向绑定如何实现
    在使用WinForm编写测试小软件的时候,经常需要把数值通过Textbox显示出来。通常的做法为把改变后的数值显示在textbox上重新显示,需要通过数值重新赋值给相应的textbox.text属性才可显示。这个方法虽然很简单,但是在数值多的时候,这个做法非常麻烦而且不容易维护。那么有没有一种简单的......
  • inno setup 6 打包C# winform 程序安装包
    注:checkablealone"选中"意思unchecked"非选中"意思图标选择:IconFilename:"{app}\Images\SysIcon.ico"#defineMyAppName"LuoCore"#defineMyAppVersion"1.5"#defineMyAppPublisher"LuoCore"#define......
  • C#.NET WINFORM 缓存 System.Runtime.Caching MemoryCache
    C#.NETWINFORM缓存 System.Runtime.CachingMemoryCache 工具类:usingSystem;usingSystem.Runtime.Caching;namespaceCommonUtils{///<summary>///基于MemoryCache的缓存辅助类///</summary>publicstaticclassMemoryCacheHelper{......
  • C#的winform如何嵌套另一个exe程序
    这篇文章主要介绍了C#的winform如何嵌套另一个exe程序问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教 −目录C#winform嵌套另一个exe程序第一种第二种总结C#winform嵌套另一个exe程序一共有二种方法,也不知道作者从哪里复制来的,......
  • c#、winform数据统计,需要统计三四张表的数据,在一个界面上显示
    前几天我就遇到了这个需求,我之前实习写过.noodjs,处理过一些这样的需求,不过那时候我很菜,都是SQL大法整出来的,但是SqlServer就没有MySQL那么好用了,sql写复杂了对效率和稳定性会有不小的影响我现在的需求就是我先截一张图,给各位看看,需求是什么四张表,一个表未基本表,另外三张表分别......
  • DevExpress WinForms数据编辑器组件,提供丰富的数据输入样式!(一)
    DevExpressWinForms超过80个高影响力的WinForms编辑器和多用途控件,从屏蔽数据输入和内置数据验证到HTML格式化,DevExpress数据编辑库提供了无与伦比的数据编辑选项,包括用于独立数据编辑或用于容器控件(如Grid,TreeList和Ribbon)的单元格。PS:DevExpressWinForm拥有180+组件和UI......
  • winform编译时怎么把指定dll拷贝到debug里面(非引用)
    选中winform项目-》右键属性-》选择生成事件-》在生成后事件命令行配置以下代码Copy"$(ProjectDir)dll\*.*""$(ProjectDir)$(OutDir)" ......
  • Winform控件自适应窗体大小
    思路[参考他人]:1>保存窗体的初始宽度和高度;2>保存窗体内所有控件的初始宽度,初始高度和坐标;3>窗体的Resize事件触发时,计算新的Size和初始Size的比例prec;4>遍历窗体内所有控件,将其的Size和坐标乘以prec;代码:窗体注册,保存窗体及其所有控件的初始尺寸,编写Resize事件逻辑:public......