C#易失性读取行为

在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.

点赞