.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~。


免責聲明!

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



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