.net开发笔记(十三) Winform常用开发模式第一篇

     上一篇博客最后我提到“异步编程模型”(APM),之后本来打算整理一下这方面的材料然后总结一下写篇文章与诸位分享,后来在整理的过程中不断的延伸不断地扩展,发现完全偏离了“异步编程”这个概念,前前后后所有的加起来完全可以写一篇关于框架原理的东西,而“异步编程”只是其中的一小部分,后来我一狠心,打算把所有的都包含进来写出来,希望给诸位带来帮助。

     文章开始之前,先了解几个概念:

一、回调方法。

这个概念想必都很清楚,被系统调用的方法就叫做“回调方法”。是的,描述的没错,通常我们注册一个事件,事件处理程序就属于“回调方法”。可是不知道诸位有没有想过,我们在编程过程中,哪些不属于“回调方法”呢?有人肯定会说,我们主动调用的方法就不属于回调方法。先不管对不对,我们先来考虑一些问题:系统调用方法A,那么方法A就是回调方法,如果我在A中又调用了方法B,那么B算作回调方法吗?再者,什么是所谓的“系统”?指操作系统吗?亦或是我们编程中使用到的“框架”?最后,我们写的程序,系统从哪开始调用?又是在哪结束?接下来我一一做解答:

1)广义上讲,我们写的每一行代码都属于“回调代码”(由代码组成的方法就叫回调方法,意思差不多),为什么这么讲?因为我们都知道,任何一个程序开始运行,都是由操作系统调用某一个入口方法,那么显然,这个入口方法就是理所当然的“回调方法”,进入“入口方法”中去之后,就会执行许许多多的其他代码,也就是说,不管你来回调用了多少次、嵌套调用了多少次,我们编写的所有代码都间接被操作系统调用。那么像上面有人可能提到的“主动调用的方法不属于回调方法”,其实在程序中,压根儿就没有你能主动调用的方法,如果你写如下的代码:

 1 private void btn1_Click(object sender,EventArgs e)
 2 {
 3 Thread th = new Thread((ThreadStart)delegate()
 4 {
 5      func();
 6 })
 7 th.Start();
 8 }
 9 
10 void func()
11 {
12     // do something
13 }
View Code

你可能会说,func()是我主动调用的,所以它不是回调方法,但是你要明白,不管你代码怎么写,它最终执行的主动权不是在你手中,既然主动权不在你手中,那么它就应该属于回调。

2)这里的“系统”其实是相对而言的,我前面都把它当做操作系统,其实不然,我们口口声声说“系统调用的方法属于回调方法”,这里的“系统”绝大多数是指编程中使用到的“框架”,你比如如下代码:

1 btn1.Click+=new EventHandler(btn1_Click);
2 private void btn1_Click(object sender,EventArgs e)
3 {
4     //do something
5 }
View Code

这里的“回调方法”btn1_Click是由“微软Winform开发框架”调用的,因此“系统”就是指“Winform开发框架”。1)中讲到的“任何一个程序开始运行,都是由操作系统调用某一个入口方法”,那么这里的“系统”就是指操作系统。任何都是相对而言的,任何“框架”对于操作系统而言,都可以算得上是“回调”,而我们写的所有代码相对于“框架”而言,也都能算得上“回调”。

3)我们写的程序,就用Windows Form应用程序为例子吧,对于操作系统而言,第一个回调方法应该是Main方法,由Main进入Application.Run(…),至于何时结束,当然是Main方法最后的一个“反花括号”。至于中间使用了哪些“框架”,我们自己又写了哪些代码,对于操作系统而言,全部都是一样的,那都属于“回调代码”。总之,广义上讲,没有不是“回调”的代码(方法)。

图1

二、泵。

这是个非常重要的概念,也是本篇文章之后的核心。 生活中提到泵,我们至少可以想到两点:

1)持续运作。也就是说,泵能长时间循环工作。

