前言
WinForm 应用程序开发中,跨线程更新UI和程序的优雅退出是两个非常常见且重要的技术点。当我们在后台线程中执行耗时操作(如从PLC或远程服务器读取数据)并需要将结果显示在UI界面上时,必须处理跨线程访问的问题。
同时,在程序退出时,如果后台线程仍在运行,很容易引发"对象已释放"等异常。
本文将深入探讨这些问题的根源,并提供一系列逐步优化的解决方案,最终实现一个既安全又友好的程序退出机制。
问题场景与初始实现
我们考虑一个典型的实时数据监控场景:一个窗体程序需要定时从PLC或远程服务器获取数据,并更新界面上的标签。
为了不阻塞UI线程,数据获取操作通常在后台线程中执行。
public partial class Form1 : Form
{
public Form1( )
{
InitializeComponent( );
}
private void Form1_Load( object sender, EventArgs e )
{
thread = new System.Threading.Thread( new System.Threading.ThreadStart( ThreadCapture ) );
thread.IsBackground = true;
thread.Start( );
}
private void ThreadCapture( )
{
System.Threading.Thread.Sleep( 200 );
while (true)
{
// 我们假设这个数据是从PLC或是远程服务器获取到的,因为可能比较耗时,我们放在了后台线程获取,并且处于一直运行的状态
// 我们还假设获取数据的频率是200ms一次,然后把数据显示出来
int data = random.Next( 1000 );
// 接下来是跨线程的显示
Invoke( new Action( ( ) =>
{
label1.Text = data.ToString( );
} ) );
System.Threading.Thread.Sleep( 200 );
}
}
private System.Threading.Thread thread;
private Random random = new Random( );
}
这种实现方式在程序正常运行时工作良好,但当用户点击窗口关闭按钮时,经常会遇到异常。这是因为窗体关闭过程中,UI组件开始释放资源,而后台线程可能仍在尝试访问这些已被释放的组件。
优化方案一 避免频繁创建委托实例
首先,我们可以优化代码,避免在循环中频繁创建委托实例,以减少内存开销和GC压力。
private void ThreadCapture( )
{
showInfo = new Action<string>( m =>
{
label1.Text = m;
} );
System.Threading.Thread.Sleep( 200 );
while (true)
{
// 我们假设这个数据是从PLC或是远程服务器获取到的,因为可能比较耗时,我们放在了后台线程获取,并且处于一直运行的状态
// 我们还假设获取数据的频率是200ms一次,然后把数据显示出来
int data = random.Next( 1000 );
// 接下来是跨线程的显示
Invoke( showInfo, data.ToString( ) );
System.Threading.Thread.Sleep( 200 );
}
}
private Action<string> showInfo;
private System.Threading.Thread thread;
private Random random = new Random( );
优化方案二:增加安全检查
为了防止在窗体已释放后仍尝试更新UI,可以在更新前进行安全检查。
private void ThreadCapture( )
{
showInfo = new Action<string>( m =>
{
label1.Text = m;
} );
System.Threading.Thread.Sleep( 200 );
while (true)
{
// 我们假设这个数据是从PLC或是远程服务器获取到的,因为可能比较耗时,我们放在了后台线程获取,并且处于一直运行的状态
// 我们还假设获取数据的频率是200ms一次,然后把数据显示出来
int data = random.Next( 1000 );
// 接下来是跨线程的显示
if(IsHandleCreated && !IsDisposed) Invoke( showInfo, data.ToString( ) );
System.Threading.Thread.Sleep( 200 );
}
}
优化方案三:使用异常捕获
虽然不推荐作为主要手段,但可以使用try-catch来捕获ObjectDisposedException异常。
private void ThreadCapture( )
{
showInfo = new Action<string>( m =>
{
label1.Text = m;
} );
System.Threading.Thread.Sleep( 200 );
while (true)
{
// 我们假设这个数据是从PLC或是远程服务器获取到的,因为可能比较耗时,我们放在了后台线程获取,并且处于一直运行的状态
// 我们还假设获取数据的频率是200ms一次,然后把数据显示出来
int data = random.Next( 1000 );
try
{
// 接下来是跨线程的显示
if (IsHandleCreated && !IsDisposed) Invoke( showInfo, data.ToString( ) );
}
catch (ObjectDisposedException)
{
break;
}
catch
{
throw;
}
System.Threading.Thread.Sleep( 200 );
}
}
优化方案四:使用退出标志
引入一个布尔标志来指示窗体是否仍处于显示状态。
private void ThreadCapture( )
{
showInfo = new Action<string>( m =>
{
label1.Text = m;
} );
isWindowShow = true;
System.Threading.Thread.Sleep( 200 );
while (true)
{
// 我们假设这个数据是从PLC或是远程服务器获取到的,因为可能比较耗时,我们放在了后台线程获取,并且处于一直运行的状态
// 我们还假设获取数据的频率是200ms一次,然后把数据显示出来
int data = random.Next( 1000 );
// 接下来是跨线程的显示
if (isWindowShow) Invoke( showInfo, data.ToString( ) );
else break;
System.Threading.Thread.Sleep( 200 );
}
}
private bool isWindowShow = false;
private Action<string> showInfo;
private System.Threading.Thread thread;
private Random random = new Random( );
private void Form1_FormClosing( object sender, FormClosingEventArgs e )
{
isWindowShow = false;
}
优化方案五:使用同步事件
为了确保后台线程完全停止后再关闭窗体,可以使用AutoResetEvent进行同步。
private void ThreadCapture( )
{
showInfo = new Action<string>( m =>
{
label1.Text = m;
} );
isWindowShow = true;
System.Threading.Thread.Sleep( 200 );
while (true)
{
// 我们假设这个数据是从PLC或是远程服务器获取到的,因为可能比较耗时,我们放在了后台线程获取,并且处于一直运行的状态
// 我们还假设获取数据的频率是200ms一次,然后把数据显示出来
int data = random.Next( 1000 );
// 接下来是跨线程的显示,并检测窗体是否关闭
if (isWindowShow) Invoke( showInfo, data.ToString( ) );
else break;
System.Threading.Thread.Sleep( 200 );
// 再次检测窗体是否关闭
if (!isWindowShow) break;
}
// 通知主界面是否准备退出
resetEvent.Set( );
}
private System.Threading.AutoResetEvent resetEvent = new System.Threading.AutoResetEvent( false );
private bool isWindowShow = false;
private Action<string> showInfo;
private System.Threading.Thread thread;
private Random random = new Random( );
private void Form1_FormClosing( object sender, FormClosingEventArgs e )
{
isWindowShow = false;
resetEvent.WaitOne( );
}
优化方案六:使用异步调用避免死锁
为了避免Invoke可能引起的死锁,可以改用BeginInvoke。
private void ThreadCapture( )
{
showInfo = new Action<string>( m =>
{
label1.Text = m;
} );
isWindowShow = true;
System.Threading.Thread.Sleep( 200 );
while (true)
{
// 我们假设这个数据是从PLC或是远程服务器获取到的,因为可能比较耗时,我们放在了后台线程获取,并且处于一直运行的状态
// 我们还假设获取数据的频率是200ms一次,然后把数据显示出来
int data = random.Next( 1000 );
// 接下来是跨线程的显示,并检测窗体是否关闭
if (isWindowShow) BeginInvoke( showInfo, data.ToString( ) );
else break;
System.Threading.Thread.Sleep( 200 );
// 再次检测窗体是否关闭
if (!isWindowShow) break;
}
// 通知主界面是否准备退出
resetEvent.Set( );
}
private System.Threading.AutoResetEvent resetEvent = new System.Threading.AutoResetEvent( false );
private bool isWindowShow = false;
private Action<string> showInfo;
private System.Threading.Thread thread;
private Random random = new Random( );
private void Form1_FormClosing( object sender, FormClosingEventArgs e )
{
isWindowShow = false;
resetEvent.WaitOne( );
}
最终方案:友好的退出窗口
为了提供更好的用户体验,可以创建一个专门的退出窗口来处理清理工作。
public partial class FormQuit : Form
{
public FormQuit( Action action )
{
InitializeComponent( );
this.action = action;
}
private void FormQuit_Load( object sender, EventArgs e )
{
}
// 退出前的操作
private Action action;
private void FormQuit_Shown( object sender, EventArgs e )
{
// 调用操作
action.Invoke( );
Close( );
}
}
主窗体的最终实现:
public partial class Form1 : Form
{
public Form1( )
{
InitializeComponent( );
}
private void Form1_Load( object sender, EventArgs e )
{
thread = new System.Threading.Thread( new System.Threading.ThreadStart( ThreadCapture ) );
thread.IsBackground = true;
thread.Start( );
}
private void ThreadCapture( )
{
showInfo = new Action<string>( m =>
{
label1.Text = m;
} );
isWindowShow = true;
System.Threading.Thread.Sleep( 200 );
while (true)
{
// 我们假设这个数据是从PLC或是远程服务器获取到的,因为可能比较耗时,我们放在了后台线程获取,并且处于一直运行的状态
// 我们还假设获取数据的频率是200ms一次,然后把数据显示出来
int data = random.Next( 1000 );
// 接下来是跨线程的显示,并检测窗体是否关闭
if (isWindowShow) Invoke( showInfo, data.ToString( ) );
else break;
System.Threading.Thread.Sleep( 200 );
// 再次检测窗体是否关闭
if (!isWindowShow) {System.Threading.Thread.Sleep(50);break;}
}
// 通知主界面是否准备退出
resetEvent.Set( );
}
private System.Threading.AutoResetEvent resetEvent = new System.Threading.AutoResetEvent( false );
private bool isWindowShow = false;
private Action<string> showInfo;
private System.Threading.Thread thread;
private Random random = new Random( );
private void Form1_FormClosing( object sender, FormClosingEventArgs e )
{
FormQuit formQuit = new FormQuit( new Action(()=>
{
isWindowShow = false;
resetEvent.WaitOne( );
} ));
formQuit.ShowDialog( );
}
}
总结
本文通过一个实际的开发场景,系统地探讨了WinForm应用中跨线程更新UI和程序退出的问题。我们从最简单的实现开始,逐步分析了各种潜在问题,并提出了相应的优化方案。
关键要点总结
1、避免资源浪费:在循环中避免频繁创建委托实例,应将其定义为成员变量。
2、安全检查:在跨线程更新UI前,检查IsHandleCreated和!IsDisposed状态。
3、优雅退出:使用布尔标志和同步事件(如AutoResetEvent)确保后台线程完全停止后再关闭窗体。
4、避免死锁:在可能引起死锁的场景下,考虑使用BeginInvoke替代Invoke。
5、用户体验:通过专门的退出窗口处理清理工作,可以提供更友好、更可靠的退出体验。
通过这些优化,我们可以构建出既稳定又用户友好的WinForm应用程序。
关键词
跨线程更新、UI界面、WinForms、线程安全、Invoke、BeginInvoke、AutoResetEvent、异步调用、程序退出、异常处理、后台线程
最后
如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。
也可以加入微信公众号 [DotNet技术匠] 社区,与其他热爱技术的同行一起交流心得,共同成长!
优秀是一种习惯,欢迎大家留言学习!
作者:dathlin
出处:cnblogs.com/dathlin/p/10147736.html
声明:网络内容,仅供学习,尊重版权,侵权速删,歉意致谢!