C# 运行时或处理器能否对.NET中的后续写入进行重新排序?

C# 运行时或处理器能否对.NET中的后续写入进行重新排序?,c#,thread-safety,memory-model,C#,Thread Safety,Memory Model,我有不可变的对象,我希望惰性地计算它们的哈希代码。我已经实现了 private bool _HasHashCode = false; private int _HashCode; public override int GetHashCode() { if (_HasHashCode) return _HashCode; long hashCode; unchecked { hashCode = Digits; h

我有不可变的对象,我希望惰性地计算它们的哈希代码。我已经实现了

private bool _HasHashCode = false;
private int _HashCode;
public override int GetHashCode()
{
    if (_HasHashCode)
        return _HashCode;

    long hashCode;
    unchecked
    {
        hashCode = Digits;
        hashCode = (hashCode*397) ^ XI;
        hashCode = (hashCode*397) ^ YI;
        hashCode = (int) ( hashCode % Int32.MaxValue);
    }

    // is it possible that these two write instructions
    // get reordered on a certain .NET/CPU architecture 
    // combination:

    _HashCode = (int)hashCode;
    _HasHashCode = true;

    return _HashCode;
}
我的推理是,32位的HashCode成员是32位的,对它的写入是原子的,因此,即使由于设置\u HashCode属性的竞争条件而运行了两次计算,也无所谓,因为每次都会计算相同的值


我担心CLR可能会重新排序写入\u HashCode\u HashCode。这是一个问题还是我可以确保CLR不会对写入进行重新排序?

不,它不是线程安全的,因为您提到了一个问题:JIT编译器可以对写入进行重新排序

这在(前几段)中得到了证实。(也是文章的重点。)

解决方案是不要使用
volatile
。相反,您应该按如下方式使用:

_HashCode = (int)hashCode;
Thread.MemoryBarrier(); // Prevents reordering of the statements before and after.
_HasHashCode = true;
MemoryBarrier
正好具有此代码所需的语义

但是,请注意,根据microsoft的说法:

MemoryBarrier仅在具有弱 内存排序(例如,采用多个Intel的系统 安腾处理器)

此外,我并不完全相信这样做比缓存构造函数中的哈希代码更快(从而从
GetHashCode()
实现中删除所有逻辑)


我当然会对这两种方法进行一些仔细的计时以确保安全。

编辑:@Groo将我的注意力集中在通过底层框架(CLR可以做到这一点)或操作系统对指令重新排序上。我相信
lock
块可以防止这种情况发生,并且根据它们可以防止
指令重新排序。另一个来源是一个声明“
Monitor.Enter
Monitor.Exit
都会生成完整的围栏”

它不是线程安全的;但首先,我的主张是:

private bool _HasHashCode = false;
private int _HashCode;
private readonly object _lock = new object();

public override int GetHashCode()
{
    if (_HasHashCode)
        return _HashCode;

    lock (_lock)
    {
        if (_HasHashCode)
            return _HashCode;

        long hashCode;
        unchecked
        {
            hashCode = Digits;
            hashCode = (hashCode*397) ^ XI;
            hashCode = (hashCode*397) ^ YI;
            hashCode = (int) (hashCode%Int32.MaxValue);
        }

        _HashCode = (int) hashCode;
        _HasHashCode = true;
        return _HashCode;
    }
}
在并行/异步编程中,我经常遇到的一个问题是“这项工作已经完成了吗?”。这段代码处理这个问题
lock
语句非常快,只会命中几次(哈希代码不会被重新计算!)。哈希代码将仅在第一个锁处计算。下面的内容(如果您一次又一次地快速创建此对象)将显示
\u HasHashCode
为true并返回它

好的部分是,除了最初创建的一些初始对象之外;没有一个迟到的人会撞到锁!因此,
lock
块最多只命中几次


注意:我回答得很匆忙。我应该问:如果这个对象是不可变的,为什么不在构建时计算散列呢?:)

为了补充其他答案,下面的表格显示了不同体系结构上可能的重新排序:

(信用证:)

关于英特尔体系结构和OP的问题,它表明:

  • 存储不能与x86上的其他存储重新排序(包括IA-32和Intel64,或者Intel的x86-64实现,不要与IA-64/Itanium混淆)

  • 但是存储可以与IA-64(安腾)处理器上的其他存储一起重新排序

另一方面,.NET(自2.0以来)应确保不会发生无序写入(即使在此类体系结构上):

在.NET(…)上,这种代码移动和处理器重新排序是不合法的。这个具体的例子是我们对.NETFramework2.0在CLR中实现的内存模型进行强化更改的主要动机写入总是按顺序退出。为了禁止无序写入,CLR的JIT编译器会在适当的体系结构上发出适当的指令(即,在这种情况下,确保IA64上的所有写入都是存储/释放的)

还解释了:

strong模型2:.NET Framework 2.0

此模型的规则(在.NET 2.0中介绍)为:

  • ECMA模型中包含的所有规则,特别是三个基本内存模型规则以及易失性的ECMA规则
  • 不能引入读取和写入
  • 只有当一个读取与来自同一线程的同一位置的另一个读取相邻时,才能删除该读取。只有当一个写入与来自同一线程的同一位置的另一个写入相邻时,才能删除该写入。规则5可用于在应用此规则之前使读或写相邻
  • 写入操作不能超过同一线程中的其他写入操作。
  • 读取只能在时间上提前移动,但决不能超过从同一线程到同一内存位置的写入
考虑到Microsoft最近在Windows Server和Visual Studio中的应用,从现在起基本上只能针对x86/x64,因为它们具有上面提到的更严格的内存模型,不允许无序写入


当然,由于微软的.NET(Mono)存在不同的实现,像这样的说法应该保留。

这里有一种懒惰的方法:避免问题,避免问题。例如,只有当存在两个“事物”时,重新排序才是一个问题——一个“事物”永远不会出现故障。您可以牺牲哨兵值
0
来表示“尚未计算”-然后作为计算的最后一步,避免哨兵:

int hash;
public override int GetHashCode()
{
    var snapshot = hash;
    if(snapshot == 0) // means: not yet calculated
    {
        // snapshot = ... your actual implementation

        if(snapshot == 0) snapshot = -124987; // avoid sentinel value
        hash = snapshot;
    }
    return snapshot;
}

请注意,
int
读取和写入保证是原子的,这也有帮助。

我对这类事情的了解不是很好,但我建议您在两个私有变量上加上“volatile”。只需使用0作为特殊值,它永远不会出现在哈希代码中-因此您可以只有一个变量。。。旁注:如果您以后可以更新hashcode,那么您的对象并不是真正不可变的。计算时间