2)传输作用。泵能够将水等液体从一个地方运输到另外一个地方,供其他人使用。

图2

以上是生活中见到的泵,那么在编程中,泵是指什么呢?类比一下,其实很容易想到,程序中的泵具备以下两个特点:

1)  循环执行。类似一个while循环,能够长时间循环工作。

2)  数据传输。它能够将数据(不再是水等液体)从一个地方搬迁到另一个地方,供其他人使用。

程序中的泵,最简单的利用while就能实现,如下代码:

 1 Queue<Data> container = new Queue<Data>();   //数据容器
 2 // do something before
 3 while(GetData(data)) //从container取数据
 4 {
 5     //实际中,可以先将data稍作处理
 6     SendDataToOtherPlace(data); //将数据传送到其他地方,供其他人使用
 7 }
 8 //do something … 
 9 //end
10 
11 
12 void SendDataToOtherPlace(data)
13 {
14      DealWithData(data); //使用数据
15 }
View Code

    如你所见,以上代码很好的解释了编程中的“泵”含义。那么,程序中为啥需要使用泵?原因可以从泵的作用中找,我们很清楚,平时调试一些代码时,可能很多人使用Console程序,如果我们在代码中不设置一个阻塞断点的话(如Console.Read()),程序执行完毕后,黑屏就会消失,我们看不到任何结果,如下代码:

1 void main()
2 {
3      int a =0,b=1;
4      int c = a;
5      a=b;
6      b=c;
7     Console.WriteLine(“a is ”+a+”,b is ” + b);
8 }
View Code

    其根本原因,是我们写的程序是“线段”状,所谓线段,即它是有限长度直线,线程从main开始,笔直的结束了。可是我们用的大多数软件从来不会一开始运行,马上就结束(除非某些特定功能软件)了,他们绝大多数都是长时间持续运行,好了,听到“长时间持续运行”,我们就想到了“泵”有这种功能,是的,“泵”不仅仅有这种功能,它还能将数据从一个地方搬迁到另外一个地方,供其他人使用。到此,程序中使用“泵”是必然。

    讲到这里,相信有很多人开始意识到自己在编程中已经见过或者使用过“泵”,比如一般界面编程中的“windows消息循环”,如果有人说没听说,它见过你你却没见过它,那说明你对Windows桌面开发还不是很了解,建议看看本系列博客之透过现象看本质。反观Windows操作系统,它其实就是一个非常大的“泵”,长时间持续工作,从“串口”、“键盘鼠标”、“麦克风”、“摄像头”、“网络端口”等等缓冲区中获取数据,传递给各种各样的程序使用。看一张程序中“泵”结构图:

图3

实际编程中,用到“泵”的地方很多,只要某一个环节(跟模块的意思差不多,只是个人觉得环节更具体,模块指的范围太大,下同)需要长时间持续工作,同时不断存在一系列数据需要被处理,那么就可以使用泵。总结一下,程序中需要使用“泵”的地方有两个明显特点:

1)  该环节需要持续运作,也就是需要循环运行,不会马上结束;

2)  有一些数据需要被处理,这些数据一般存放在某个容器中,需要不断地取出来传给别人使用。

    前面提到的“Windows消息循环”就是一个泵,它符合以上两个特点,第一,UI线程不可能马上结束,需要长时间持续运作;第二,源源不断的有Windows消息(一种数据)需要被处理(数据存放在线程的消息队列中)。为了更好理解,附图一张:

图4

既然“泵”是一种循环,并且每一次循环执行都是需要时间损耗的,这样就出现了一个问题,如果某一次循环耗时太长,单次循环不能立刻返回,那么需要处理的数据就会大量累积,不能及时取出处理,造成堵塞。这个问题其实我们经常遇见过(或许又是它天天见到你,你却没看见它),我们编程时,有时候会遇见界面卡、不流畅、反应慢等现象,大部分原因就是因为,消息处理泵(消息循环)某一次循环耗时太长,循环不能迅速返回,windows消息大量累积,得不到及时处理,造成界面反应迟钝。

