.Net開發筆記(五) 關於事件


我前面幾篇博客中提到過.net中的事件與Windows事件的區別,本文討論的是前者,也就是我們代碼中經常用到的Event。Event很常見,Button控件的Click、KeyPress等等,PictureBox控件的Paint等等都屬於本文討論范疇,本文會例舉出有關“事件編程”的幾種方法,還會提及由“事件編程”引起的Memory Leak(跟“內存泄露”差不多),以及由“事件編程”引起的一些異常。

引子:

.net中事件最常用在“觀察者”設計模式中,事件的發布者(subject)定義一個事件,事件的觀察者(observer)注冊這個事件,當發布者激發該事件時,所有的觀察者就會響應該事件(表現為調用各自的事件處理程序)。知道這個邏輯過程后,我們可以寫出以下代碼:

View Code
 1 Class Subject
 2 {
 3      public event XXEventHandler XX;
 4      protected virtual void OnXX(XXEventArgs e)
 5      {
 6           If(XX!=null)
 7           {
 8                XX(this,e);
 9           }
10      } 
11      public void DoSomething()
12      {
13           //符合某一條件
14           OnXX(new XXEventArgs()); 
15      }
16 }
17 delegate void XXEventHandler(object sender,XXEventArgs e);
18 Class XXEventArgs:EventArgs
19 {
20     
21 }

以上就是一個最最原始的含有事件類的定義。外部對象可以注冊Subject對象的XX事件,當某一條件滿足時,Subject對象就會激發XX事件,所以觀察者作出響應。注:編碼中請按照標准的命名方式,事件名、事件參數名、虛方法名、參數名等等,標准請參考微軟。

事件觀察者注冊事件代碼為:

View Code
Subject sub = new Subject();
Sub.XX += new XXEventHandler(sub_XX);

void sub_XX(object sender,XXEventArgs e)
{
     //do something
}

以上是一個最簡單的“事件編程”結構代碼,其余所有的寫法都是從以上擴展出來的,基本原理不變。

升級:

在定義事件變量時,有時候我們可以這樣寫:

View Code
 1 Class Subject
 2 {
 3      private XXEventHandler _xx;
 4      public event XXEventHandler XX
 5      {
 6           add
 7           {
 8                _xx = (XXEventHandler)Delegate.Combine(_xx,value);
 9           }
10           remove
11           {
12                _xx = (XXEventHandler)Delegate.Remove(_xx,value);
13           }
14      }
15      protected virtual void OnXX(XXEventArgs e)
16      {
17           if(_xx!=null)
18           {
19                _xx(this,e);
20           }
21      } 
22      public void DoSomething()
23      {
24           //符合某一條件
25           OnXX(new XXEventArgs()); 
26      }
27 }

其余代碼跟之前一樣,升級后的代碼顯示的實現了“add/remove”,顯示實現“add/remove”的好處網上很多人都說可以在注冊事件之前添加額外的邏輯,這個就像“屬性”和“字段”的關系,

View Code
 1 public event XXEventHandler XX
 2      {
 3           add
 4           {
 5                 //添加邏輯
 6                _xx = (XXEventHandler)Delegate.Combine(_xx,value);
 7           }
 8           remove
 9           {
10                //添加邏輯
11 _xx = (XXEventHandler)Delegate.Remove(_xx,value);
12           }
13      }

沒錯,確實與“屬性(Property)”的作用差不多,但它不止這一個好處,我們知道(不知道的上網看看),在多線程編程中,很重要的一點就是要保證對象“線程安全”,因為多線程同時訪問同一資源時,會出現預想不到的結果。當然,在“事件編程”中也要考慮多線程的情況。“引子”部分代碼經過編譯器編譯后,確實可以解決多線程問題,但是存在問題,它經過編譯后:

View Code
 1 public event XXEventHandler XX;
 2 //該行代碼編譯后類似如下:
 3 
 4 private XXEventHandler _xx;
 5 [MethodImpl(MethodImplOptions.Synchronized)]
 6 public void add_XX(XXEventHandler handler)
 7 {
 8 _xx = (XXEventHandler)Delegate.Combine(_xx,handler);
 9 }
10 
11 [MethodImpl(MethodImplOptions.Synchronized)]
12 public void remove_XX(XXEventHandler handler)
13 {
14     _xx = (XXEventHandler)Delegate.Remove(_xx,handler);
15 }

以上轉換為編譯器自動完成,事件(取消)注冊(+=、-=)間接轉換由add_XX和remove_XX代勞,通過在add_XX方法和remove_XX方法前面添加類似[MethodImpl(MethodImplOptions.Synchronized)]聲明,表明該方法為同步方法,也就是說多線程訪問同一Subject對象時,同時只能有一個線程訪問add_XX或者是remove_XX,這就確保了不可能同時存在兩個線程操作_xx這個委托鏈表,也就不可能發生不可預測結果。那么,[MethodImpl(MethodImplOptions.Synchronized)]是怎么做到線程同步的呢?其實查看IL語言,我們不難發現,[MethodImpl(MethodImplOptions.Synchronized)]的作用類似於下:

View Code
 1 Class Subject
 2 {
 3 private XXEventHandler _xx;
 4 public void add_XX(XXEventHandler handler)
 5 {
 6     lock(this)
 7     {
 8        _xx = (XXEventHandler)Delegate.Combine(_xx,handler);
 9      }
10 }
11 public void remove_XX(XXEventHandler handler)
12 {
13      lock(this)
14      {
15            _xx = (XXEventHandler)Delegate.Remove(_xx,handler);
16       }
17 }
18 }

如我們所見,它就相當於給自己加了一個同步鎖,lock(this),我不知道諸位在使用同步鎖的時候有沒有刻意去避免lock(this)這種,我要說的是,使用這種同步鎖要謹慎。原因至少兩個:

1) 將自己(Subject對象)作為鎖定目標的話,客戶端代碼中很可能仍以自己為目標使用同步鎖,造成死鎖現象。因為this是暴露給所有人的,包括代碼使用者。

View Code
 1 private void DoWork(Subject sub) //客戶端代碼
 2 {
 3      lock(sub)   //客戶端代碼鎖定sub對象
 4      {
 5          sub.XX+=new XXEventHandler(…); //嵌套鎖定同一目標
 6          // sub.add_XX(new XXEventHandler(…));相當於調用add_XX,出現死鎖
 7          // 
 8          //
 9          //
10          //do other thing
11      }
12 }

2) 當Subject類包含多個事件,XX1、XX2、XX3、XX4…時,每注冊(或取消)一個事件時,都需要鎖定同一目標(Subject對象),這完全沒必要。因為不同的事件有不同的委托鏈表,多個線程完全可以同時訪問不同的委托鏈表。然而,編譯器還是這樣做了。

View Code
 1 Class Subject
 2 {
 3       private XXEventHandler _xx1
 4       private EventHandler _xx2;
 5       public void add_XX1(XXEventHandler handler)
 6 {
 7            lock(this)
 8            {
 9 _xx1 = (XXEventHandler)Delegate.Combine(_xx1,handler);
10               }
11 }
12 public void remove_XX1(XXEventHandler handler)
13 {
14            lock(this)
15 {
16                   _xx1 = (XXEventHandler)Delegate.Remove(_xx1,handler);
17               }
18 
19 }
20 public void add_XX2(EventHandler handler)
21 {
22         lock(this)
23         {
24 _xx2 = (EventHandler)Delegate.Combine(_xx2,handler);
25            }
26 }
27 public void remove_XX2(EventHandler handler)
28 {
29         lock(this)
30 {
31               _xx2= (EventHandler)Delegate.Remove(_xx2,handler);
32            }
33 }
34  }

 在一個線程中執行sub.XX1+=new XXEventHandler(…)(間接調用sub.add_XX1(new XXEventHandler(…)))的時候,完全可以在另一線程中同時執行 sub.XX2+=new EventHandler(…)(間接調用sub.add_XX2(new EventHandler(…)))。_xx1和_xx2兩個沒有任何聯系,訪問他們更不需要線程同步。如果這樣做了,影響性能效率(編譯器自動轉換成的代碼就是這樣子)。

結合以上兩點,可以將“升級”部分代碼修改為以下,從而可以很好的解決“線程安全”問題而且不會像編譯器自動轉換的代碼那樣影響效率:

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

在Subject類中增加一個同步鎖目標“_xxSync”,不再以對象本身為同步鎖目標,這樣_xxSync只在類內部可見(客戶端代碼不可使用該對象作為同步鎖目標),不會出現死鎖現象。另外,如果Subject有多個事件,那么我們可以完全增加多個類似“_xxSync”這樣的東西,比如“_xx1Sync、_xx2Sync…”等等,每個同步鎖目標之間沒有任何關聯。

當一個類(比如前面提到的Subject)中包含的事件增多時,幾十個甚至幾百個,而且派生類還會增加事件,在這種情況下,我們需要統一管理這些事件,由一個集合來統一管理這些事件是個不錯的選擇,比如:

View Code
  1 Class Subject
  2 {
  3      protected Dictionary<object,Delegate> _handlerList = new Dictionary<object,Delegate>();
  4      Static object _XX1_KEY = new object();
  5      Static object _XX2_KEY = new object();
  6      Static object _XXn_KEY = new object();
  7 
  8      //事件
  9      public event EventHandler XX1
 10      {
 11          add
 12          {
 13              if(_handlerList.ContainsKey(_XX1_KEY))
 14              {
 15                   _handlerList[_XX1_KEY] = Delegate.Combine(_handlerList[_XX1_KEY],value);
 16              }
 17              else
 18              {
 19                   _handlerList.Add(_XX1_KEY,value);
 20              }
 21          }
 22          remove
 23          {
 24              if(_handlerList.ContainsKey(_XX1_KEY))
 25              {
 26                   _handlerList[_XX1_KEY] = Delegate.Remove(_handlerList[_XX1_KEY],value);
 27              }
 28          }
 29      }
 30      public event EventHandler XX2
 31      {
 32          add
 33          {
 34               if(_handlerList.ContainsKey(_XX2_KEY))
 35              {
 36                   _handlerList[_XX2_KEY] = Delegate.Combine(_handlerList[_XX2_KEY],value);
 37              }
 38              else
 39              {
 40                   _handlerList.Add(_XX2_KEY,value);
 41              }
 42          }
 43          remove
 44          {
 45               if(_handlerList.ContainsKey(_XX2_KEY))
 46              {
 47                   _handlerList[_XX2_KEY] = Delegate.Remove(_handlerList[_XX2_KEY],value);
 48              }
 49          }
 50      }
 51      public event EventHandler XXn
 52      {
 53          add
 54          {
 55              if(_handlerList.ContainsKey(_XXn_KEY))
 56              {
 57                   _handlerList[_XXn_KEY] = Delegate.Combine(_handlerList[_XXn_KEY],value);
 58              }
 59              else
 60              {
 61                   _handlerList.Add(_XXn_KEY,value);
 62              }
 63 
 64          }
 65          remove
 66          {
 67              if(_handlerList.ContainsKey(_XXn_KEY))
 68              {
 69                   _handlerList[_XXn_KEY] = Delegate.Remove(_handlerList[_XXn_KEY],value);
 70              }
 71 
 72          }
 73      }
 74      protected virtual void OnXX1(EventArgs e)
 75      {
 76           if(_handlerList.ContainsKey(_XX1_KEY))
 77           {
 78                EventHandler handler = _handlerList[_XX1_KEY] as EventHandler;
 79                If(handler != null)
 80                {
 81                      Handler(this,e);
 82                }
 83           }
 84      } 
 85     protected virtual void OnXX2(EventArgs e)
 86      {
 87           if(_handlerList.ContainsKey(_XX2_KEY))
 88           {
 89                EventHandler handler = _handlerList[_XX2_KEY] as EventHandler;
 90                if(handler != null)
 91                {
 92                      Handler(this,e);
 93                }
 94           }
 95     }
 96     protected virtual void OnXXn(EventArgs e)
 97      {
 98           if(_handlerList.ContainsKey(_XXn_KEY))
 99           {
100                EventHandler handler = _handlerList[_XXn_KEY] as EventHandler;
101                If(handler != null)
102                {
103                      Handler(this,e);
104                }
105           }
106      } 
107 
108      public void DoSomething()
109      {
110           //符合某一條件
111           OnXX1(new EventArgs()); 
112           OnXX2(new EventArgs());
113           OnXXn(new EventArgs());
114      }
115 }

存放事件委托鏈表的容器為Dictionary<object,Delegate>類型,該容器存放各個委托鏈表的表頭,每當有一個“事件注冊”的動作發生時,先查找字典中是否有表頭,如果有,直接加到表頭后面;如果沒有,向字典中新加一個表頭。“事件注銷”操作類似。

圖1

字典的作用是將每個委托鏈表的表頭組織起來,便於查詢訪問。可能有人已經看出來修改后的代碼並沒有考慮“線程安全”問題,的確,引進了集合去管理委托鏈表之后,再也沒辦法解決“線程安全”而又不影響效率了,因為現在各個事件不再是獨立存在的,它們都放在了同一集合。另外,集合Dictionary<object,Delegate>聲明為protected,子類完全可以使用該集合對子類的事件委托鏈表進行管理。

      注:上圖中委托鏈中各節點引用的都是實例方法,沒有列舉靜態方法。

其實,.net中所有從System.Windows.Forms.Control類繼承下來的類,都是用這種方式去維護事件委托鏈表的,只不過它不是用的字典(我只是用字典模擬),它使用一個EventHandlerList類對象來存儲所有的委托鏈表表頭,作用跟Dictionary<object,Delegate>差不多,並且,.net中也沒去處理“線程安全”問題。總之,CLR在處理“線程安全”問題做得不是足夠好,當然,一般事件編程也基本用在單線程中(比如Winform中的UI線程中),打個比方,在UI線程中創建的Control(或其派生類),基本上都在同一線程中訪問它,基本不涉及跨線程去訪問Control(或其派生類),所以大可不必擔心事件編程中遇到“線程安全”問題。

事件編程中的內存泄露

說到“內存泄露”,可能很多人認為這不應該是.net討論的問題,因為GC自動回收內存,不需要編程的人去管理內存,其實不然。凡是發生了不能及時釋放內存的情況,都可以叫“內存泄露”,.net中包括“托管內存”也包括“非托管內存”,前者由GC管理,后者必然由編程者考慮了(類似C++中的內存),這里我們討論的是前者,也就是托管內存的泄露。

我們知道(假設諸位都知道),當一個托管堆中的對象不可達時,也就是程序中沒有對該對象有引用時,該對象所占堆內存就屬於GC回收的范圍了。可是,如果編程者認為一個對象生命期應該結束(該對象不再使用)的時候,同時也理所當然地認為GC會回收該對象在堆中占用的內存時,情況往往不是TA所認為的那樣,應為很有可能(概率很大),該對象在其他的地方仍然被引用,而且該引用相對來說不會很明顯,我們叫這個為“隱式強引用”(Implicit strong reference),而對於Class A = new Class();這樣的代碼,A就是“顯示強引用”(Explicit strong reference)了。(至於什么是強引用什么是弱引用,這個在這里我就不說了)那么,不管是“顯示強引用”還是“隱式強引用”都屬於“強引用”,一個對象有一個強引用存在的話,GC就不會對它進行內存回收。

事件編程中,經常會產生“隱式強引用”,參考前面的“圖1”中委托鏈表中的每個節點都包含一個target,當一個事件觀察者向發布者注冊一個事件時,那么,發布者就會保持一個觀察者的強引用,這個強引用不是很明顯,因此我們稱之為隱式強引用。因此,當觀察者被編程者理所當然地認為生命期結束了,再沒有任何對它的引用存在時,事件發布者卻依然保持了一個強引用。如下圖:

圖2

盡管有時候,Observer生命期結束(我們理所當然地那樣認為),Subject(發布者)卻依舊對Observer有一個強引用(strong reference)(圖2中紅色箭頭),該引用稱作為“隱式強引用”。GC不會對Observer進行內存回收,因為還有強引用存在。如果Observer為大對象,且系統存在很多這樣的Observer,當系統運行時間足夠長,托管堆中的“僵屍對象”(有些對象雖然已經沒有使用價值了,但是程序中依舊存在對它的強引用)越來越多,總有一個時刻,內存不足,程序崩潰。

事件編程中引起的異常

其實還是因為我們的Observer注冊了事件,但在Observer生命期結束(編程者認為的)時,釋放了一些必備資源,但是Subject還是對Observer有一個強引用,當事件發生后,Subject還是會通知Observer,如果Observer在處理事件的時候,也就是事件處理程序中用到了之前已經釋放了的“必備資源”,程序就會出錯。導致這個異常的原因就是,編程者以為對象已經死了,將其資源釋放,但對象本質上還未死去,仍然會處理它注冊過的事件。

View Code
 1 //Form1.cs中:
 2 private void form1_Load(object sender,EventArgs e)
 3 {
 4       Form2 form2 = new Form2();
 5       form2.Click += new EventHandler(form2_Click);
 6       form2.Show();
 7 }
 8 private void form2_Click(object sender,EventArgs e)
 9 {
10       this.Show();
11 }

form1為Observer,form2為Subject,form1監聽form2的Click事件,在事件處理程序中將自己Show出來,一切運行良好,但是,當form1關閉后,再次點擊form2激發Click事件時,程序報錯,提示form1已經disposed。原因就是我們關閉form1時,認為form1生命期已經結束了,事實上並非如此,form2中還有對form1的引用,當事件發生后,還是會通知form1,調用form1的事件處理程序(form2_Click),而碰巧的是,事件處理程序中調用了this.Show()方法,意思要將form1顯示出來,可此時form1已經關閉了。

小結  

不管是內存泄露還是引起的異常,都是因為我們注冊了某些事件,在對象生命期結束時,沒有及時將已注冊的事件注銷,告訴事件發布者“我已死,請將我的引用刪除”。因此一個簡單的方法就是在對象生命期結束時將所有的事件注銷,但這個只對簡單的代碼結構有效,復雜的系統幾乎無效,事件太多,根本無法記錄已注冊的事件,再者,你有時候根本不知道對象什么時候生命期結束。下次介紹利用弱引用概念(Weak reference)引申出來的弱委托(Weak delegate),它能有效地解決事件編程中內存泄露問題。原理就是將圖2中每個節點中的Target由原來的強引用(Strong Reference)改為弱引用(Weak Reference)。

希望有幫助O(∩_∩)O~。

跟之前一樣,代碼未調試運行,可能有錯誤。


免責聲明!

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



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