WinForm二三事(二)

监视消息循环

在上一篇文章中,我们讨论了消息循环是响应用户输入的根本,还提到了在WinForm中执行耗时操作是因为这个耗时操作与消息循环在同一个UI Thread上,导致不能处理用户的后续响应,造成程序假死。除此之外,还说到了Form中的WndProc方法,说这个方法就是Win32时代那个处理消息的方法的.Net版。

那么今天这篇文章我们就来编个小程序来模拟一下这个耗时操作,看看是不是如上一篇所说:耗时操作造成消息循环的临时中断不能响应用户后续输入。

程序很简单,就是一个简单的窗体,上面放置一个按钮,按钮里有一个Thread.Sleep(50*1000)模拟耗时操作:

public partial class LongTimeForm : Form{ public LongTimeForm() { InitializeComponent(); Debug.Listeners.Add(new ConsoleTraceListener()); }  private void btnLongTime_Click(object sender, EventArgs e) { Thread.Sleep(50 * 1000); }  //既然这个WndProc是Win32中处理消息的方法的.Net版,
那么我们应该在这里可以监视到所有用户操作的“消息” protected override void WndProc(ref Message m) { Debug.WriteLine(m.Msg.ToString()); base.WndProc(ref m); }}

WndProc是一个虚方法,我们可以override,那么我们就来看看当用户点击“耗时操作”的按钮后,这个方法是不是还能接收到用户其他的输入呢。

小技巧

在开发WinForm程序时,为了方便显示程序的一些操作日志,我们经常将项目属性里的“Windows Application”项目类型修改为“Console Application”,这样在启动程序后,除了会显示窗体外,还会显示一个控制台,在控制台里会显示程序里通过Debug.Write等输出的日志。当产品发布的时候,我们可以将项目属性修改回”Windows Application”。

启动程序,鼠标在窗体上滑动,后面的控制台上就会显示很多的数字,这些都是消息循环从消息队列里取出的消息(数字是消息的类别,在Windows的一个头文件里定义),然后传递给WndProc方法处理的,控制台上还能输出数字说明现在消息循环还是“流畅的”。当我们点击“耗时操作”按钮后,我们发现,窗体这个时候“死掉了”,不能再接受用户任何的操作,而不管鼠标如何在窗体上滑动、点击,后面的控制台没有一条输出。唔,消息循环阻塞了。Thread.Sleep是50s时间,50s后程序又活过来了,控制台上又有源源不断的输出了。

虽然,我们用实例证明了上一篇所说的东西貌似是正确的,但是难道我们就对这种耗时操作无能为力了么?不啊,我们有多线程啊,我们有异步操作啊。我们就来看看如何使用异步操作来处理这种耗时操作。

使用委托中的BeginInvoke进行异步操作

还记得委托的BeginInvoke方法和EndInvoke方法么?今儿我们就用这两个方法来做一个异步的操作(.Net中如果你看到这种BeginXXX和EndXXX成对出现的方法,那说明就是可以进行异步操作了)。

当你定义一个委托后,编译器会自动的为你生成一个类,还会为你在这个类里提供一个BeginInvoke方法和一个EndInvoke方法,这两个方法的实现是由CLR提供的,而这个BeginInvoke和EndInvoke只是起一个包装的作用。我们还是先来看看将上面的耗时操作修改为异步的代码吧:

public partial class LongTimeForm : Form{ public LongTimeForm() { InitializeComponent(); Debug.Listeners.Add(new ConsoleTraceListener()); }  private void LongTimeMethod() { Thread.Sleep(50 * 1000); }  private void btnLongTime_Click(object sender, EventArgs e) { //咱们这儿就不自己定义新的委托了,.Net为我们定义了一串的通用委托使用 Action longTimeAction = new Action(LongTimeMethod); longTimeAction.BeginInvoke(null, null); } protected override void WndProc(ref Message m) { Debug.WriteLine(m.Msg.ToString()); base.WndProc(ref m); }}

现在再来运行程序。哇塞,虽然“耗时操作”的按钮点击后,消息循环继续进行,界面也没有假死。嗯,这才是我们要的用户体验(当然,我们应该还给用户一些提示,说耗时操作正在进行,请不要关闭窗口)。

接下来我们来看看这个异步操作是咋实现的。首先,我们在LongTimeMethod方法里设置断点,点击“耗时操作”按钮后,等待命中断点,断点命中后,选择Visual Studio的“Debug”->”Windows”->”Threads”,这样会打开线程的窗口,在这里我们可以看到类似下图的图片:

有箭头指示的是正在执行的LongTimeMethod方法的Worker Thread,从这里可以看出Main方法是Main Thread中,看来BeginInvoke就是从Thread Pool中选择一个空闲线程来执行我们的耗时操作。我提到这里是因为经常有人问我:异步和多线程有什么区别?其实,异步是目的,而多线程是实现这个目的的方法。异步是说,A发起一个操作后(一般都是比较耗时的操作,如果不耗时的操作就没有必要异步了),可以继续自顾自的处理它自己的事儿,不用干等着这个耗时操作返回。.Net中的这种异步编程模型,就简化了多线程编程,我们甚至都不用去关心Thread类,就可以做一个异步操作出来。

去了还要回

实际上上面演示的耗时操作是“一去不复返”的操作(相当于WCF中的One-Way操作),也就是我发起这个操作后,我就不用管它了,我甚至不关心它运算的结果。但大部分时候我们需要这样的操作:执行完后返回来更新一下UI,比如告诉用户一声我执行完了或者显示执行结果。那这样我们就要考虑异步调用的几种方式了。如果我们要从异步操作里获取结果,我们就得调用EndInvoke方法,那我们又用什么手段来得到异步操作完成的信号呢?因为如果异步操作没有完成,我们就直接调用EndInvoke方法,这样就会阻塞,一直等到异步操作执行完毕后才会执行。

在继续讨论之前我们来看看BeginInvoke的返回值:

1: public interface IAsyncResult 2: { 3: object AsyncState { get; } 4:  5: WaitHandle AsyncWaitHandle { get; } 6:  7: bool CompletedSynchronously { get; } 8:  9: bool IsCompleted { get; } 10: }

根据BeginInvoke返回的结果,我们就有两种调用异步操作的方式:

轮询

IAsyncResult的IsCompleted属性会在异步操作结束后返回true,否则返回false。那么我们就可以用一个循环不断的访问IsCompleted属性,当IsCompleted为true的时候再调用EndInvoke方法:

1: public partial class LongTimeForm : Form 2: { 3: public LongTimeForm() 4: { 5: InitializeComponent(); 6: Debug.Listeners.Add(new ConsoleTraceListener()); 7: } 8:  9: private int LongTimeMethod() 10: { 11: Thread.Sleep(50 * 1000); 12: return 10; 13: } 14:  15: private void btnLongTime_Click(object sender, EventArgs e) 16: { 17: //咱们这儿就不自己定义新的委托了,.Net为我们定义了一串的通用委托使用 18: Func<int> longTimeAction = new Func<int>(LongTimeMethod); 19: IAsyncResult asynResult = longTimeAction.BeginInvoke(null, null); 20:  21: //可以做别的事情 22: while (!asynResult.IsCompleted) 23: { 24: 25: } 26: int result = longTimeAction.EndInvoke(asynResult); 27:  28: } 29: protected override void WndProc(ref Message m) 30: { 31: Debug.WriteLine(m.Msg.ToString()); 32: base.WndProc(ref m); 33: } 34: }

WaitOne

在IAsyncResult里还有一个AsyncWaitHandle属性,这是一个WaitHandle类型的属性,这个对象有一个WaitOne方法,还能接受一个超时时间,它会等待这个超时时间指定的长度:

1: private int LongTimeMethod() 2: { 3: Thread.Sleep(50 * 1000); 4: return 10; 5: } 6: private void btnLongTime_Click(object sender, EventArgs e) 7: { 8: Func<int> longTimeAction = new Func<int>(LongTimeMethod); 9: IAsyncResult asynResult = longTimeAction.BeginInvoke(null, null); 10:  11: //可以继续处理别的事情 12:  13: if (asynResult.AsyncWaitHandle.WaitOne(10000, true)) 14: { 15: int result = longTimeAction.EndInvoke(asynResult); 16: } 17: }

上面的代码的意思就是,异步调用耗时操作后,继续干自己的事儿,然后干完自己的事儿再来等着一个信号,啥信号呢?就是这个耗时操作完成的信号。而且您还别让我等得太久,等久了我就不耐烦了(我可只等待10秒钟啊)。晕死,上面这耗时操作就要执行50秒钟,你就等10秒钟,这不是玩我吗(10秒钟时间过去了,这个WaitOne就不再等待了,线程将继续执行)。

回调

其实不管是上面使用轮询的方式还是使用WaitOne等待一个信号量,还是要等待。等待是个让人很恼火的事情。.Net考虑了这一点,为我们准备了回调的方式:你异步调用后继续干你的事儿,等你执行完后,你告我一声就ok了。

1: private void btnLongTime_Click(object sender, EventArgs e) 2: { 3: Func<int> longTimeAction = new Func<int>(LongTimeMethod); 4: //这里使用了一个lambda表达式,省了不少力啊 5: IAsyncResult asynResult = longTimeAction.BeginInvoke((result) => { 6: int ret = longTimeAction.EndInvoke(result); 7: }, null); 8: }

当异步操作完成后,上面代码中用lambda表达式表示的一个回调方法就会执行,在这里调用EndInvoke获取耗时操作的结果。在这里想想为什么用lambda,如果不用lambda也不用匿名方法(不管你用啥,实际上就是形成一个闭包)你要怎么做?留作您自己思考。

更新UI

上面四种异步调用的方式:一种无声无息,一去不复返。一种轮询、一种等待,外加一个回调。实际上耗时操作的结果都让代码给“吃”了。一般情况下,我们处理完耗时操作总要有所表现吧,比如更新一下UI等等。那我们就来看看如何更新UI。

当你运行这个程序时,当耗时操作结束后,啪嚓一下,程序出异常了:

啊?为什么啊,为什么就不行啊。还不能从不是创建这个控件的线程中访问这个控件。那怎么办?看来我们的异步操作还得改进改进啊。

(未完待续)

时间: 2024-08-30 10:41:28

WinForm二三事(二)的相关文章

站长原创文章二三事(二) 软文需要“区别对待”

中介交易 SEO诊断 淘宝客 云主机 技术大厅 在之前的文章站长原创文章二三事(一):以正确的态度看待原创中讲到了原创文章对于站长的重要性.站长写作原创文章可谓乐此不疲,除了分享经验交流学习外,原创文章另外一个重要的作用就是软文.也可以说站长的原创文章大都是软文性质.相比较为直接的硬性广告,首先,软文的投入成本较低.现在即便最小的广告投入,也要动辄几百上千一个月.个人站长大多处于创业时期难以筹集更多的资金用于广告投入.而软文无需花费一般站长难以承受的高昂广告费用.其次,软文可发布在博客.论坛.门

黑客二三事:熊猫烧香其实不入流

 本文讲的是 :  黑客二三事:熊猫烧香其实不入流  ,  [IT168 评论]随着李俊二度入狱的消息传来,这个沉寂多年的名字以一种颇具娱乐意味的姿态再度出现,同时随着各种泄露事件和棱镜门的不断升级,黑客和网络安全相关的一系列话题再度登上了话题榜. 360带动了杀毒软件免费潮后,没用再出现类似熊猫烧香的大规模破坏性病毒,互联网世界似乎干净了许多,甚至杀毒软件们开始使用检查软件升级.计算开机时间等方式来寻找一些存在感. 不过"圈里人"看来,网络威胁只是换了一种形式存在,公众的眼界外,存在

IOS有关内存管理的二三事

IOS有关内存管理的二三事 一.前引 随着移动设备的内存越来越大,程序员也已经度过了为了那一两M的内存在系统的抽丝剥茧的年代,对于JAVA的开发者,对内存更是伸手即取,并且从不关心什么时候还回去.但是,程序的掌控度对程序员来说是至关重要的,任何语言的内存管理机制的初衷也是在有限的空间里完成最精致的逻辑. 二.Xcode工程设置ARC ARC是xcode5中引入的自动引用计数,其原理与MRC是一样,只是系统帮助我们添加了retain和release.现在在xcode中新建的项目默认都是ARC的环境

DuplexPipe二三事(五)——来自内网的呼唤

穿越防火墙 你是否曾经尝试过去连接一台远程计算机,却因为被防火墙拦截或路由器没有转发而造成无法通信?这是主动式连接的一个弊端:它依赖服务器的状态,而对服务器有生杀大权的只有管理员.如果能让服务器主动尝试连接我们的计算机,那就没问题了!因为防火墙.路由等一般不会过滤向外的连接(反弹式木马就是利用这一原理). 但以往的服务端程序都是采用监听本地端口的方式(比如 Windows 远程桌面程序监听本地 3389 端口),我们需要将它的连接方式改成主动连接外部网络的方式.我想你也一定要将想到(如果你看过<

请问谁有C# WINFORM结合BarTender二次开发的源码或案例。

问题描述 请问谁有C#WINFORM结合BarTender二次开发的源码或案例,给一个下载的谢谢. 解决方案 解决方案二:如题..需要WINFORM引用BarTender的编辑器的源码.谢谢解决方案三:7w底价,其余费用甲方负责,我帮你弄,2个月交付解决方案四:引用2楼conmajia的回复: 7w底价,其余费用甲方负责,我帮你弄,2个月交付 ......不是吧,我只是想把BarTender的编辑器引用支WINFORM中..做一个小编辑软件..我自己都不收钱的.

DuplexPipe二三事(四)——网络连接方式随心换

连接方式 在<DuplexPipe二三事(一)>中提到建立连接有两种方式:监听本地端口,等待其他程序来连接(以下简称"监听方式"):或者主动连接其他程序(以下简称"连接方式").排列组合一下,会得到三种结果:监听-连接.监听-监听.连接-连接.其中只有"监听-连接"方式能正确地建立连接,<DuplexPipe二三事(三)>中介绍的 FPipe 只是在其中添加了一节"监听-连接-监听-连接",其中粗体部分

DuplexPipe二三事(三)——网络中转站:端口映射

端口映射 在<DuplexPipe二三事(二)>中介绍了瑞士军刀 nc 的用法:nc 就像给程序一张飞机票,让原本只能在本地执行的程序也能运行在网络上!但网络和现实交通一样,很多地方需要中转才能到达(比如外网计算机无法直接访问公司内部 Web 服务器). 为实现公网对内部网络的访问,需要对数据进行转发(即端口映射).比如内网"主机A"开启了 Web(port 80)服务,需要在"服务器的网关"上设置:凡来自 Internet 上对 80 端口的请求,全部

DuplexPipe二三事(六)——没有第七

我的设想 在着手编写 DuplexPipe 之前,我规划过我的需求:我想要一个最通用的通信工具,换言之就是能让所有具有输入/输出的程序都可以相互通信.DuplexPipe 本身远没达到这个设想,至少还得具备以下几中模式: -f file # 通过读写文件获得数据 -s # 从 stdio 中获得数据 -e exefile # 从本地程序的输入输出中获得数据 -r url # 这是一个附加功能.如果你玩过几天木马,你可能也渴望将它变成一个强大的后门!通过这个选项可以从URL中获得IP地址和端口,主

DuplexPipe二三事(二)——瑞士军刀再显锋芒:让程序相互聊天

瑞士军刀NC nc(NetCat)是我很喜欢的一个小工具.在我常用的网络小工具中使用频率仅次于 cURL .nc 属于传输层的工具(TCP/IP 四层模型请看这里),它能保证准确无误地发送和接收数据,但并不关心数据的具体含义(这些工作交给应用层的软件).难怪说 nc 是网络的瑞士军刀,因为所有应用层的软件都是通过各自的网络协议来解析来自传输层的数据.所以,只要你了解应用层协议(比如HTTP.POP3等),就可以手工模拟出任何服务端或客户端程序.而且,只要你愿意,以后设计网络程序完全可以只实现一个

DuplexPipe二三事(一)——有趣的起因:算24

写 DuplexPipe 是因为"无聊"!真的,那天很无聊,想起小时候用扑克牌和姐姐比赛算24,就随手写了一个 Shell 脚本重温一下: #!/bin/sh for ((i=0;i<4;i++)) do ((n=$RANDOM%10+1)) echo -n "$n " done echo i=0 while read exp do ((i++)) ((value=$exp)) if [[ $value -ne 24 ]] then echo -n "