9

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

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 将其更改为 null,如果它不为 null,则不会更改 NewMail。换句话说,CompareExchange 根本不会更改 NewMail 中的值,但它会以原子、线程安全的方式返回 NewMail 中的值。杰弗里里希特 (2010-02-12)。通过 C# 进行 CLR(第 265 页)。OReilly Media - A. Kindle 版。

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

要么这本书有错误,要么我误解了它。有没有人实现过这个方法?或者有更好的方法来防止这里的竞争条件?

更新

这是我的错误,锁定代码有效。我只是指定了错误的铸造,但根据布拉德利(下),在 .net 2.0 及更高版本的 Windows 中没有必要。

4

4 回答 4

8

不允许编译器(或 JIT)对其进行优化if/temp(在 CLR 2.0 及更高版本中);CLR 2.0 内存模型不允许引入堆读取(规则 #2)。

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

有关这种情况的详细讨论,请参阅我的博客文章,并解释为什么标准模式很好。

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

关于您的问题Interlocked.CompareExchange,语法public event EventHandler<NewMailEventArgs> NewMail只是用于声明类型的私有字段EventHandler<NewMailEventArgs>和具有addremove方法的公共事件的 C# 语法糖。该Interlocked.CompareExchange调用读取私有EventHandler<NewMailEventArgs>字段的值,因此该代码确实可以按照 Richter 的描述进行编译和工作;这在 Microsoft CLR 中是不必要的。

于 2012-06-22T15:24:08.243 回答
4

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

问题是编译器可能会优化 if/temp ,

好吧,根据CLR 的 C# (p. 264–265)

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

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

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

不能引入读写。

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

 

然而,遵循更弱的内存模型的 Mono可以优化该局部变量。

底线:除非您打算使用 Mono,否则不要担心。

即便如此,这种行为也可以通过 volatile 声明来抑制。

于 2012-06-22T15:43:55.923 回答
1

我想你错过了解释。位置仅表示指向对象引用的指针 [ msdn 版本:与比较对象进行比较并可能被替换的目标对象。]。以下代码在 .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);
    }
}
于 2012-06-22T15:39:20.263 回答
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
于 2012-06-22T15:45:20.457 回答