本文主要讲讲C#窗体的程序中一个经常遇到的情况,就是在退出窗体的时候的,发生了退出的异常。

联系作者及加群方式(激活码在群里发放):http://www.hslcommunication.cn/Cooperation

欢迎技术探讨

我们先来看看一个典型的场景,定时从PLC或是远程服务器获取数据,然后更新界面的标签,基本上实时更新的。我们可以把模型简化,简化到一个form窗体里面,开线程定时读取

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

我们很有可能这么写,当我们点击了窗口的时候,会出现如下的异常

发送这个问题的根本原因在于,当你点击了窗体关闭,窗体所有的组件开始释放资源,但是线程还没有立即关闭,所以针对上诉的代码,我们来进行优化。

1. 优化不停的创建委托实例


        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( );

这样就避免了每隔200ms频繁的创建委托实例,这些委托实例在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 ); // 接下来是跨线程的显示
if(IsHandleCreated && !IsDisposed) Invoke( showInfo, data.ToString( ) ); System.Threading.Thread.Sleep( 200 );
}
}

  

显示界面的时候,判断下句柄是否创建,当前是否被释放。出现异常的频率少了,但是还是会出。下面提供了一个简单粗暴的思路,既然报错已释放,我就捕获异常

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

  

这样子就简单粗暴的解决了,但是还是觉得不太好,所以不采用try..catch,换个思路,我自己新增一个标记,指示窗体是否显示出来。当窗体关闭的时候,复位这个标记

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

整个程序变成了这个样子,我们再多次测试测试,还是经常会出,这时候我们需要考虑更深层次的事了,我程序退出的时候需要做什么事?把采集线程关掉,停止刷新,这时候才能真正的退出

这时候就需要同步技术了,我们继续改造

        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显示的时候,就会形成死锁,所以方式一,改下下面的机制

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

Invoke的时候改成异步的机制,就可以解决这个问题,但是BeginInvoke方法并不是特别的安全的方式,而且我们在退出的时候有可能会卡那么一会会,我们需要想想有没有更好的机制。

如果我们做一个等待的退出的窗口会不会更好?既不卡掉主窗口,又可以完美的退出,我们新建一个小窗口

去掉了边框,界面就是这么简单,然后改造代码

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

我们只要把这个退出前的操作传递给退出窗口即可以

        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 )
{
FormQuit formQuit = new FormQuit( new Action(()=>
{
isWindowShow = false;
resetEvent.WaitOne( );
} ));
formQuit.ShowDialog( );
}
}

至此你的程序再也不会出现上面的对象已经被释放的异常了,退出的窗体显示时间不定时,0-200毫秒。为了有个明显的逗留作用,我们多睡眠200ms,这样我们就完成了最终的程序,如下:

    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(()=>
{
System.Threading.Thread.Sleep( 200 );
isWindowShow = false;
resetEvent.WaitOne( );
} ));
formQuit.ShowDialog( );
}
}

  

