.net開發筆記(十六) 對前部分文章的一些補充和總結


補充有兩個:

  1. 一個是系列(五)中講到的事件編程(網址鏈接),該文提及到了事件編程的幾種方式以及容易引起的一些異常,本文補充“多線程事件編程”這一塊。
  2. 第二個是前三篇博客中提及到的“泵”結構在編程中的應用,我稍微做一點補充。

總結有一個:

  1. 如果您善於總結和類比,您會發現世界好多東西其實都是一樣的。這部分主要理清楚框架時代中的框架和我們coder所寫代碼之間的關聯。

下面是正文:

多線程事件編程

    系列(五)中提及到了事件在注冊和注銷時,系統已經做了多線程處理,只是不太完美(以this為鎖對象,this是public的,鎖對象是不能對外公開的),后來通過自己定義鎖對象加鎖來實現的。可是該篇文章並沒有提到在類內部激發事件時可能引發的異常:

 1 class Subject
 2 {
 3      XXEventHandler _xx;
 4      object _xxSync = new Object();
 5      public event XXEventHandler XX
 6      {
 7          add
 8          {
 9               lock(_xxSync)
10               {
11                    _xx = (XXEventHandler)Delegate.Combine(_xx,value);
12               }
13          }
14          remove
15          {
16               lock(_xxSync)
17               {
18                   _xx = (XXEventHandler)Delegate.Remove(_xx,value);
19               }
20          }
21      }
22     protected virtual void OnXX(XXEventArgs e)
23     {
24          if(_xx != null)
25          {
26               _xx(this,e);
27          }
28     }
29    public void DoSomething()
30    {
31          //
32          OnXX(new XXEventArgs(…));
33    }
34 }
View Code

如上代碼所述,在多線程情況下,if(_xx != null)這行代碼執行為true后,在執行下一行_xx(this,e);之前,_xx可能已經為null,引發異常理所當然。解決方法很簡單,照葫蘆畫瓢,在OnXX中加鎖,源代碼變為:

 1     protected virtual void OnXX(XXEventArgs e)
 2     {
 3          lock(_xxSync)
 4          {
 5             if(_xx != null)
 6             {
 7               _xx(this,e);
 8             }
 9          }
10     }
View Code

沒錯,這樣確實能解決激發事件時有可能引發的異常,但如果僅僅是為了說明該方法可以解決問題的話,我是不會特大篇幅來說明它的。我們來看另外一種巧妙解決方法:

1     protected virtual void OnXX(XXEventArgs e)
2     {
3          XXEventHandler xx = _xx;
4          if(xx != null)
5          {
6             xx(this,e);
7          }
8     }
View Code

如上代碼所述,在判斷_xx是否為null之前,我們先用一個臨時變量代替它,之后將使用_xx的地方全部替換為xx。這樣,就不用擔心xx會由其它線程改變為null了,因為xx對其他線程不可見。這個原理很簡單,委托鏈是不可改變的(Delegates is immutable),也就是說,我們注冊或者注銷事件時,並不是在原來的委托鏈表基礎上進行增加或者刪除節點,而是每次都是重新生成了一個全新鏈表再賦給委托變量。其實這個諸位可以找到規律,我們在注冊注銷事件時,一般obj.Event+=…或者Event = Delegate.Combine(…) Event = Delegate.Remove(),可以看出,每次都是將一個全新的值賦給原來委托變量,並沒有在原來鏈表基礎上進行操作,因此,_xx和xx雖然同是指向同一鏈表,但是我們注銷注冊事件時,只是讓_xx指向另外一個鏈表而已,原鏈表(xx)並沒有變。

    這個其實就是我們剛學習編程的時候,使用值傳遞調用方法時,實參將值傳遞給了形參,形參如果改變了(被重新賦值),實參的值是不會變的。指針(引用)也一樣,形參指向了另外一個對象,實參還是指向原來的對象。

“泵”結構的另外一種方式

    系列(十三)中(網址鏈接)講到,在泵結構中,如果在獲取數據環節直接處理數據容易降低獲取數據的效率,也就是說,最好不要一獲取到數據就處理它,因為處理數據大多數情況下是一個耗時過程,數據處理結束前,下一次“數據獲取”不能開始,影響獲取數據的效率。如下圖:

圖1

如圖所示,處理數據在泵循環體內,數據處理結束之前,緩沖區中的數據就會大量積累。我們當時的做法是,獲取數據后不馬上進行分析處理,而是先將數據寫入一個有序緩沖區,然后另創建“泵”去分析處理這些數據,這樣一來,不會影響數據獲取環節的效率。因此,諸位可以看見有三個“泵”(數據接收,數據分析,數據處理)聯合工作。

    事實上,太多“泵”協作工作也是會影響整個系統效率的,這就像多個人協同工作,雖然人多力量大,但是人多需要考慮同步共享資源、人跟人之間的協作能力等情況,這個好比“生產者消費者模式”,

圖2

當“生產者-緩沖區-消費者”這一結構過多時,數據從接收到最終被處理,是需要一個漫長的過程,因此,我們需要尋找一個平衡點。有兩種改進方式:

1)

圖3

