WinForm“假死”问题汇总
一、一处消息死锁分析
最近维护一个工控机上运行的winform程序,我的前任在一个弹出窗口(窗口B)里面调用了ShowDialog方法弹出对话框(窗口C),导致了一个问题是有时关闭窗口C时程序假死(无规律),最后用windbg和远程调试找到了问题。
解决方法如下:用一个委托来执行ShowDialog。
public delegate DialogResult DelegateShowMessageForm(string msg);
参考资料:一处消息死锁分析
二、Application.DoEvents()解决UI线程调用Invoke/BeginInvoke出现“假死”问题
最近做winform程序时,在主窗口用线程加了个刷新电量的线程(用于实现充电状态的效果),后面导致其他窗口关闭时假死。
用DebugView抓取Debug信息后发现,该窗口的From_Closing事件和Close方法都执行完了,但窗口未关闭。最后将刷新电量的线程取消,改用下文方法,贴上部分代码。
int i = 0; //用于充电时刷新电池图片 private void ChangeBatteryPic(IDModulePower power) { if (!currIDModulePower.Equals(power)) { int picNum = power.QuantityOfBattery; switch (power.PowerStatus) { case IDModulePowerStatus.ExternalPower: picNum = 7; RefreshBatteryPic(picNum); break; case IDModulePowerStatus.BatteryPower: RefreshBatteryPic(picNum); break; case IDModulePowerStatus.OnCharging: { if (i < 6) { i++; } else { i = 0; } RefreshBatteryPic(i); } break; case IDModulePowerStatus.ChargeException: picNum = 6; RefreshBatteryPic(picNum); break; default: break; } currIDModulePower = power; } } public delegate void RefreshControl(int i); private void RefreshBatteryPic(int picNum) { if (this.InvokeRequired) { this.BeginInvoke(new RefreshControl(RefreshBatteryPic), picNum); } else { this.pbBattery.BackgroundImage = VALWELL.SSLC.Resource.Resources.CurrBatteryStatus(picNum); this.pbBattery.BackgroundImageLayout = ImageLayout.Center; Application.DoEvents(); } }
Control的Invoke和BeginInvoke
对于这两个方法,首先我们要有以下的认识:
- Control.Invoke,Control.BeginInvoke和delegate.Invoke,delegate.BeginInvoke是不同的。
- Control.Invoke中的委托方法,执行在主线程,也就是我们的UI线程。而Control.BeginInvoke从命名上来看虽然具有异步调用的特征(Begin),但也仍然执行在UI线程。
- 如果在UI线程中直接调用Invoke和BeginInvoke,数据量偏大时,依然会造成UI的假死。
有很多开发者在初次接触这两个函数时,很容易就将它们同异步联系起来、有些人会认为他们是独立于UI线程之外的工作线程,实际上,他们都被这两个函数的命名所蒙蔽了。如果以传统调用异步的方式,直接调用Control.BeginInvoke,与同步函数的执行无异,UI线程还是会处理所有辛苦的操作,造成我们的应用程序阻塞。
Control.Invoke的调用模型很明确:在UI线程中以代码顺序同步执行,因此,抛开工作线程调用UI元素的干扰,我们可以将Control.Invoke视为同步,本文不做过多介绍。
很多开发者在接触异步后,再来处理窗体假死的问题,很容易想当然的将Control.BeginInvoke视为WinForm封装的异步。
参考资料:谈.Net委托与线程——解决窗体假死