在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的字段?
答案 0 :(得分:2)
易失性读取具有获取语义,这意味着它先于其他内存访问。
如果它不是易失性读取,那么我们刚刚得到的Node
字段的下一次读取可以由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
的未初始化值。
同样重要的是由comparer
执行的内存访问。如果节点的对象在没有易失性版本的情况下进行了初始化,那么您可能正在阅读陈旧的,可能是未初始化的数据。
Volatile.Read
,因为C#本身无法在数组元素上表达易失性读取。在阅读m_next
字段时,不需要它,因为它已声明volatile
。
答案 1 :(得分:0)
非常感谢你提出一个非常好的问题!
简短的回答:评论是错误的(或非常令人困惑)
更长的回答:至少有two bugs in ConcurrentDictionary。
Volatile.Read<Node>(ref tables._buckets[bucketNo])
和Volatile.Read<Node>(ref buckets[i])
是为了保护我们免于阅读可能被另一个线程更改的引用,因此我们有一个引用副本指向Node
的具体实例。
错误是即使使用这些Volatile.Read
(因为它不是出于评论的原因),如果我们使用以下代码分配引用,我们可以观察到类型为Node
的部分构造的对象:{{ 1}}。