在C#.net ConcurrentDictionary(
C# reference source)的参考源代码中,我不明白为什么在以下代码片段中需要进行易失性读取:
public bool TryGetValue(TKey key, out TValue value)
{
if (key == null) throw new ArgumentNullException("key");
int bucketNo, lockNoUnused;
// We must capture the m_buckets field in a local variable.
It is set to a new table on each table resize.
Tables tables = m_tables;
IEqualityComparer<TKey> comparer = tables.m_comparer;
GetBucketAndLockNo(comparer.GetHashCode(key),
out bucketNo,
out lockNoUnused,
tables.m_buckets.Length,
tables.m_locks.Length);
// We can get away w/out a lock here.
// The Volatile.Read ensures that the load of the fields of 'n'
//doesn't move before the load from buckets[i].
Node n = Volatile.Read<Node>(ref tables.m_buckets[bucketNo]);
while (n != null)
{
if (comparer.Equals(n.m_key, key))
{
value = n.m_value;
return true;
}
n = n.m_next;
}
value = default(TValue);
return false;
}
评论:
// We can get away w/out a lock here.
// The Volatile.Read ensures that the load of the fields of 'n'
//doesn't move before the load from buckets[i].
Node n = Volatile.Read<Node>(ref tables.m_buckets[bucketNo]);
我有点困惑.
在从数组中读取变量n之前,CPU如何读取n的字段?
最佳答案 易失性读取具有获取语义,这意味着它先于其他存储器访问.
如果它不是用于易失性读取,那么下一次从我们刚刚得到的节点读取的字段可以由JIT编译器或体系结构推测地重新排序到读取节点本身之前.
如果这没有意义,想象一下JIT编译器或体系结构读取将分配给n的任何值,并开始speculatively read n.m_key,这样如果n!= null,则没有mispredicted branch,没有pipeline bubble或更糟,pipeline flushing .
这可能是when the result of an instruction can be used as an operand for the next instruction(s),但还在筹备中.
对于易失性读取或具有类似获取语义的操作(例如,输入锁定),C#规范和CLI规范都说它必须在任何进一步的存储器访问之前发生,因此不可能获得未初始化的n.m_key.
也就是说,如果写操作也是易失性的或由具有类似释放语义的操作(例如退出锁定)保护.
如果没有volatile语义,这种推测性读取可能会返回n.m_key的未初始化值.
同样重要的是比较器执行的内存访问.如果节点的对象是在没有易失性版本的情况下初始化的,那么您可能正在阅读陈旧的,可能是未初始化的数据.
这里需要Volatile.Read,因为C#本身无法在数组元素上表达易失性读取.读取m_next字段时不需要它,因为它声明为volatile.