如上圖,接收數據后,直接開啟異步分析和處理過程。

2)

圖4

如上圖,數據接收后,將其寫入緩沖區,然后另外再開啟線程分析和處理數據,這個就把“數據分析”和“數據處理”合並在一塊了。這兩個嚴格來說耦合度比原來那個要高。

    注:在通訊編程中,圖3適合UDP通信,因為UDP每次接受到的數據都是一個完整的數據包,數據接收后直接開始分析處理,圖4適合TCP通信,因為TCP傳輸數據是以“流”格式傳輸的,並且每次接收到的數據不一定是完整的,我們必須先將接收到的數據按順序寫入一個有序的緩沖區中,然后再從緩沖區中提取完整的數據進行分析處理。

框架與客戶端代碼之間的關系

    總結這個的主要原因是上次在網上看見有一個人問,使用基類引用指向一個派生類實例時,為什么不能通過該引用訪問派生類中使用new關鍵字覆蓋基類的方法,而只能訪問到基類中的方法。我看了他給出的實例代碼,發現其實根本就沒必要使用基類引用去指向派生類實例,純屬濫用。是的,好多時候我們不知道為什么要那么使用,只因為我們看見別人那樣用過,代碼:

 1 class People
 2 {
 3 string _name;
 4 string _sex;
 5 //
 6 public void Info()
 7 {
 8     ShowInfo();
 9 }
10 protected virtual void ShowInfo()
11 {
12     Console.WriteLine(“基本信息  姓名:”+_name+” 性別:”+_sex);
13 }
14 }
15 class Student:People
16 {
17 //
18 protected override void ShowInfo()
19 {
20     base.ShowInfo();
21     Console.WriteLine(“附加信息  職業:學生”);
22 }
23 }
24 
25 class Teacher:People
26 {
27 //
28 protected override void ShowInfo()
29 {
30     base.ShowInfo();
31     Console.WriteLine(“附加信息  職業:教師”);
32 }
33 }
View Code

以上三個類型,現在假設我要輸出某一類型對象的信息,該怎么寫?

public void Func(People p)

{

    p.Info();

}

這是大多數人的寫法,理由很簡單,它既可以輸出Student的信息也可以輸出Teacher的信息,確實是這樣的,但是當你確定要輸出信息的對象類型時(而且很多時候屬於這種情況),是沒必要這樣寫的,比如你確定要輸出信息的對象類型為Student,那么你完全可以這樣:

public void Func(Student s)

{

    s.Info();

}

    我真不明白為什么你明明非常確定要使用哪個類型,卻偏偏要用基類引用代替派生類引用,就是因為大家常說的“依賴於抽象而非具體”嗎?這個話沒錯,但要看場合,當你不確定要使用哪個類型時,你可以用一個抽象引用(基類引用),當你已經非常確定了使用哪個類型時,你就沒必要再去使用一個抽象引用了,直接使用具體引用(派生類引用)。抽象引用能完成的東西,具體引用都能做到,反過來卻不成立,如果Student類中有一個public DoHomework(),你能用People類型的引用去訪問它嗎?你根本不能。

    因此,可以很大膽地說,“依賴於抽象而非具體”是一個迫不得已的結論,如果編程世界里沒有那么多的不確定,完全不需要這個結論,誰會去使用一個不確定性的東西呢?可是,事實上編程世界里有太多的不確定,表現最為明顯的就是框架中,之所以框架中有那么多的不確定性,那是因為通常情況下,框架具有“通用性”(沒有通用性的也就不叫框架了),也就是說,框架可以使用在多個場合下,而框架編寫者則完全不知道每個具體場合是什么樣的,有哪些功能,每個功能怎么實現的,既然不知道具體情況,那么框架編寫者只有使用一系列抽象引用臨時代替了。

    現在既然不確定性無可避免,那么,怎么才能讓框架本身與客戶端代碼(框架使用者編寫的代碼)能夠很好的“協同工作”呢?此時,我們打開我們發達的大腦,開始拼命想象,噴血聯想,協同工作?好像通信中經常聽到的詞語,兩個遠程主機如果想要協同工作,雙方必須遵守同一個通信協議,如下圖:

圖5

那么,我們完全可以把“框架”當做服務端,框架使用者編寫的代碼就為客戶端了,他們之間協同工作也應該遵守相同的協議,如下圖:

圖6

具體編碼中,這個協議就表現為接口(Interface)或基類(相對而言)這樣的東西,框架中使用這些東西訪問客戶端代碼,客戶端代碼也必須實現這些接口或者派生自這些基類。

     像框架這種依賴於抽象的做法在解決通用性的同時,還能最大限度降低耦合度,框架編寫者完全不用關心使用者的具體實現,使用者只要遵守協議,怎么實現不歸框架管。           當然也有缺陷,就是框架只能通過事先規定的協議去訪問客戶端代碼,客戶端代碼中如果有協議之外的東西,框架是訪問不到的。這就要求框架編寫者在編寫框架的時候考慮充分,將所有有可能涉及到的東西都歸納到協議之中。

    本篇需結合前面三篇博客(與“泵”有關的)一起閱讀。希望對各位有幫助。


免責聲明!

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



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