线程安全事件调用

时间:2012-06-22 15:19:26

标签: c# multithreading .net-4.0

触发事件时避免竞争条件(在多线程应用程序中)的常见做法是:

EventHandler<EventArgs> temp = SomeEvent;
if (temp != null) temp(e);

"Remember that delegates are immutable and this is why this technique works in theory. However, what a lot of developers don't realize is that this code could be optimized by the compiler to remove the local temp variable entirely. If this happens, this version of the code is identical to the first version, so a NullReferenceException is still possible."

问题(根据书中)是“编译器可以优化此代码以完全删除本地临时变量。如果发生这种情况,此版本的代码与第一个版本相同,因此NullReferenceException是仍然可能“

根据CLR通过C#,这是强制编译器复制事件指针的更好方法。

virtual void OnNewMail(NewMailEventArgs e)
{
    EventHandler<NewMailEventArgs> temp =
                          Interlocked.CompareExchange(ref NewMail, null, null);
    if (temp != null) 
        temp(this, e);
}
  

此处,如果NewMail引用为null,则CompareExchange将NewMail引用更改为null,如果不为null,则不会更改NewMail。换句话说,CompareExchange根本不会更改NewMail中的值,但它确实以原子,线程安全的方式返回NewMail中的值。       里希特,杰弗里(2010-02-12)。 CLR通过C#(第265页)。 OReilly Media - A. Kindle版。

我在.Net 4.0框架上,并且不确定这可能如何工作,因为Interlocked.CompareExchange需要对位置的引用,而不是对事件的引用。

书中有错误,或者我误解了它。有没有人实现过这种方法?或者有更好的方法来预防这里的竞争条件?

更新

这是我的错误,iterlocked代码工作。我刚刚指定了错误的转换,但根据Bradley(下文),在.net 2.0和Windows上没有必要。

4 个答案:

答案 0 :(得分:8)

不允许编译器(或JIT)优化if/temp(在CLR 2.0及更高版本中); CLR 2.0 Memory Model不允许引入堆读取(规则#2)。

因此,第二次无法读取MyEvent;必须在temp语句中读取if的值。

有关此情况的详细讨论,请参阅my blog post,并解释为何标准模式正常。

但是,如果您运行的是不提供CLR 2.0内存模型保证的非Microsoft CLR(例如单声道)(但仅遵循ECMA内存模型),或者您在Itanium上运行(有一个众所周知的弱硬件内存模型),你需要像Richter这样的代码来消除潜在的竞争条件。

关于Interlocked.CompareExchange的问题,语法public event EventHandler<NewMailEventArgs> NewMail只是C#语法糖,用于声明类型为EventHandler<NewMailEventArgs>的私有字段和具有add的公共事件和remove方法。 Interlocked.CompareExchange调用读取私有EventHandler<NewMailEventArgs>字段的值,因此此代码的编译和工作正如Richter所描述的那样;它在Microsoft CLR中是不必要的。

答案 1 :(得分:4)

现在这只是你问题的部分答案,因为我无法评论使用Interlocked.CompareExchange,但我认为这些信息可能有用。

  

问题是编译器可以优化if / temp away,

嗯,根据 CLR通过C#(第264-265页)

  

编译器可以优化代码以完全删除本地变量。如果发生这种情况,此版本的代码与[引用事件两次的版本]相同,因此仍然可以使用NullReferenceException。

所以, 可能 ,但是,重要的是要知道Microsoft的即时(JIT)编译器 优化掉局部变量。虽然这可能会改变,但不太可能,因为它可能会破坏很多应用程序。

这是因为.Net有一个强大的内存模型:http://msdn.microsoft.com/en-us/magazine/cc163715.aspx#S5

  

无法进行读写操作。

  

然而,该模型不允许引入读取,因为这意味着从内存中重新获取值,而低锁代码内存可能会发生变化。

然而,跟随much weaker memory model的Mono可以优化该局部变量。

结论:除非你打算使用Mono,否则不用担心。

即便如此,使用volatile声明也可以抑制这种行为。

答案 2 :(得分:1)

我认为你错过了解释。位置仅指向对象引用的指针[ msdn version:与comparand进行比较并可能被替换的目标对象。]。以下代码在.NEt 4.0中正常工作

public class publisher
{

    public event EventHandler<EventArgs> TestEvent;
    protected virtual void OnTestEvent(EventArgs e)
    {
        EventHandler<EventArgs> temp = Interlocked.CompareExchange(ref TestEvent, null, null);
        if (temp != null)
            temp(this,e);
    }
}

答案 3 :(得分:0)

我看看你产生的IL你会看到这个方法就像这样调用

IL_000d:  ldsflda    class [mscorlib]System.EventHandler`1<class [mscorlib]System.EventArgs> ConsoleApplication1.Program::MyEvent
IL_0012:  ldnull
IL_0013:  ldnull
IL_0014:  call       !!0 [mscorlib]System.Threading.Interlocked::CompareExchange<class [mscorlib]System.EventHandler`1<class [mscorlib]System.EventArgs>>(!!0&,!!0,!!0)

看到ldsflda - 我的活动是静态的,但它正在加载字段的地址。该字段是编译器为每个事件生成的自动生成的委托字段。

该字段的定义如下:

.field private static class [mscorlib]System.EventHandler`1<class [mscorlib]System.EventArgs> MyEvent