三、线程和方法的关系

这个问题其实本系列第一篇博客中讲到过,线程和方法没有一对一的关系,一个线程可以调用许多方法,一个方法也可以运行在多个线程中。前面一句很好理解,后面一句其实也好理解,看如下代码:

1 void func()
2 {
3     //do something
4 }
5 Thread th1 = new Thread(new ThreadStart(func));
6 Thread th2 = new Thread(new ThreadStart(func));
7 
8 Th1.Start(); th2.Start();   //th1 和 th2 执行了同一个方法func
View Code

如果func中没有访问外部变量,基本上不会出问题,但是如果func中访问了外部对象,而该对象不是线程安全的,那么你就得在func中做一些“安全措施”了,这点很容易被忽略,如下:

1 List<int> list = new List<int>();
2 void func()
3 {
4     list.Add(DateTime.Now.Hours);
5 }
View Code

我们在设计func方法的时候,应该考虑该方法将来可能在哪些地方被调用,如果只在一个线程中调用(比如UI线程),那么没有任何问题,但是如果func有可能运行在多个线程中,那么你就需要做一些“安全措施”了,比如加锁等。

    总之你在设计一个方法的时候,务必要考虑这个方法将来可能在哪些地方调用,如果是控件类的成员方法,你更要考虑,因为控件类成员方法一般都会方法UI,如果这个成员方法将来被其它线程(非UI线程)调用,那么就会出现异常。

以上三个概念有些本篇文章有用,有些阅读下一篇我分享一个UDP通信demo的时候有用。

正文:

理解以上三个概念,我认为对熟悉接下来要说的有很大帮助。下面,我介绍一个winform中常用到的开发模式,该模式就是通过“泵”来实现的,不敢说诸位平时用到的所有的框架都是基于这种模式,但我敢说我用到过的框架都是以此为基础的(下一篇博客,我会分享一个UDP通信demo,用具体的实例来说明该开发模式)。

据我开发经验,总结出来4种需要使用到“泵”的场合:

(1)当然是之前提到过的有关“Windows消息循环”这一块,它几乎是所有Windows桌面应用程序开发的精髓。

(2)Socket通信这一块,包括UDP和TCP两部分,我之后会做一个UDP的Demo。

(3)串口通信这一块。

(4)麦克风、摄像头数据采集这一块。

大概常用的有这四种,其实意思都差不多,就是之前我们讲到的:都涉及到持续运行,都需要不断的取数据、分配(传递)数据、别人再处理(使用)数据。我具体说一说(1)和(2),弄清楚前两个,后面两个也就清楚明了了。

(1)要了解“Windows消息循环”,我们先得了解一个流程:鼠标点击按钮,鼠标驱动采集物理信息,转换成数字信息,存在一个缓冲区A,我们称该数字信息为“原始数据”(你可以理解为包含鼠标XY坐标、左右键状态等等),之所以称之为“原始数据”,是因为该数据跟咱们的程序没有任何关联,它只是简单地包含了鼠标当前状态信息。接下来就有一个“数据采集泵”循环将这些原始数据采集过来,放到另外一个缓冲区B,对应有一个“数据分析泵”,循环将缓冲区B中的原始数据取出,分析该“原始数据”,参照Windows系统“内部数据库”(一种存放窗体、线程等资源的组织),将原始数据转换成标准的“Windows消息”(一种数据结构,包含窗体Handle,类型、参数等),接着再将转换之后生成的“Windows消息”存放到缓冲区C(就是我们经常听到的消息队列),此时,又有一个“数据处理泵”(就是我们常说的消息循环)循环取出缓冲区C中的“Windows消息”,分配该消息给对应的窗口过程(WndProc),供其使用(处理),窗口过程就会激发Click事件,接着,你的事件处理程序(如btn1_Click)就会被调用,至此,整个过程结束。上图一张,更清楚:

