

一个超经典 WinForm 卡死问题的最后一次反思
source link: https://www.cnblogs.com/huangxincheng/p/17654394.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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;
}
}
}
一旦控件在工作线程上被创建,代码内部就会实例化 MarshalingControl
和 WindowsFormsSynchronizationContext
,这里就用前者来探究。
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;
}
}
}
这个脚本供大家参考吧,这里要提醒一下,我实测了下需要在运行时需要反复点以及最小最大话可能会遇到一次,不管怎么说还是非常好的宝贵资料。
关于对 非主线程创建控件
的问题,这已经是第三篇思考了,希望后续不要再写这个主题了。

Recommend
-
20
记一次因 Redis 使用不当导致应用卡死 bug 的排查及解决!文章>记一次因 Redis 使用不当导致应用卡死 bug 的排查及解决!记一次因 Redis 使用不当导致应用卡死 bug 的排查及解决!
-
4
1. 讲故事 上个月底,有位朋友微信找到我,说他的程序 多线程处理 RabbitMQ 时过慢,帮忙分析下什么原因,截图如下: 这问题抛出来,有点懵逼,没说CPU爆高,也没说内存泄漏,也没说程序卡死。。。鬼知道为啥 Rabbitmq 处理过慢哈🤣🤣🤣 。
-
6
1. 讲故事 10月份星球里的一位老朋友找到我,说他们公司的程序在一个网红直播带货下给弄得无响应了,无响应期间有大量的 RabbitMQ 超时,寻求如何找到根源,聊天截图我就不发了。 既然无响应了,那必然是程序的大量线程被主动或者被动的挂起,...
-
6
记一次 .NET 某药品仓储管理系统 卡死分析 既然朋友说 api 有 request 无 response,那怎么去验证朋友的话对不对呢?我们都知道 .NET 用 HttpContext 来表示一个请求,言外之意就是可以去抓 HttpContext 下的时长属性,N...
-
6
记一次Tomcat卡死在 Deploying web application 步骤的问题 ...
-
8
1. 讲故事 这几个月经常被朋友问,为什么不更新这个系列了,哈哈,确实停了好久,主要还是打基础去了,分析 dump 的能力不在于会灵活使用 windbg,而是对底层知识有一个深厚的理解,比如:汇编,C, C++,Win32 Api,虚拟内存,Windows 用户...
-
1
1. 讲故事 前段时间遇到了好几起关于窗体程序的 进程加载锁 引发的 程序卡死 和 线程暴涨 问题,这种 dump 分析难度较大,主要涉及到 Windows操作系统 和 C++ 的基础知识,所以有必要简单整理和大家...
-
6
1. 讲故事 前段时间遇到了一个难度比较高的 dump,经过几个小时的探索,终于给找出来了,在这里做一下整理,希望对大家有所帮助,对自己也是一个总结,好了,老规矩,上 WinDBG 说话。 二:WinDbg 分析
-
7
一个超经典 WinForm 卡死问题的再反思
-
7
基于一次应用卡死问题所做的前端性能评估与优化尝试 在上个月,由于客户反馈客户端卡死现象但我们远程却难以复现此现象,于是我...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK