艾伟_转载: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-07-31 10:53:31

艾伟_转载:WinForm二三事(二)的相关文章

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

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

艾伟_转载:WinForm二三事(一)

在进入正文之前,想请大家先欣赏下面两段代码: 1: //这是一个控制台程序,请先添加System.Windows.Form.dll的引用 2: using System.Windows.Form; 3:  4: public class ConsoleApplicationShowDialog 5: { 6: static void Main() 7: { 8: Form frm = new Form(); 9: frm.ShowDialog(); 10: } 11: } 1: //这是一个控制

艾伟_转载:基于.NET平台的Windows编程实战(二)—— 需求分析与数据库设计

本系列文章导航 基于.NET平台的Windows编程实战(一)--前言 基于.NET平台的Windows编程实战(二)-- 需求分析与数据库设计 基于.NET平台的Windows编程实战(四)-- 数据库操作类的编写 基于.NET平台的Windows编程实战(五)-- 问卷管理功能的实现 基于.NET平台的Windows编程实战(六)-- 题目管理功能的实现 大家都知道一个系统的成败与否关键在于其所做的需求分析是否到位,数据库的设计是否合理.因为本系列文章的目的是在于提高大家对.NET Wind

艾伟_转载:二十行C#代码打造Ruby Markup Builder

从.NET诞生之日起就有了XML类库,但是从使用上来说非常不方便.例如我们需要构造一个XML文档时,使用DOM API就要这样搞: var xmlDoc = new XmlDocument(); var rootEle = xmlDoc.CreateElement("persons"); xmlDoc.AppendChild(rootEle); var person1 = xmlDoc.CreateElement("person"); person1.InnerTe

艾伟_转载:.NET Discovery 系列之二--string从入门到精通(勘误版下)

本系列文章导航 .NET Discovery 系列之一--string从入门到精通(上) .NET Discovery 系列之二--string从入门到精通(勘误版下) .NET Discovery 系列之三--深入理解.NET垃圾收集机制(上) .NET Discovery 系列之四--深入理解.NET垃圾收集机制(下) .Net Discovery 系列之五--Me JIT(上) .NET Discovery 系列之六--Me JIT(下) .NET Discovery 系列之七--深入理解

艾伟_转载:Cookie是什么?用法是怎样?与SESSION有什么区别?(二)

二session 简介 IE中: 有效的窗品包括 1.Session对象只在建立Session对象的窗口中有效. 2.在建立Session对象的窗口中新开链接的窗口 无效的窗口包括 1.直接启动IE浏览器的窗口 2.不是在建立Session对象的窗口中新开链接的窗口 NetScape中: 只要一个窗口有了某个Session对象,则全部窗口对此Session都有效 Session是什么呢?简单来说就是服务器给客户端的一个编号.当一台WWW服务器运行时,可能有若干个用户浏览正在运正在这台服务器上的网

艾伟_转载:LINQ to SQL、NHibernate比较(二)-- LINQ to SQL实例

    用ADO.NET操作数据库大家一定再熟悉不过了,select.insert.update等等SQL语句大家也都必然滚瓜烂熟.我将自己在学习LINQ to SQL过程中的动手经历记录下来,作为今后学习的参考,也希望对刚刚接触的人有一点帮助.     我在本文涉及到一个很简单的系统,利用DataGridView实现数据库数据的批量增.删.改,不是什么强大的功能.     如果有人感兴趣,可以在看完我的这篇文章之后用ADO.NET实现同样的功能,看看到底会比使用LINQ to SQL多多少时间

艾伟_转载:C# WinForm开发系列 - TextBox

包含金额/日期输入框,带弹出数字面板的计算输入框,安全密码输入等控件(文章及相关代码搜集自网络,仅供参考学习,版权属于原作者! ).   1.CalculatorBox    CalculatorBox.rar 2.带行号+自定义颜色显示的TextBox 3.金额输入框   currency_textbox.zip   CurrencyBox.rar   NumberPicker_src.zip   NumericTextBox_src.zip   NumberedTextbox.rar 4.日

艾伟_转载:WinForm界面开发之酒店管理系统--开篇

星移斗转,时光似箭,不知不觉中,酒店管理系统的开发从开始到现在的结束,已经2个月了,2个月的业余时间,2个月的生活情趣,都寄托在这个软件当中,经历了各种艰苦和困惑,终于得以修成正果---深田之星酒店管理系统的顺利发布. 技术的历程是一个开拓进取.攻克难题的历程,其中有困惑也有兴奋,有苦涩也有甜蜜, 在这个过程中,再一次检阅了我的Database2Sharp代码自动生成的开发工具的,再一次从"深田之星送水管理系统"进行升华,技术从来没有尽头,只有不断完善,以及不断的超越和创新.在这个过程