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

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

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

    // ...
}

基于my other question,上述实现存在错误,因为在与使用同时进行初始化时可能会引起比赛危险:
ThreadSafeQueue<int> tsqueue = null;

Parallel.Invoke(
    () => tsqueue = new ThreadSafeQueue<int>(),
    () => tsqueue?.Enqueue(5));

上面的代码是不确定的,可以接受:该项目可能会入队,也可能不会入队。但是,在当前的实现下,它也被破坏了,并且可能引起不可预测的行为,例如,抛出IndexOutOfRangeExceptionNullReferenceException,多次使同一项目入队或陷入无限循环。发生这种情况是因为Enqueue调用可能在将新实例分配给本地变量tsqueue之后但在内部_queue字段的初始化完成(或似乎完成)之前运行。

Jon Skeet:



可以通过向构造函数添加内存屏障来解决这种种族危险:
    public ThreadSafeQueue()
    {
        Thread.MemoryBarrier();
    }

同样,可以通过使字段可变为更简洁的解决方法:
    private volatile readonly Queue<T> _queue = new Queue<T>();

但是,C#编译器禁止后者:
'Program.ThreadSafeQueue<T>._queue': a field cannot be both volatile and readonly

鉴于以上内容似乎是volatile readonly的合理用例,那么此限制是否是语言设计中的缺陷?

我知道一个人可以简单地删除readonly,因为它不会影响该类的公共(public)接口(interface)。但是,这很重要,因为一般来说readonly可以这样说。我也知道现有的问题“Why readonly and volatile modifiers are mutually exclusive?”;但是,这解决了一个不同的问题。

具体方案:此问题似乎影响.NET Framework类库本身的System.Collections.Concurrent命名空间中的代码。 ConcurrentQueue<T>.Segment 嵌套类具有几个仅在构造函数内分配的字段:m_arraym_statem_indexm_source。其中,只有m_index声明为只读;其他线程不能(尽管应该如此)是因为必须将它们声明为 volatile 才能满足线程安全性的要求。
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;
    }

    // ...
}

最佳答案

readonly字段可以从构造函数的主体中完全写入。确实,可以使用对volatile字段的readonly访问来引起内存障碍。我认为您的案例是这样做的一个很好的案例(而且语言也阻止了这种情况)。

的确,在ctor完成之后,其他线程可能看不到在构造函数内部进行的写入。它们甚至可能以任何顺序变为可见。这不是众所周知的,因为在实践中很少发挥作用。构造函数的末尾不是内存障碍(通常根据直觉来假设)。

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

class Program
{
    readonly int x;

    public Program()
    {
        Volatile.Write(ref x, 1);
    }
}

我测试了这个编译。我不确定是否允许将ref形成为readonly字段,但是确实如此。

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

也许这应该是一个警告。

在C#1.0时Volatile.Write不存在,因此针对1.0发出警告的情况会更强。现在有一种解决方法,这种情况很容易出错。

我不知道CLR是否禁止readonly volatile。如果是,那可能是另一个原因。 CLR具有允许大多数合理实现的操作的样式。 C#比CLR的限制要严格得多。因此,我非常确定(未经检查)CLR允许这样做。

关于c# - volatile和readonly应该互斥吗?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/39004125/

10-12 22:14