test.OnBoiled += (s, e) => Console.WriteLine("加热完成事件被调用");
var heaters = new List() { test, test };
heaters.Clear();
test.Begin();
test = null;
GC.Collect();
执行结果如下图:
这种情况下,test即使被赋值为null,事件还是会乖乖执行,因为是匿名函数,你也没法取消订阅,而GC强制收集也没用! 这就是我们真实场景中最可怕的事情,你认为它已经消失了,可是它还挂在事件上!
其实这里有个破绽:Heater类里开了线程,我即使赋值为null,线程肯定还没有被销毁,事件确实可能会执行,时间所限,我没有尝试在写一个类测试不开线程的情况,有兴趣的读者可以帮忙试一试。
而且,经过我查阅资料,当你的对象订阅了外部的事件,而又没有取消订阅,那么该对象是不会被GC回收的!这会造成很恐怖的问题,产生了几千万个对象没法被回收。可是,匿名函数让我怎么么取消订阅?!
所以我们得到了结论,除非确实是一般场景,比如界面开发的window,生成了一直存在,或者在应用程序关闭时回收,否则少用匿名函数吧!记得取消事件订阅!否则会是非常麻烦的事情!
五.高潮: 多线程和事件
多线程本来就是程序员头疼的问题,笔者在多线程知识上只是入门,没开发过高并发系统,倒是经常用并行库加速算法执行。 让我们看看多线程和事件两个最难搞的东西纠缠在一起时是个什么样子。
一种常见的场景,是事件处理很耗时,比如执行长时间的IO操作,或者进行了复杂的数学计算,我们不想影响主线程,那么你想当然的会通过多线程的方法解决。
创建对象的线程,一般是主线程(或者UI线程),那么,怎么让事件处理函数在另外一个线程执行呢? 你真的保证处理函数在另外一个线程中执行了?异步调用?好办法,不过我们此处不说这个。
//////////////////**************///////////////////////////
修正:经过了重新的测试,发现我的测试用例写的有问题,为了让Heater类自己触发事件,我在内部写了一个新线程,导致测试不准确。
结论应该是: 不论是不是在多线程环境下,事件处理函数一定在触发事件位置所在的线程中,和事件订阅者的创建线程,订阅事件时所在的线程无关。。。。。。我第五节的内容,有多半都是错的。。。。
因此,若是触发事件所在线程是主线程的话,基本上只能用我提出的第二种做法,通过事件内部使用线程池来执行了。感谢 的讨论。
/////////////////*************/////////////////////
1. 新建线程方法:
初学者会这么做:
test.OnBoiled += (s, e) =>
var newThread = new Thread(
new ThreadStart(
Thread.Sleep(2000); //模拟长时间操作
Console.WriteLine("总算把热好的水加到了暖瓶里");
newThread.Start();
test.Begin();
我的手指还是选择了匿名函数,用起来真爽,这种情况下,显然事件处理函数所在线程和主线程不一样。
可是,稍微有点基础的人就知道,当事件被频繁触发时,线程就会被频繁生成,线程同样是非常昂贵的系统资源,更何况,线程的启动时间是不确定的,可能会耽误大事。这不是个好方案。
2. 线程池
采用.NET 4.0的线程池试试看,代码如下:
var mainThread = Thread.CurrentThread;
test.OnBoiled += (s, e) =>
ThreadPool.QueueUserWorkItem((d) =>
Thread.Sleep(2000); //模拟长时间操作
Console.WriteLine("总算把热好的水加到了暖瓶里");
if (Thread.CurrentThread != mainThread)
Console.WriteLine("两者执行的是不同的线程");
Console.WriteLine("两者执行的是相同的线程");
test.Begin();
我们通过缓存主线程,并比较处理函数中的线程,得到结果如下:
确实,采用线程池时,会是两个是不一样的线程,线程池由于内部做了管理,因此可以有效的利用线程,避免疯狂新开线程造成的严重的性能问题。
可是,我觉得还是麻烦,尤其是有多种事件时,挨个写线程池还是太麻烦了。那么,我们是不是有两种方案?
一种是将构造函数写在一个新线程中,另外一种是将事件订阅函数写在新线程中,两者会发生怎样的情况呢?
3. 对象的构造函数处在新线程时:
如下测试代码:
var mainThread = Thread.CurrentThread;
var autoResetEvent = new AutoResetEvent(false); //通过信号机制保证对象首先被创建
ThreadPool.QueueUserWorkItem((d) =>
test=new Heater();
autoResetEvent.Set();
autoResetEvent.WaitOne();
test.OnBoiled += (s, e) => Console.WriteLine(Thread.CurrentThread != mainThread ? "两者执行的是不同的线程" : "两者执行的是相同的线程");
test.Begin();
代码值得一提的是,为了保证对象被首先创建,采用了信号机制实现线程同步,当创建后,主线程才会往下执行,否则会抛出空引用的异常.
结果如下:
可见: 主线程称为Main, 若对象构造函数在B线程执行,事件不在主线程中执行。那是不是在B线程中执行呢?暂时还不知道。
4. 对象的事件订阅函数处在新线程时:
在另外一个线程里创建对象是更麻烦的,你要解决线程同步问题,恶心不,哈哈。
那么,若订阅事件的代码在线程B时,情况是怎样的呢?
var mainThread = Thread.CurrentThread;
ThreadPool.QueueUserWorkItem((d) =>
var bThread = Thread.CurrentThread;
test.OnBoiled += (s, e) =>
if(Thread.CurrentThread == mainThread )
Console.WriteLine("事件在主线程中执行");
else if (bThread==Thread.CurrentThread)
Console.WriteLine("事件在订阅事件的线程B中执行");
Console.WriteLine("事件在第三个线程中执行");
test.Begin();
说实话,我看到这个场景的时候大吃一惊,居然执行事件的代码不在主线程,不在订阅事件的线程,而在另外一个第三者线程!这可能就是线程池的无敌之处吧,它连事件订阅函数都给托管了!真是碉堡了!!
不过,管它是什么线程里执行,反正我主线程是不会被堵塞了,哈哈.
本来想今天把最后一个问题都解决的,可是时间实在太晚,而且文章已经够长了。不妨最后一个问题,“在复杂软件环境下,如何理性正确的使用委托和事件”放在第二部分吧。有些问题我也没搞清,在做实验的情况下,才逐渐接近结论。 写完这篇文章,我深有收获。
其实,按照惯例,应该把IL代码好好搞出来给大家看才算是“专业”的选择,不过我确实不懂IL,就不拿出来丢人了,高手们请自行脑补。
本文介绍了C#的委托和事件的订阅和取消订阅,并在匿名函数和多线程两个环境下讨论了一些问题。如果你觉得这篇文章对你有帮助,请点一下推荐,若有任何问题,欢迎留言讨论,共同学习。
测试代码见附件,请将不同Region的代码解开注释进行测试。