假设我正在设计一个包装内部集合的线程安全类:
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));
上面的代码是不确定的,可以接受:该项目可能会入队,也可能不会入队。但是,在当前的实现下,它也被破坏了,并且可能引起不可预测的行为,例如,抛出
IndexOutOfRangeException
,NullReferenceException
,多次使同一项目入队或陷入无限循环。发生这种情况是因为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_array
,m_state
,m_index
和m_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/