動力之源:程式碼中的“泵”

周見智發表於2015-02-04

相關文章連線:

程式設計之基礎:資料型別(一)

程式設計之基礎:資料型別(二)

高屋建瓴:梳理程式設計約定

完整目錄與前言

動力之源:程式碼中的"泵"    

"迴圈"語句作為一種常見的程式碼結構,幾乎存在於我們寫的任何一段程式程式碼中,它負責實現"程式碼重複執行"的功能,像.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:序列處理資料的泵是按順序處理資料的,本次資料處理結束之前,下一次處理不能開始;並行處理資料的泵不是按順序處理資料,所有的資料處理均是同時進行的,沒有先後順序,不能確保先開始處理的資料一定先結束處理,也不能保證後開始處理的資料一定後結束處理。通過非同步程式設計很容易實現兩種泵結構。

(本章完)

 

相關文章