C#易失性读取行为

时间:2016-05-17 11:54:13

标签: c# volatile memory-model

在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的字段?

2 个答案:

答案 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}}。