動力之源:代碼中的“泵”


相關文章連接:

編程之基礎:數據類型(一)

編程之基礎:數據類型(二)

高屋建瓴:梳理編程約定

完整目錄與前言

動力之源:代碼中的"泵"    

"循環"語句作為一種常見的代碼結構,幾乎存在於我們寫的任何一段程序代碼中,它負責實現"代碼重復執行"的功能,像.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[] { "\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.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:串行處理數據的泵是按順序處理數據的,本次數據處理結束之前,下一次處理不能開始;並行處理數據的泵不是按順序處理數據,所有的數據處理均是同時進行的,沒有先后順序,不能確保先開始處理的數據一定先結束處理,也不能保證后開始處理的數據一定后結束處理。通過異步編程很容易實現兩種泵結構。

(本章完)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM