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

跟之前一样,代码未调试运行,可能有错误。

posted @ 2013-01-14 15:36  周见智  阅读(4125)  评论(7编辑  收藏  举报