以线程安全方式引发事件(修正)
《CLR via C#》3rd中提到,应该以线程安全的方式引发事件,不禁冒冷汗,一直以来还真没注意到这个问题,以前写的不少代码得重新审查修正了。下面是引用原文说明:
.Net Framework最初发布时,是建议开发者用以下方式引发事件:
protected virtual void OnNewMail(NewMailEventArgs e)
{
if (NewMail != null) NewMail(this, e);
}
这个OnNewMail方法的问题在于,线程可能发现NewMail不为null,然后,就在调用NewMail之前,另一个线程从委托链中移除了一个委托,是NewMail变成了null。这会造成抛出一个NullReferenceException异常。
于是我写了以下代码测试重现这种线程竞态的情况:
using System;
using System.Threading;
using System.Diagnostics;
namespace Neutra.Utils
{
class EventTest
{
public event EventHandler MyEvent;
static void Main(string[] args)
{
Console.WriteLine("Test1 start");
Test1();
Console.WriteLine("Test1 end");
Console.WriteLine("Test2 start");
Test2();
Console.WriteLine("Test2 end");
}
static void AddAndRemoveEventHandler(object obj)
{
var instance = obj as EventTest;
for (int i = 0; i < 100000; i++)
{
instance.MyEvent += HandleEvent;
Thread.Sleep(0);
instance.MyEvent -= HandleEvent;
Thread.Sleep(0);
}
}
static void HandleEvent(object sender, EventArgs e)
{
Console.Write('>');
}
static void Test1()
{
var sw = Stopwatch.StartNew();
var instance = new EventTest();
Thread thread = new Thread(AddAndRemoveEventHandler);
thread.Start(instance);
int i = 0;
try
{
for (i = 0; i < 2000; i++)
{
if (instance.MyEvent != null)
{
instance.MyEvent(instance, EventArgs.Empty);
}
Thread.Sleep(0);
}
Console.WriteLine();
}
catch (Exception exception)
{
sw.Stop();
Console.WriteLine();
Console.WriteLine("index = {0}, time: {1}", i, sw.Elapsed);
Console.WriteLine(exception);
}
finally
{
thread.Abort();
thread.Join();
}
}
static void Test2()
{
var sw = Stopwatch.StartNew();
var instance = new EventTest();
Thread thread = new Thread(AddAndRemoveEventHandler);
thread.Start(instance);
int i = 0;
try
{
for (i = 0; i < 2000; i++)
{
var handler=Interlocked.CompareExchange(ref instance.MyEvent, null, null);
if (handler != null)
{
handler(instance, EventArgs.Empty);
}
Thread.Sleep(0);
}
Console.WriteLine();
}
catch (Exception exception)
{
sw.Stop();
Console.WriteLine();
Console.WriteLine("index = {0}, time: {1}", i, sw.Elapsed);
Console.WriteLine(exception);
}
finally
{
thread.Abort();
thread.Join();
}
}
}
}
我测试了好几次,index最小的一次是60多,最大的1000多,并发问题还是比较明显的。下面是其中一次测试结果:
有些人倾向于使用EventHandler handler = instance.MyEvent;代替使用Interlocked.CompareExchange方法,书中也提到了,这种方式也是可行的,因为MS的JIT编译器不会将这里的handler优化掉。书中最后说道“另外由于事件主要在单线程的情形中使用(WinForm/WPF/SilverLight),所以线程安全并不是一个问题。”
我认为,这个问题还是有必要注意一下的。这种问题一般都很难重现,而且还是该死的NullReferenceException异常,一看上下文代码,霎时间还真是“莫名其妙”,最后归于人品问题倒是相当无奈了。
===============================================================
今天发现代码中有误,Interlocked.Exchange会交换两引用,应该使用Interlocked.CompareExchange方法。(上面代码已修正)