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,那么您的对象并不是真正不可变的。计算时间