动力之源:代码中的“泵“

Posted “逛丢一只鞋”

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动力之源:代码中的“泵“相关的知识,希望对你有一定的参考价值。

"泵"的概念

现实生活中的"泵"

平时生活中提到"泵"这个词,会让我们联想到"水泵",它主要用于传输类似水这样的液体,下图10-1为一种类型的水泵:

在这里插入图片描述

图10-1 水泵

水泵一般包含两个口,一个是液体入口,一个是液体出口,泵能够长时间、不断循环地将液体从一个地方传输到另外一个地方,为液体流动提供动力。现实生活中的泵主要有两个特征:

1)持续性;

泵能够长时间、不间断地干着同一件事情,像汽车发动机一样,启动后会一直重复地做着"转动"运动。

2)动力性。

泵具备传输液体的功能,能为液体流动提供动力支持,尤其是在地势相差很大的场合,泵能够将处于地势低的液体传送到地势高的地方。

图10-2 水泵的作用

上图10-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循环则通常当作泵来使用。

代码中"泵"的作用

经过前面两小节的讨论,我们应该很容易知道代码中"泵"的重要性,和生活中的"水泵"一样,它主要有以下两个作用:

1)持续性;

代码中的泵能够让线程持续运行而不是很快结束,这是程序(线程)持续工作的前提。

2)动力性。

既然水泵能够产生动力,将水等液体从一个地方传送到另外一个地方,代码中泵照样具备"提供动力"的特性,它能够将"数据"从一个地方搬到另外一个地方,供其他人(模块)使用,见下图10-4:

在这里插入图片描述

图10-4 代码中"泵"的动力效果

上图10-4显示了代码中泵的动力效果,它能将某个地方的数据源源不断地传送给使用者。

在一个典型的"生产者-消费者"模式系统中,"泵"的作用尤其重要,生产者不停地将数据存入数据容器,消费者需要使用泵源源不断地将数据从容器中取出,进而传送给数据处理者,泵是消费者能够持续工作的核心部件,见下图10-5:
在这里插入图片描述

图10-5 "生产者-消费者"模式中的泵

如上图10-5所示,消费者中包含一个泵结构,它是消费者持续稳定工作的支柱。

注:"泵"结构在"生产者-消费者"模式中起到了非常关键的作用,
"很不幸的是",任何一个软件系统总会有若干模块属于"生产者-消费者"模式,
比如Windows操作系统中,用户鼠标键盘等外设的输入可以看成是"生产者",
而操作系统内部肯定会有一个"泵"结构不断地获取用户外设输入,
然后传递给其他处理者。更详细的有关常见的"泵"结构请参见10.2节。

常见"泵"结构

本节将介绍几种常见的"泵"结构,我们可以从以下这些成熟的应用实例中获取灵感,进而将"泵"运用在自己的程序代码中。其中"桌面GUI框架"中使用到的泵可以参见第八章,"Socket通信"中使用到的泵可以参见本书第九章。

桌面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消息转换、外设信息采集等结构均属于"示意结构",并不代表真实情况。

Socket通信

第九章中讲到Socket网络编程时就提到过"泵"的概念,比如"侦听泵"、"数据接收泵"等,如果按照本章前面介绍的"生产者-消费者"模式,数据接收泵如下图10-8:

在这里插入图片描述

图10-8 Socket网络编程中的"泵"

图10-5与图10-8类似,可以说后者是前者的一个具体实例。

图10-8中,如果处理者在处理数据时,耗时太长(即所谓的"阻塞方法"),那么一次循环不能及时完成,系统缓冲区中的数据就会大量累积,得不到及时的处理,这种泵虽然确保了数据的顺序处理(即先接收到的数据先处理完毕,后接收到的数据后处理完毕,前一次处理结束之前,后一次处理不能开始),但是影响了处理效率,如何解决这个问题,请参见下一小节。

注:如何提高"泵"处理数据的效率这个问题,在第九章结尾有所提示。

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[] { "\\r\\n" },     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\\r\\n";
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\\r\\nContent-Length:{0}\\r\\n", 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\\r\\n";
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\\r\\nContent-Length:{0}\\r\\n", 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)'\\r', (byte)'\\n' }); //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-18:

在这里插入图片描述

图10-18 "泵"在框架中的体现

图10-18中方框表示框架,循环代表"泵"结构,可以看出,"泵"是框架提供动力的源头,虽然用图10-18来轻率地描述框架结构显然是不准确的,但是它足以能够说明"泵"在框架中的重要位置。

注:框架控制程序的运行流程称为"控制反转(IoC)",本书前面章节多次提到过。

本章回顾

本章主要介绍了代码中"泵"的具体表现形式,以及它对软件系统的重要性。本章可以说是对第八章和第九章的一个补充,第八章中讲"桌面GUI框架"时就已经涉及到了"泵"的概念,第九章中讲"Socket网络编程"时也已经提出了"泵"的定义,本章结合前两章的内容,系统性地对"泵"在编程中的应用做了统一阐述。

本章思考

1…NET中循环结构有哪些?分别主要用于什么场合?

A:.NET中的循环结构有for循环、foreach循环、while循环以及do-while循环。for循环主要用于重复执行指定次数的操作,foreach循环主要用于遍历容器元素,while循环和do-while循环主要用于重复执行某项操作直到某一条件满足或不满足为止。代码中的"泵"结构主要由while循环来实现。

2.简述代码中"泵"结构的作用。

A:代码中的"泵"结构具备"持续性"和"动力性"两大特点,它能够维持程序的持续运行状态,为程序运转提供动力支持。

3.串行处理数据的泵与并行处理数据的泵之间有什么区别?

A:串行处理数据的泵是按顺序处理数据的,本次数据处理结束之前,下一次处理不能开始;并行处理数据的泵不是按顺序处理数据,所有的数据处理均是同时进行的,没有先后顺序,不能确保先开始处理的数据一定先结束处理,也不能保证后开始处理的数据一定后结束处理。通过异步编程很容易实现两种泵结构。

(本章完)

以上是关于动力之源:代码中的“泵“的主要内容,如果未能解决你的问题,请参考以下文章

梦想之源

万恶之源-再谈编码

谷歌浏览器调试jsp 引入代码片段,如何调试代码片段中的js

万恶之源-Class类型反射

万恶之源-数据类型

分享《21个项目玩转深度学习:基于TensorFlow的实践详解》+PDF+源码+何之源