图5

    如我们所见,整个过程使用了3个泵,他们互相配合使用,“数据采集泵”负责将“原始数据”从缓冲区A传递到缓冲区B,“数据分析泵”负责取出缓冲区B中的原始数据,然后进行分析,转换成Windows消息(一种程序能够识别的数据结构),进而传递到缓冲区C,也就是我们常说到的“消息队列”,然后“数据处理泵”,我们常说的“消息循环”,循环从缓冲区C中取出消息,分配给对应的窗口过程,供其使用。

有人可能会说,干嘛要分三个“泵”,一个“泵”不就能搞定吗,在“数据采集泵”中分析数据、转换数据、处理数据?不能的原因至少有两个:

  1. 各所其职,符合软件开发的原则
  2. 如果什么东西都放在一个“泵”中做,必然会影响原有的效率,比如将“数据分析”放在“数据采集泵”中,势必会影响采集的效率,其他类似。

以上是“泵”在Windows消息处理中的应用。接下来说一下Socket编程中的应用,我以UDP通信为例,TCP类似。

(2)我们先理清UDP通信流程:远程主机给本地主机发送一个UDP数据包,需要注意的是,在到达本地主机之前(传输过程中),数据包应该是一种物理信息,经过网卡驱动转换后,物理信息变成数字信息,存放在缓冲区A中(一串字节流,称之为原始数据),此时,需要一个“数据接收泵”循环取出缓冲区A中的原始数据(UDP中该数据应该是一个完整的数据包),将其存放到缓冲区B中,对应有一个“数据分析泵”循环取出缓冲区B中的原始数据,根据事先规定好的“协议”(一种通信规则,通信各方必须同时遵守),将该原始数据解析成程序可识别数据(数据头,程序中可识别数据,远程IP端口等),紧接着将解析之后的数据存放到缓冲区C,对应又有一个“数据处理泵”循环从C中取出数据,分配数据,通知他人处理。上图一张:

图6

现在已经很清楚,这个模式跟“windows消息循环”是一个意思,接收数据->分析数据->处理数据,每个环节都有一个“泵”与之关联,当然还有一个缓冲区。其实再拓展一下,我们会发现它们都有输入,都有分析,都有响应

  1. 前者鼠标输入,后者远程输入;
  2. 前者有分析泵,后者照样有;
  3. 前者激发一些事件,比如Winform中的Click事件,你可以在事件处理程序中访问数据库、操作IO、更新界面,后者你注册相关事件之后,照样可以做这些事情。

再不说了,说多了都是泪,发现原来它们都是一样一样的。TCP跟UDP差不多,只是服务端需要有“socket侦听泵”用来监听socket连入,而且每个连入的socket都对应有自己的“数据接收泵”跟“数据分析泵”,原因很简单,因为TCP按照“流”来传输数据的,数据包之间没有界限,某一次接收到的“原始数据”可能不是一个完整的包,因此,每个客户端socket必须有自己的“数据接收泵”和“数据分析泵”以及对应的缓冲区,并且“数据分析泵”中还要具备检测完整包的功能。TCP版本Demo以后我再做一个,稍微比UDP复杂一点。

    以上是所有的介绍,理论性的东西非常多,下一篇文章我打算分享一个UDP通信demo,采用本篇所讲内容,简单的实现了类似飞鸽传书的功能。

    顺便带个题外话,这一系列文章可能跟实际具体开发关联性不是很大,特别像之前说到的“运行时和设计时”、“winform框架原理”等等这些,基本上跟平时工作沾不上边,我也没有刻意去写平时工作中遇到的问题,写出来的东西大都是概念性、原理性偏多一些。各位在看的时候没必要跟实际工作内容做比较,全当做是一种业余研究就OK了, O(∩_∩)O~。

posted @ 2013-07-02 19:53  周见智  阅读(6537)  评论(4编辑  收藏  举报