相关文章连接:
动力之源:代码中的"泵"
- 10.1 "泵"的概念 215
- 10.1.1 现实生活中的"泵" 215
- 10.1.2 代码中的"泵" 216
- 10.1.3 代码中"泵"的作用 218
- 10.2 常见"泵"结构 219
- 10.2.1 桌面GUI框架 220
- 10.2.2 Socket通信 221
- 10.2.3 Web服务器 221
- 10.3 "泵"对框架的意义 228
- 10.3.1 重新回到框架定义 228
- 10.3.2 框架离不开"泵" 228
- 10.4 本章回顾 229
- 10.5 本章思考 229
"循环"语句作为一种常见的代码结构,几乎存在于我们写的任何一段程序代码中,它负责实现"代码重复执行"的功能,像.NET中常见的While循环、Do-While循环、For循环等等。从微观上看这些循环语句,它们仅仅只是简单地控制代码运行流程,但如果从宏观上去看一些稍微复杂的模块、系统,我们会发现,"循环"原来是整个程序的"动力之源",我们称这些能够支撑整个模块乃至整个系统长时间重复运作的结构为"泵"。
10.1 "泵"的概念
10.1.1 现实生活中的"泵"
平时生活中提到"泵"这个词,会让我们联想到"水泵",它主要用于传输类似水这样的液体,下图10-1为一种类型的水泵:
图10-1 水泵
水泵一般包含两个口,一个是液体入口,一个是液体出口,泵能够长时间、不断循环地将液体从一个地方传输到另外一个地方,为液体流动提供动力。现实生活中的泵主要有两个特征:
1)持续性;
泵能够长时间、不间断地干着同一件事情,像汽车发动机一样,启动后会一直重复地做着"转动"运动。
2)动力性。
泵具备传输液体的功能,能为液体流动提供动力支持,尤其是在地势相差很大的场合,泵能够将处于地势低的液体传送到地势高的地方。
图10-2 水泵的作用
上图10-2显示了水泵的一个简单使用场合,它负责将水从水库传送到水池,供稻田和畜牧等使用。
10.1.2 代码中的"泵"
在我们刚学习计算机编程语言时,上课需要写一些实践程序,那时候我们不知道Web网站,也不知道桌面程序,更不知道手机APP,我们只能写一些简单的控制台程序,比如我们测试"冒泡排序"的代码这样写:
1 //Code 10-1 2 3 class Program 4 { 5 static List<int> list = new List<int>() { 89, 14, 59, 32, 29, 78, 2, 77, 89, 73 }; 6 static void Main(string[] args) //NO.1 entry 7 { 8 Console.WriteLine("排序前数组为:"); 9 foreach (var item in list) 10 { 11 Console.Write(string.Format("{0} ", item)); 12 } 13 int temp = 0; 14 for (int i = list.Count; i > 0; i--) 15 { 16 for (int j = 0; j < i - 1; j++) 17 { 18 if (list[j] > list[j + 1]) 19 { 20 temp = list[j]; 21 list[j] = list[j + 1]; 22 list[j + 1] = temp; 23 } 24 } 25 } 26 Console.WriteLine(); 27 Console.WriteLine("冒泡排序后数组为:"); 28 foreach (var item in list) 29 { 30 Console.Write(string.Format("{0} ", item)); 31 } 32 Console.Read(); //NO.2 stop 33 } 34 }
如上代码Code 10-1所示,一个简单的控制台程序,Main()方法为程序入口(NO.1处),它被系统调用。冒泡排序完成后,我们将结果显示在屏幕上,NO.2处设置一个"等待点",当时老师告诉我们之所以要在这里增加一行"Console.Read();"是为了让我们能看到排序后的输出结果,不然黑屏显示后马上就会关闭,这种解释没错,至少从功能上达到的就是这种效果。现如今再看这段代码时,我们应该要有更深的理解。
程序的运行是"流水型"的,有起点也有终点,从微观上看,程序是由许许多多的线程组成,每个线程有运行开始也有运行结束,也就是说,一个线程开始执行后,理论上讲,它必须在某一时刻结束。而如果一个程序只有一个线程,那么该线程结束就意味着整个程序退出(程序退出意味着操作系统会清理回收它活动时占用到的资源),要想线程处于持续工作状态而不马上结束,唯一的办法就是在线程中调用阻塞方法或者线程中包含循环,从实用角度上讲,调用阻塞方法没有什么实际作用,因为阻塞方法大多时候只能处理一件事件,阻塞方法返回后线程结束,而对于循环来说,每次循环都能处理一件事情,多次循环就可以持续处理一类事情,见下图10-3:
图10-3 三种线程
如上图10-3所示,左边为普通线程,线程开始后,马上结束;中间为调用了阻塞方法的线程,阻塞方法耗时较长,但是当它返回后,线程也会马上结束;右边为包含循环结构的线程,该线程能够持续处理一类问题。
注:"阻塞方法"和"非阻塞方法"是一个相对概念,它们之间并没有准确的界线,我们可以认为耗时超过10s的方法属于"阻塞方法",也可以认为耗时超过100ms的方法就应该属于"阻塞方法"。图10-3中的普通线程就是指只调用非阻塞方法的线程,有关"阻塞"与"非阻塞"的概念请参见本书第二章。
大多数系统或者软件程序不可能一开始运行就马上结束,通常情况下,它们都会持续、不间断地循环处理一类问题,这个时候程序代码中肯定会包含循环结构,我们称能够持续处理一类问题的循环结构为"泵",和生活中的"水泵"一样,代码中的"泵"结构能够为程序提供持续运行的动力。.NET代码中的常见循环结构有:
1)While循环;
1 //Code 10-2 2 3 While(条件) 4 { 5 //do some work 6 }
2)Do-While循环;
1 //Code 10-3 2 3 do 4 { 5 //do some work 6 } 7 while(条件);
3)For循环;
1 //Code 10-4 2 3 for(int i=0;i<最大值;++i) 4 { 5 //do some work 6 }
4)Foreach循环。
1 //Code 10-5 2 3 foreach(…) 4 { 5 //do some work 6 }
上面代码Code 10-2、Code 10-3、Code 10-4以及Code 10-5中的四种循环结构中,后两种主要用于遍历容器中的元素,一般很少当作泵来使用,而While循环和Do-While循环则通常当作泵来使用。
10.1.3 代码中"泵"的作用
经过前面两小节的讨论,我们应该很容易知道代码中"泵"的重要性,和生活中的"水泵"一样,它主要有以下两个作用:
1)持续性;
代码中的泵能够让线程持续运行而不是很快结束,这是程序(线程)持续工作的前提。
2)动力性。
既然水泵能够产生动力,将水等液体从一个地方传送到另外一个地方,代码中泵照样具备"提供动力"的特性,它能够将"数据"从一个地方搬到另外一个地方,供其他人(模块)使用,见下图10-4:
图10-4 代码中"泵"的动力效果
上图10-4显示了代码中泵的动力效果,它能将某个地方的数据源源不断地传送给使用者。
在一个典型的"生产者-消费者"模式系统中,"泵"的作用尤其重要,生产者不停地将数据存入数据容器,消费者需要使用泵源源不断地将数据从容器中取出,进而传送给数据处理者,泵是消费者能够持续工作的核心部件,见下图10-5:
图10-5 "生产者-消费者"模式中的泵
如上图10-5所示,消费者中包含一个泵结构,它是消费者持续稳定工作的支柱。
注:"泵"结构在"生产者-消费者"模式中起到了非常关键的作用,"很不幸的是",任何一个软件系统总会有若干模块属于"生产者-消费者"模式,比如Windows操作系统中,用户鼠标键盘等外设的输入可以看成是"生产者",而操作系统内部肯定会有一个"泵"结构不断地获取用户外设输入,然后传递给其他处理者。更详细的有关常见的"泵"结构请参见10.2节。
10.2 常见"泵"结构
本节将介绍几种常见的"泵"结构,我们可以从以下这些成熟的应用实例中获取灵感,进而将"泵"运用在自己的程序代码中。其中"桌面GUI框架"中使用到的泵可以参见第八章,"Socket通信"中使用到的泵可以参见本书第九章。
10.2.1 桌面GUI框架
第八章中在讲"桌面GUI框架解密"中已经提到过,桌面程序的UI线程中包含一个消息循环(确切的说,应该是While循环),该循环不断地从消息队列中获取Windows消息,最终调用对应的窗口过程,将Windows消息传递给窗口过程进行处理。如果按照本章前面的介绍,消息循环就应该是"泵",消息队列就应该是"数据容器",Windows消息就应该是"数据",而窗口过程就应该是"处理者",那么整个结构应该是这样的:
图10-6 GUI框架中的"泵"
图10-5跟图10-6类似,可以说后者是前者的一个具体实例。
到目前为止,我们只是知道GUI框架中获取Windows消息的结构是一个"泵"结构,它维持着整个桌面GUI界面的运转,殊不知,图10-6中右侧省略的关于"生产者"的部分也是一种"泵"结构,这部分由操作系统负责,数据的最终源头是计算机鼠标键盘等外设,见下图10-7:
图10-7 完整的GUI框架结构
如上图10-7,我们可以看到,作为Windows消息的生产者,它依旧包含有"泵"结构,源源不断地将用户外设输入信息转换成Window消息,进而存入消息队列。正因为有这些"泵"相互配合着工作,才能给整个系统提供持续运转的动力。
注:图10-7右侧有关Windows消息转换、外设信息采集等结构均属于"示意结构",并不代表真实情况。
10.2.2 Socket通信
第九章中讲到Socket网络编程时就提到过"泵"的概念,比如"侦听泵"、"数据接收泵"等,如果按照本章前面介绍的"生产者-消费者"模式,数据接收泵如下图10-8:
图10-8 Socket网络编程中的"泵"
图10-5与图10-8类似,可以说后者是前者的一个具体实例。
图10-8中,如果处理者在处理数据时,耗时太长(即所谓的"阻塞方法"),那么一次循环不能及时完成,系统缓冲区中的数据就会大量累积,得不到及时的处理,这种泵虽然确保了数据的顺序处理(即先接收到的数据先处理完毕,后接收到的数据后处理完毕,前一次处理结束之前,后一次处理不能开始),但是影响了处理效率,如何解决这个问题,请参见下一小节。
注:如何提高"泵"处理数据的效率这个问题,在第九章结尾有所提示。
10.2.3 Web服务器
本节将详细介绍Web服务器的工作原理,并为大家演示"泵"结构是如何在Web服务器中担当着重要角色。
在第九章曾提到过,无论是Web服务器还是装在普通用户电脑中的浏览器,均要遵守应用层协议:HTTP协议,而我们一提到Http协议时,就会想到它至少有以下两个特点:
1)无连接;
我们常说Http协议是一种无连接协议,这可能给人一种误导,这里的"无连接"并不是指遵循HTTP协议的Web服务器与浏览器之间通信不需要建立连接就可以进行,因为Http协议在传输层是使用TCP进行传输的,而TCP协议是一种面向连接的协议,也就是说,Web服务器与浏览器通信之前必须建立连接,那么我们常说的"Http协议是一种无连接的协议"到底是个什么意思呢?
如果我们了解Web服务器与浏览器之间的通信过程,我们就能很清楚为什么称Http协议是无连接的,
图10-9 Web服务器与浏览器通信过程
如上图10-9所示,浏览器每次发送http请求时,都必须与Web服务器建立连接,Web服务器端请求处理结束后,连接立刻关闭,浏览器下一次发送http请求时,必须再一次重新与服务器建立连接。由此我们应该了解,我们所说的Http协议是面向无连接的,是指Web服务器一次连接只处理一个请求,请求处理完毕后,连接关闭,浏览器在前一次请求结束到下一次请求开始之前这段时间,它是处于"断开"状态的,因此我们称Http协议是"无连接"协议。
2)无状态。
Web服务器除了跟浏览器之间不会保持持久性的连接之外,它也不会保存浏览器的状态,也就是说,同一浏览器先后两次请求同一个Web服务器,后者不会保留第一次请求处理的结果到第二次请求阶段,如果第二次请求需要使用第一次请求处理的结果,那么浏览器必须自己将第一次的处理结果回传到服务器端。
如果结合本章前面讲到的"生产者-消费者"模式,我们可以将浏览器端的请求看作是"生产者",而将Web服务器端的请求处理看作成"消费者",消费者不断地处理来自生产者的"请求",见下图10-10:
图10-10 Web服务器中的"泵"结构
如上图10-10,Web服务器中的数据接收泵源源不断地接收来自浏览器的请求数据,然后传递给其他人(模块)进行处理,处理完毕后,将结果(Reponse Data)发回给浏览器。
注:Http协议数据是按照TCP协议进行传输的,所以我们完全可以通过Socket编程来实现一个简单的Web服务器。
我们注意到图10-10中,如果Web服务器在处理某一次请求时耗时过长,阻塞了泵循环,那么系统缓冲区中就会积累大量请求不能及时被处理,这显然影响了服务器的响应速度,如果我们将处理数据的环节放在泵循环以外,也就是说,数据接收泵只负责接收数据,而不负责处理数据,见下图10-11:
图10-11 Web服务器中改进后的"泵"结构
如上图10-11所示,改进后的数据接收泵只负责数据的接收,而不负责数据的处理和回复,这样一来,任何阻塞处理数据都不会影响后面的请求处理,因为所有的处理都是"并行"发生的。
使用第九章介绍的Socket编程知识,我们可以模拟一个Web服务器程序,并对比两种"泵"结构对浏览器请求的影响:
1)主线程;
1 //Code 10-6 2 3 class Program 4 { 5 static void Main(string[] args) 6 { 7 IPAddress localIP = IPAddress.Loopback; 8 IPEndPoint endPoint = new IPEndPoint(localIP, 8010); 9 Socket server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); 10 server.Bind(endPoint); //NO.1 11 server.Listen(10); 12 Console.WriteLine("开始监听,端口号:{0}", endPoint.Port); 13 server.BeginAccept(new AsyncCallback(OnAccept), server); //NO.2 asynchronous accept 14 Console.Read(); //NO.3 15 } 16 }
如上代码Code 10-6所示,创建Socket套接字对象,绑定端口8010(NO.1处),并开始一个异步侦听过程(NO.2处),NO.3处阻塞当前主线程,防止屏幕退出关闭。
2)侦听泵。
1 //Code 10-7 2 3 static void OnAccept(IAsyncResult ar) 4 { 5 Socket server = ar.AsyncState as Socket; 6 Socket proxy_socket = server.EndAccept(ar); //NO.1 get proxy socket 7 Console.WriteLine(proxy_socket.RemoteEndPoint); 8 byte[] bytes_to_recv = new byte[4096]; 9 int length_to_recv = proxy_socket.Receive(bytes_to_recv); //NO.2 receive request data 10 string received_string = Encoding.UTF8.GetString(bytes_to_recv, 0, length_to_recv); 11 Console.WriteLine(received_string); //NO.3 12 string statusLine = ""; 13 string responseContent = ""; 14 string responseHeader = ""; 15 byte[] statusLine_to_bytes; 16 byte[] responseContent_to_bytes; 17 byte[] responseHeader_to_bytes; 18 string[] items = received_string.Split(new string[] { " " }, StringSplitOptions.None); //items[0] like "GET / HTTP/1.1" NO.4 resolve the request string 19 if (items[0].Contains("Sleep")) //NO.5 20 { 21 Thread.Sleep(1000 * 10); 22 statusLine = "HTTP/1.1 200 OK "; 23 statusLine_to_bytes = Encoding.UTF8.GetBytes(statusLine); 24 responseContent = "<html><head><title>Sleeping Web Page</title></head><body><h2>Sleeping 10 seconds,Hello Microsoft .NET<h2></body></html>"; 25 responseContent_to_bytes = Encoding.UTF8.GetBytes(responseContent); 26 responseHeader = string.Format("Content-Type:text/html;charset=UTF-8 Content-Length:{0} ", responseContent_to_bytes.Length); 27 responseHeader_to_bytes = Encoding.UTF8.GetBytes(responseHeader); 28 } 29 else //NO.6 30 { 31 statusLine = "HTTP/1.1 200 OK "; 32 statusLine_to_bytes = Encoding.UTF8.GetBytes(statusLine); 33 responseContent = "<html><head><title>Normal Web Page</title></head><body><h2>Hello Microsoft .NET<h2></body></html>"; 34 responseContent_to_bytes = Encoding.UTF8.GetBytes(responseContent); 35 responseHeader = string.Format("Content-Type:text/html;charset=UTF-8 Content-Length:{0} ", responseContent_to_bytes.Length); 36 responseHeader_to_bytes = Encoding.UTF8.GetBytes(responseHeader); 37 } 38 proxy_socket.Send(statusLine_to_bytes); //NO.7 39 proxy_socket.Send(responseHeader_to_bytes); //NO.8 40 proxy_socket.Send(new byte[] { (byte)' ', (byte)' ' }); //NO.9 41 proxy_socket.Send(responseContent_to_bytes); //NO.10 42 proxy_socket.Close(); //NO.11 43 server.BeginAccept(new AsyncCallback(OnAccept), server); //start the next accept NO.12 44 }
如上代码Code 10-7所示,当有浏览器发送Http请求时,NO.1处获得请求连接的代理Socket,NO.2处接收浏览器发送的请求数据(Request Data),并将其显示到屏幕(NO.3处),NO.4处简单地解析了Http请求数据,NO.5处判断请求URL中是否包含"Sleep字符串"(即URL为"http://localhost:8010/Sleep"),如果是,则线程等待10秒(模拟耗时操作),最终,按照Http协议规定的数据格式,将应答数据(Response Data)发送给浏览器(NO.7、NO.8、NO.9以及NO.10处),数据发送完成后,立即关闭Socket,意味着服务器与浏览器的连接关闭(NO.11处),这一切完成后,开始下一次异步侦听过程(NO.12处)。
注意Http请求数据的格式类似如下:
图10-12 Http请求数据格式
上图10-12表示浏览器向Web服务器发送Http请求的数据格式,图中方框中的第二格表示请求的路径(图中完整的URL应该为:http://localhost:8010/),Web服务器按照Http协议格式解析浏览器发送过来的数据,然后进行处理,将结果按照Http协议规定的格式发回浏览器,Http应答数据格式见下图10-13:
图10-13 Http应答数据格式
上图10-13表示Web服务器向浏览器返回数据的格式(方框内表示返回的Html文档内容),浏览器按照Http协议格式解析服务器发送过来的数据,然后进行网页显示。
串行处理请求的"泵":
代码Code10-6和Code10-7最终的效果是:如果浏览器前一次请求URL为"http://localhost:
8010/Sleep",服务器端会调用"Thread.Sleep(1000*10);"这行代码,这意味着会阻塞整个"泵"的运转,这时候如果使用URL为http://localhost:8010/请求服务器,Web服务器不能做出应答,因为前一次请求还未处理完成,也就是说,Http请求是"串行"处理的,见下图10-14:
图10-14 串行处理请求的"泵"
如上图10-14所示,第一次请求处理完毕之前,第二次、第三次请求均不可能被处理,也就是说,其余的浏览器请求均处于"等待"状态,见下图10-15:
图10-15 串行处理请求的"泵"效果图
图10-15显示,先访问http://localhost:8010/Sleep地址,然后马上请求http://localhost:8010/地址,在"Sleeping Web Page"页面返回之前,"Normal Web Page"页面一直处于等待状态,直到"Sleeping Web Page"返回。
并行处理请求的"泵":
将代码Code 10-7中NO.12行代码移到NO.1下一行,也就是说,侦听到一个浏览器请求后,马上开始另外一个异步侦听过程,这样一来,任何数据处理均不会因为耗时长而影响到后面请求的处理,因为它们都是"并行"处理的:
图10-16 并行处理请求的"泵"
如上图10-16所示,第一次请求处理完毕之前,就可以开始第二次甚至第三次请求的处理,不管请求处理是否耗时,其余浏览器请求均能及时返回,见下图10-17:
图10-17 并行处理请求的"泵"效果图
图10-17显示,先访问http://localhost:8010/Sleep地址,然后马上请求http://localhost:8010/地址,在"Sleeping Web Page"页面返回之前,"Normal Web Page"页面就能立刻返回。
10.3 "泵"对框架的意义
10.3.1 重新回到框架定义
本书第二章中介绍"框架与库"的区别时曾讲到,框架是一个不完整的应用程序,理论上讲,我们不做任何处理,框架就可以正常运行起来,只是这运行起来的框架不具备任何功能或者只具备简单的通用功能。我们在使用框架开发程序时,实际上就是结合实际具体的功能需求,在框架的基础上进行一系列的扩展,最终开发的软件系统能够帮助我们解决某一具体工作。由于主流框架均是由出色的技术团队开发完成,他们无论在技术造诣还是业务了解程度上几乎都比我们要高,因此,借助框架来开发应用程序不仅能够缩短开发周期,还能够保证最后应用程序的稳定性。
既然我们最终的应用程序是在框架的基础之上扩展出来的,这说明应用程序的主要运行逻辑、主要的流程控制均是由框架决定的,框架控制应用程序的启动、决定主要的流程转向,负责调用框架使用者编写的"扩展代码",总之,框架能够保证最终应用程序的持续正常工作。
注:上面提到的"扩展代码"可以理解为开发者在使用框架开发程序时编写的所有代码。
10.3.2 框架离不开"泵"
既然框架能够保证最终应用程序的持续正常工作,按照本章前面的结论,那说明框架内部必然有一种结构能够重复性处理问题,这种结构就是"泵",泵的"持续性"和"动力性"特性完全满足框架的需求。如果需要将这种抽象关系图形化显示出来,见下图10-18:
图10-18 "泵"在框架中的体现
图10-18中方框表示框架,循环代表"泵"结构,可以看出,"泵"是框架提供动力的源头,虽然用图10-18来轻率地描述框架结构显然是不准确的,但是它足以能够说明"泵"在框架中的重要位置。
注:框架控制程序的运行流程称为"控制反转(IoC)",本书前面章节多次提到过。
10.4 本章回顾
本章主要介绍了代码中"泵"的具体表现形式,以及它对软件系统的重要性。本章可以说是对第八章和第九章的一个补充,第八章中讲"桌面GUI框架"时就已经涉及到了"泵"的概念,第九章中讲"Socket网络编程"时也已经提出了"泵"的定义,本章结合前两章的内容,系统性地对"泵"在编程中的应用做了统一阐述。
10.5 本章思考
1..NET中循环结构有哪些?分别主要用于什么场合?
A:.NET中的循环结构有for循环、foreach循环、while循环以及do-while循环。for循环主要用于重复执行指定次数的操作,foreach循环主要用于遍历容器元素,while循环和do-while循环主要用于重复执行某项操作直到某一条件满足或不满足为止。代码中的"泵"结构主要由while循环来实现。
2.简述代码中"泵"结构的作用。
A:代码中的"泵"结构具备"持续性"和"动力性"两大特点,它能够维持程序的持续运行状态,为程序运转提供动力支持。
3.串行处理数据的泵与并行处理数据的泵之间有什么区别?
A:串行处理数据的泵是按顺序处理数据的,本次数据处理结束之前,下一次处理不能开始;并行处理数据的泵不是按顺序处理数据,所有的数据处理均是同时进行的,没有先后顺序,不能确保先开始处理的数据一定先结束处理,也不能保证后开始处理的数据一定后结束处理。通过异步编程很容易实现两种泵结构。
(本章完)