以线程安全方式引发事件(修正)

  《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方法。(上面代码已修正)

原文地址:https://www.cnblogs.com/neutra/p/1849721.html