補充有兩個:
- 一個是系列(五)中講到的事件編程(網址鏈接),該文提及到了事件編程的幾種方式以及容易引起的一些異常,本文補充“多線程事件編程”這一塊。
- 第二個是前三篇博客中提及到的“泵”結構在編程中的應用,我稍微做一點補充。
總結有一個:
- 如果您善於總結和類比,您會發現世界好多東西其實都是一樣的。這部分主要理清楚框架時代中的框架和我們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 }
如上代碼所述,在多線程情況下,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 }
沒錯,這樣確實能解決激發事件時有可能引發的異常,但如果僅僅是為了說明該方法可以解決問題的話,我是不會特大篇幅來說明它的。我們來看另外一種巧妙解決方法:

1 protected virtual void OnXX(XXEventArgs e) 2 { 3 XXEventHandler xx = _xx; 4 if(xx != null) 5 { 6 xx(this,e); 7 } 8 }
如上代碼所述,在判斷_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 }
以上三個類型,現在假設我要輸出某一類型對象的信息,該怎么寫?
public void Func(People p)
{
p.Info();
}
這是大多數人的寫法,理由很簡單,它既可以輸出Student的信息也可以輸出Teacher的信息,確實是這樣的,但是當你確定要輸出信息的對象類型時(而且很多時候屬於這種情況),是沒必要這樣寫的,比如你確定要輸出信息的對象類型為Student,那么你完全可以這樣:
public void Func(Student s)
{
s.Info();
}
我真不明白為什么你明明非常確定要使用哪個類型,卻偏偏要用基類引用代替派生類引用,就是因為大家常說的“依賴於抽象而非具體”嗎?這個話沒錯,但要看場合,當你不確定要使用哪個類型時,你可以用一個抽象引用(基類引用),當你已經非常確定了使用哪個類型時,你就沒必要再去使用一個抽象引用了,直接使用具體引用(派生類引用)。抽象引用能完成的東西,具體引用都能做到,反過來卻不成立,如果Student類中有一個public DoHomework(),你能用People類型的引用去訪問它嗎?你根本不能。
因此,可以很大膽地說,“依賴於抽象而非具體”是一個迫不得已的結論,如果編程世界里沒有那么多的不確定,完全不需要這個結論,誰會去使用一個不確定性的東西呢?可是,事實上編程世界里有太多的不確定,表現最為明顯的就是框架中,之所以框架中有那么多的不確定性,那是因為通常情況下,框架具有“通用性”(沒有通用性的也就不叫框架了),也就是說,框架可以使用在多個場合下,而框架編寫者則完全不知道每個具體場合是什么樣的,有哪些功能,每個功能怎么實現的,既然不知道具體情況,那么框架編寫者只有使用一系列抽象引用臨時代替了。
現在既然不確定性無可避免,那么,怎么才能讓框架本身與客戶端代碼(框架使用者編寫的代碼)能夠很好的“協同工作”呢?此時,我們打開我們發達的大腦,開始拼命想象,噴血聯想,協同工作?好像通信中經常聽到的詞語,兩個遠程主機如果想要協同工作,雙方必須遵守同一個通信協議,如下圖:
圖5
那么,我們完全可以把“框架”當做服務端,框架使用者編寫的代碼就為客戶端了,他們之間協同工作也應該遵守相同的協議,如下圖:
圖6
具體編碼中,這個協議就表現為接口(Interface)或基類(相對而言)這樣的東西,框架中使用這些東西訪問客戶端代碼,客戶端代碼也必須實現這些接口或者派生自這些基類。
像框架這種依賴於抽象的做法在解決通用性的同時,還能最大限度降低耦合度,框架編寫者完全不用關心使用者的具體實現,使用者只要遵守協議,怎么實現不歸框架管。 當然也有缺陷,就是框架只能通過事先規定的協議去訪問客戶端代碼,客戶端代碼中如果有協議之外的東西,框架是訪問不到的。這就要求框架編寫者在編寫框架的時候考慮充分,將所有有可能涉及到的東西都歸納到協議之中。
本篇需結合前面三篇博客(與“泵”有關的)一起閱讀。希望對各位有幫助。