volatile和readonly应该互相排斥吗?

Dou*_*las 6 .net c# concurrency multithreading volatile

假设我正在设计一个包装内部集合的线程安全类:

public class ThreadSafeQueue<T>
{
    private readonly Queue<T> _queue = new Queue<T>();

    public void Enqueue(T item)
    {
        lock (_queue)
        {
            _queue.Enqueue(item);
        }
    }

    // ...
}
Run Code Online (Sandbox Code Playgroud)

基于我的另一个问题,上面的实现是错误的,因为当它的初始化与其使用同时执行时可能会出现种族危险:

ThreadSafeQueue<int> tsqueue = null;

Parallel.Invoke(
    () => tsqueue = new ThreadSafeQueue<int>(),
    () => tsqueue?.Enqueue(5));
Run Code Online (Sandbox Code Playgroud)

上面的代码是可接受的非确定性的:该项目可能会或可能不会入队.但是,在当前的实现中,它也被破坏了,并且可能引起不可预测的行为,例如抛出IndexOutOfRangeException,NullReferenceException多次将相同的项目入队,或者陷入无限循环.这是因为Enqueue调用可能将新实例分配给局部变量tsqueue之后但内部_queue字段的初始化完成(或似乎完成)之前运行.

Per Jon Skeet:

在将对新对象的引用分配给实例之前,Java内存模型不能确保构造函数完成.Java内存模型经历了1.5版的重新加工,但是在没有volatile变量的情况下,双重检查锁定仍然被破坏(如在C#中).

可以通过向构造函数添加内存屏障来解决此种族危险:

    public ThreadSafeQueue()
    {
        Thread.MemoryBarrier();
    }
Run Code Online (Sandbox Code Playgroud)

同样,通过使字段变化可以更简洁地解决:

    private volatile readonly Queue<T> _queue = new Queue<T>();
Run Code Online (Sandbox Code Playgroud)

但是,C#编译器禁止后者:

'Program.ThreadSafeQueue<T>._queue': a field cannot be both volatile and readonly
Run Code Online (Sandbox Code Playgroud)

鉴于上述内容似乎是一个合理的用例volatile readonly,这种限制是否是语言设计中的一个缺陷?

我知道可以简单地删除它readonly,因为它不会影响类的公共接口.然而,这是不重要的,因为readonly一般来说可以这样说.我也知道现存的问题" 为什么readonly和volatile修饰符是互斥的?"; 然而,这解决了另一个问题.

具体方案:此问题似乎会影响System.Collections.Concurrent.NET Framework类库本身的命名空间中的代码.该ConcurrentQueue<T>.Segment嵌套类有那些永远只能在构造函数中分配的几个领域:m_array,m_state,m_index,和m_source.其中,仅m_index被宣布为只读; 其他人不能 - 尽管他们应该 - 因为他们需要被声明为挥发性以满足线程安全的要求.

private class Segment
{
    internal volatile T[] m_array;                  // should be readonly too
    internal volatile VolatileBool[] m_state;       // should be readonly too
    private volatile Segment m_next;
    internal readonly long m_index; 
    private volatile int m_low;
    private volatile int m_high; 
    private volatile ConcurrentQueue<T> m_source;   // should be readonly too

    internal Segment(long index, ConcurrentQueue<T> source)
    {
        m_array = new T[SEGMENT_SIZE];              // field only assigned here
        m_state = new VolatileBool[SEGMENT_SIZE];   // field only assigned here
        m_high = -1;
        m_index = index;                            // field only assigned here
        m_source = source;                          // field only assigned here
    }

    internal void Grow()
    {
        // m_index and m_source need to be volatile since race hazards
        // may otherwise arise if this method is called before
        // initialization completes (or appears to complete)
        Segment newSegment = new Segment(m_index + 1, m_source);
        m_next = newSegment;
        m_source.m_tail = m_next;
    }

    // ...
}
Run Code Online (Sandbox Code Playgroud)

usr*_*usr 4

readonly字段可以从构造函数的主体中完全写入。事实上,人们可以使用volatile对字段的访问readonly来引起内存屏障。我认为你的例子是这样做的一个很好的例子(并且它被语言阻止了)。

确实,在构造函数完成后,在构造函数内部进行的写入可能对其他线程不可见。它们甚至可能以任何顺序变得可见。这一点并不为人所知,因为它很少在实践中发挥作用。构造函数的末尾不是内存屏障(通常凭直觉假设)。

您可以使用以下解决方法:

class Program
{
    readonly int x;

    public Program()
    {
        Volatile.Write(ref x, 1);
    }
}
Run Code Online (Sandbox Code Playgroud)

我测试过这个可以编译。我不确定是否允许形成一个ref字段readonly,但确实如此。

为什么语言会阻止readonly volatile?我最好的猜测是,这是为了防止您犯错误。大多数时候,这是一个错误。这就像await在 of 内部使用lock:有时这是完全安全的,但大多数时候并非如此。

也许这应该是一个警告。

Volatile.Write在 C# 1.0 时还不存在,因此将其作为 1.0 警告的理由更有力。现在有了解决方法,这种错误的可能性就很大了。

我不知道CLR是否不允许readonly volatile。如果是的话,那可能是另一个原因。CLR 的风格允许大多数合理的操作得以实现。C# 的限制比 CLR 严格得多。所以我非常确定(无需检查)CLR 允许这样做。