本文主要讲讲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. 指数循环节 求A的B次方模C

    phi(c)为欧拉函数, 欧拉定理 : 对于互质的正整数 a 和 n ,有 aφ(n)  ≡ 1 mod n  . A^x = A^(x % Phi(C) + Phi(C)) (mod C) (x & ...

  2. Python写ROS 订阅与发布程序

    1. 编写talker代码 vim ..../src/talker.py #!/usr/bin/env python # license removed for brevity import rosp ...

  3. shell :

    示例一.(用作注释,占位)#!/bin/bash : this is single line comment : 'this is a multiline comment, second line e ...

  4. Jmeter自动化测试 数据驱动测试,将数据存入csv文件中来调用,或将数据存在DB中进行调用

    1. 将测试的用例名称,测试请求方式,测试链接,预置数据,断言等都放到excel中,然后转成csv格式,在用Jmeter带的csv数据配置文件导入 运行之前将线程组中配置,线程数设置为1,循环的次数设 ...

  5. 安装nodejs安装npm安装cpnm安装vue-cli脚手架

    1.为什么用nodejs 因为它是javascript运行环境,不同系统直接运行各种编程语言 是我即将使用vue的推荐开发环境. 选择对应版本,https://nodejs.org/en/downlo ...

  6. MongoDB(课时22 唯一索引)

    3.6.1 唯一索引 唯一索引的主要目的是用在某一个字段上,使该字段的内容不重复. 范例:创建唯一索引 db.students.ensureIndex({"name" : 1}, ...

  7. [源][osg][osgBullet]osgBullet例子介绍

    1.BasicDemo 基本样例 一个小玩具飞机坠落,一个立方体盒子在转圈 2.centerofmass 质心 3.collision 碰撞检测 这里有一个大神自己改的例子: http://blog. ...

  8. 从celery rabbitmq with docker-compose 引出对容器、依赖注入、TDD的感悟

    用docker配置项目管理系统taiga的时候,不是我一个人遇到这个问题.https://github.com/douglasmiranda/docker-taiga/issues/5 问题描述: 用 ...

  9. English trip WeekEnd-Lesson 2018.11.10

    本周末上了三节课,做个小结吧\(^o^)/~: [102] 新概念一早读 - 27 - 28        Teacher: March Mrs. Smith's living room is lar ...

  10. [NOI2018]你的名字(68pts)

    SAM真让人头秃. 题面 https://www.luogu.org/problemnew/show/P4770 首先考虑 l=1,r=∣S∣的做法 如果对于ION2018的子串不用判重的话,对ION ...