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

一个,经典,winform,卡死,问题,最后,一次,反思 · 浏览次数 : 3419

小编点评

**背景** * 200+ dump 中反复出现经典问题,例如 WinForm 卡死问题。 * WinDbg 具有强壮的调试功能,但有时无法尽早检测程序卡死问题。 **问题** 如何解决非主线程创建控件时程序卡死的问题? **解决方案** 1. 使用 `System.Windows.Forms.Control.SetStyle()` 方法设置控件的 `IsUserCreated` 属性。 2. 在 `Paint` 事件中绘制一个隐藏的空窗,覆盖实际窗体。 3. 使用 `SetTimer()` 函数定期触发一个事件,检查窗体是否已创建。 4. 使用 `CreateControl()` 方法创建新的控件,并将其设置为 `Control.Visible` 为 `false`。 **示例代码** ```csharp // 设置控件的 IsUserCreated 属性 btnFreezeEm.SetStyle(ControlStyles.IsUserCreated, true); // 在 Paint 事件中绘制一个隐藏的空窗 private void Form1_Paint(object sender, PaintEventArgs e) { e.DrawControl(new Rectangle(0, 0, 10, 10), Color.Black); } // 创建一个计时器,每 1000 个毫秒触发一次事件 private void Timer_Tick(object sender, EventArgs e) { if (btnFreezeEm.Created) { // 设置窗体的可见性 btnFreezeEm.Visible = true; } } ``` **其他提示** * 使用 `Marshal.GetExceptionInfo()` 获取更多错误信息。 * 在测试代码中使用 `Application.SetUnhandledException(null)` 设置异常处理程序。 * 考虑使用第三方库,例如 `WpfSharp`,简化窗体的创建过程。

正文

一:背景

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;
		}
	}
}

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

三:总结

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

图片名称

与一个超经典 WinForm 卡死问题的最后一次反思相似的内容:

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

## 一:背景 ### 1. 讲故事 在我分析的 200+ dump 中,同样会遵循着 28原则,总有那些经典问题总是反复的出现,有很多的朋友就是看了这篇 [一个超经典 WinForm 卡死问题的再反思](https://www.cnblogs.com/huangxincheng/p/1686848

一个超经典 WinForm 卡死问题的再反思

一:背景 1.讲故事 这篇文章起源于昨天的一位朋友发给我的dump文件,说它的程序出现了卡死,看了下程序的主线程栈,居然又碰到了 OnUserPreferenceChanged 导致的挂死问题,真的是经典中的经典,线程栈如下: 0:000:x86> !clrstack OS Thread Id: 0

带你认识JDK8中超nice的Native Memory Tracking

摘要:从 OpenJDK8 起有了一个很 nice 的虚拟机内部功能: Native Memory Tracking (NMT)。 本文分享自华为云社区《Native Memory Tracking 详解(1):基础介绍》,作者:毕昇小助手。 0.引言 我们经常会好奇,我启动了一个 JVM,他到底会

可视化—gojs 超多超实用经验分享(三)

目录32.go.Palette 一排放两个33.go.Palette 基本用法34.创建自己指向自己的连线35.设置不同的 groupTemplate 和 linkTemplate36.监听在图形对象 GraphObject 上的右键单击37.定义节点/连线/canvas 背景上的右键菜单38.从节

可视化—gojs 超多超实用经验分享(四)

目录41.监听连线拖拽结束后的事件42.监听画布的修改事件43.监听节点被 del 删除后回调事件(用于实现调用接口做一些真实的删除操作)44.监听节点鼠标移入移出事件,hover 后显示特定元素45.监听树图实现鼠标点击节点本身展开或收起子节点的功能,而不是点击另外的按钮46.监听文本块编辑结束后

MySQL基础知识(二)-超详细 Linux安装MySQL5.7完整版教程及遇到的坑

1.简介 我们经常会在Linux上安装MySQL数据库,但是安装的时候总是会这里错,那里错,不顺利,今天整理了一下安装流程,连续安装来了两遍,没有遇到什么大错误,基本上十分钟左右可以搞定,教程如下。写着一篇文章主要是答应别人要帮忙给他在Linux上安装一下mysql(MySQL是5.7,Linux是

Scratch3之AI集成 - flappy bird AI版本

AI神秘且有趣,我们一个经典的游戏flappy bird集成AI,实现自训练成长的聪明的笨鸟。先上效果: 初始化的笨鸟拥有分身,每个分身都有自我学习功能,根据自己的移动轨迹和得分情况进行汇总,进行新一代的笨鸟的迭代,基本经过数十轮的训练和迭代,能完成高智慧的笨鸟,自我闯关能力强。 需要了解的AI知识

[转帖]A-Ops性能火焰图——适用于云原生的全栈持续性能监测工具

https://www.modb.pro/db/610990 对于开发及运维人员来讲,火焰图是一个经典的定位性能问题的方法。利用火焰图可以可视化系统资源(cpu占用、内存占用、调度、IO等)的占用情况,从而帮助技术人员快速定位资源异常使用的代码级根因,或者观察潜在性能劣化趋势,进而优化系统和应用的性

记一次 .NET 某安全生产信息系统 CPU爆高分析

一:背景 1.讲故事 今天是🐏的第四天,头终于不巨疼了,写文章已经没什么问题,赶紧爬起来写。 这个月初有位朋友找到我,说他的程序出现了CPU爆高,让我帮忙看下怎么回事,简单分析了下有两点比较有意思。 这是一个安全生产的信息管理平台,第一次听说,我的格局小了。 这是一个经典的 CPU 爆高问题,过往

[转帖]图解:什么是红黑树?

https://zhuanlan.zhihu.com/p/273829162 注:本文比较硬核但是很值得大家花心思看完,看完你一定会有所收获的 红黑树是面试中一个很经典也很有难度的知识点,网传字节跳动面试官最喜欢问这个问题。很多人会觉得这个知识点太难,不想花太多功夫去了解,也有人会认为这个数据结构在