C# 跨线程更新UI界面的适当的处理方式,友好的退出界面的机制探索的更多相关文章

  1. 简短总结一下C#里跨线程更新UI(转)

    摘自: http://my.oschina.net/sdqxcxh/blog/53707 跨线程更新UI是写多线程程序尤其是通信类的程序经常遇到的问题,这里面主要的问题是冲突,比如数据线程想要更新UI ...

  2. 简短总结一下C#里跨线程更新UI

    摘自: http://my.oschina.net/sdqxcxh/blog/53707 跨线程更新UI是写多线程程序尤其是通信类的程序经常遇到的问题,这里面主要的问题是冲突,比如数据线程想要更新UI ...

  3. C# Winform 跨线程更新UI控件常用方法汇总(多线程访问UI控件)

    概述 C#Winform编程中,跨线程直接更新UI控件的做法是不正确的,会时常出现“线程间操作无效: 从不是创建控件的线程访问它”的异常.处理跨线程更新Winform UI控件常用的方法有4种:1. ...

  4. C# Winform 跨线程更新UI控件常用方法总结(转)

    出处:http://www.tuicool.com/articles/FNzURb 概述 C#Winform编程中,跨线程直接更新UI控件的做法是不正确的,会时常出现“线程间操作无效: 从不是创建控件 ...

  5. (转).NET 4.5中使用Task.Run和Parallel.For()实现的C# Winform多线程任务及跨线程更新UI控件综合实例

    http://2sharings.com/2014/net-4-5-task-run-parallel-for-winform-cross-multiple-threads-update-ui-dem ...

  6. Winform之跨线程更新UI

    Winform之跨线程更新UI 使用`Invoke`或者`BeginInvoke`与UI线程交互示例 参考及源码 使用Invoke或者BeginInvoke与UI线程交互示例 private void ...

  7. C# 跨线程更新 UI

    Winforms 跨线程更新 UI 在 Winforms 中, 所有的控件都包含 InvokeRequired 属性, 如果我们要更新UI,通过它我们可以判断是否需要调用 [Begin]Invoke. ...

  8. C# WinForms跨线程更新 UI

    与在Android中一样, 子线程中更新UI被认为是线程不安全的, 会抛出异常. 子线程返回UI线程中更新UI的一个方法为: 1, 捕获应用的UI线程的上下文; 2, 定义线程任务; 3, 定义线程任 ...

  9. C#利用委托跨线程更新UI数据

    转:http://www.2cto.com/kf/201206/136587.html 在使用C#的过程中,难免会用到多线程,而用多线程之后,线程如何与界面交互则是一个非常头疼的问题.其实不仅仅是界面 ...

随机推荐

  1. Linux command line exercises for NGS data processing

    by Umer Zeeshan Ijaz The purpose of this tutorial is to introduce students to the frequently used to ...

  2. ros 编译包含脚本文件以及launch文件

    目录结构如下: 修改CMakeLists.txt文件 install(PROGRAMS scripts/initial_pos.py DESTINATION ${CATKIN_PACKAGE_BIN_ ...

  3. python datetime模块来获取当前的日期和时间

    #!/usr/bin/python # -*- coding: UTF- -*- import datetime i = datetime.datetime.now() print ("当前 ...

  4. Selenium 对元素element的操作举例

    前三个用的比较多,模拟用户操作最多的就是点击,输入文本,其他的UI自动化测试中涉及的不多,对判断元素是否存在,断言元素是否被选中,元素被选中后CSS属性是否更改等,还是很有必要的.

  5. 让flask在出现语法错误时仍然自动重启

    1问题描述: flask自带的reload只能在语法没毛病的情况下auto_relaod,但是如果有语法错误,进程就会报错退出. 这时修改完语法错误,还得在控制台按“↑”和“enter”重新执行一次p ...

  6. NGUI中 鼠标划出屏幕后,停止对 UIDragScrollView 的 press

    using UnityEngine; /// <summary> /// NGUI中 鼠标划出屏幕后,停止对 UIDragScrollView 的 press /// </summa ...

  7. C#中的约束类型

  8. Yii 语言设置 中文提示信息

    1.  在main.php配置文件中加入 'language'=>'zh_cn', 注: 在URL中追加参数lang=zh_cn即可实现中文 2.  在Controller方法中添加 publi ...

  9. php正则表达式的三个最基本原则分享

    我个人认为,正则表达式的常规用法可以分为如下三个最基本的原则:1.找谁.2.怎么找.3.找它干什么. 接下来,我分享一下一个正则表达式分三个部分: 原子字符 . 匹配除换行符以外的任意字符 \w 匹配 ...

  10. Mishka and Divisors CodeForces - 703E

    大意: 给定$n$个数, 求选择最少的数满足积为$k$的倍数, 并且和最小 刚开始想着暴力维护$k$的素因子向量, 用map转移, 结果T了. 看了下别的dala0题解, 不需要考虑素因子, 我们考虑 ...