上一篇部落格最後我提到“非同步程式設計模型”(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 }
你可能會說,func()是我主動呼叫的,所以它不是回撥方法,但是你要明白,不管你程式碼怎麼寫,它最終執行的主動權不是在你手中,既然主動權不在你手中,那麼它就應該屬於回撥。
2)這裡的“系統”其實是相對而言的,我前面都把它當做作業系統,其實不然,我們口口聲聲說“系統呼叫的方法屬於回撥方法”,這裡的“系統”絕大多數是指程式設計中使用到的“框架”,你比如如下程式碼:
1 btn1.Click+=new EventHandler(btn1_Click); 2 private void btn1_Click(object sender,EventArgs e) 3 { 4 //do something 5 }
這裡的“回撥方法”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 }
如你所見,以上程式碼很好的解釋了程式設計中的“泵”含義。那麼,程式中為啥需要使用泵?原因可以從泵的作用中找,我們很清楚,平時除錯一些程式碼時,可能很多人使用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 }
其根本原因,是我們寫的程式是“線段”狀,所謂線段,即它是有限長度的直線,執行緒從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
如果func中沒有訪問外部變數,基本上不會出問題,但是如果func中訪問了外部物件,而該物件不是執行緒安全的,那麼你就得在func中做一些“安全措施”了,這點很容易被忽略,如下:
1 List<int> list = new List<int>(); 2 void func() 3 { 4 list.Add(DateTime.Now.Hours); 5 }
我們在設計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中取出訊息,分配給對應的視窗過程,供其使用。
有人可能會說,幹嘛要分三個“泵”,一個“泵”不就能搞定嗎,在“資料採集泵”中分析資料、轉換資料、處理資料?不能的原因至少有兩個:
- 各所其職,符合軟體開發的原則
- 如果什麼東西都放在一個“泵”中做,必然會影響原有的效率,比如將“資料分析”放在“資料採集泵”中,勢必會影響採集的效率,其他類似。
以上是“泵”在Windows訊息處理中的應用。接下來說一下Socket程式設計中的應用,我以UDP通訊為例,TCP類似。
(2)我們先理清UDP通訊流程:遠端主機給本地主機傳送一個UDP資料包,需要注意的是,在到達本地主機之前(傳輸過程中),資料包應該是一種物理資訊,經過網路卡驅動轉換後,物理資訊變成數字資訊,存放在緩衝區A中(一串位元組流,稱之為原始資料),此時,需要一個“資料接收泵”迴圈取出緩衝區A中的原始資料(UDP中該資料應該是一個完整的資料包),將其存放到緩衝區B中,對應有一個“資料分析泵”迴圈取出緩衝區B中的原始資料,根據事先規定好的“協議”(一種通訊規則,通訊各方必須同時遵守),將該原始資料解析成程式可識別資料(資料頭,程式中可識別資料,遠端IP埠等),緊接著將解析之後的資料存放到緩衝區C,對應又有一個“資料處理泵”迴圈從C中取出資料,分配資料,通知他人處理。上圖一張:
圖6
現在已經很清楚,這個模式跟“windows訊息迴圈”是一個意思,接收資料->分析資料->處理資料,每個環節都有一個“泵”與之關聯,當然還有一個緩衝區。其實再擴充一下,我們會發現它們都有輸入,都有分析,都有響應
- 前者滑鼠輸入,後者遠端輸入;
- 前者有分析泵,後者照樣有;
- 前者激發一些事件,比如Winform中的Click事件,你可以在事件處理程式中訪問資料庫、操作IO、更新介面,後者你註冊相關事件之後,照樣可以做這些事情。
再不說了,說多了都是淚,發現原來它們都是一樣一樣的。TCP跟UDP差不多,只是服務端需要有“socket偵聽泵”用來監聽socket連入,而且每個連入的socket都對應有自己的“資料接收泵”跟“資料分析泵”,原因很簡單,因為TCP按照“流”來傳輸資料的,資料包之間沒有界限,某一次接收到的“原始資料”可能不是一個完整的包,因此,每個客戶端socket必須有自己的“資料接收泵”和“資料分析泵”以及對應的緩衝區,並且“資料分析泵”中還要具備檢測完整包的功能。TCP版本Demo以後我再做一個,稍微比UDP複雜一點。
以上是所有的介紹,理論性的東西非常多,下一篇文章我打算分享一個UDP通訊demo,採用本篇所講內容,簡單的實現了類似飛鴿傳書的功能。
順便帶個題外話,這一系列文章可能跟實際具體開發關聯性不是很大,特別像之前說到的“執行時和設計時”、“winform框架原理”等等這些,基本上跟平時工作沾不上邊,我也沒有刻意去寫平時工作中遇到的問題,寫出來的東西大都是概念性、原理性偏多一些。各位在看的時候沒必要跟實際工作內容做比較,全當做是一種業餘研究就OK了, O(∩_∩)O~。