《CLR via C#》3rd中提到,应该以线程安全的方式引发事件,不禁冒冷汗,一直以来还真没注意到这个问题,以前写的不少代码得重新审查修正了。下面是引用原文说明:
.Net Framework最初发布时,是建议开发者用以下方式引发事件:
{
if (NewMail != null ) NewMail( this , e);
}
这个OnNewMail方法的问题在于,线程可能发现NewMail不为null,然后,就在调用NewMail之前,另一个线程从委托链中移除了一个委托,是NewMail变成了null。这会造成抛出一个NullReferenceException异常。
于是我写了以下代码测试重现这种线程竞态的情况:
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方法。(上面代码已修正)