易变性和只读是否相互排斥?
假设我正在设计一个包装内部集合的线程安全类:
public class ThreadSafeQueue { private readonly Queue _queue = new Queue(); public void Enqueue(T item) { lock (_queue) { _queue.Enqueue(item); } } // ... }
基于我的另一个问题 ,上面的实现是错误的,因为当它的初始化与其使用同时执行时可能会出现种族危险:
ThreadSafeQueue tsqueue = null; Parallel.Invoke( () => tsqueue = new ThreadSafeQueue(), () => tsqueue?.Enqueue(5));
上面的代码是可接受的非确定性的:该项目可能会或可能不会入队。 但是,在当前的实现中,它也被破坏了,并且可能引起不可预测的行为,例如抛出IndexOutOfRangeException
, NullReferenceException
,多次排队同一项,或者陷入无限循环。 这是因为Enqueue
调用可能在将新实例分配给局部变量tsqueue
之后但在内部_queue
字段的初始化完成(或似乎完成)之前运行。
Per Jon Skeet :
Java内存模型不能确保构造函数在将新对象的引用分配给实例之前完成 。 Java内存模型经历了1.5版的重新加工,但是在没有volatile变量的情况下,双重检查锁定仍然被破坏( 如在C#中 )。
可以通过向构造函数添加内存屏障来解决此种族危险:
public ThreadSafeQueue() { Thread.MemoryBarrier(); }
同样,通过使字段变化可以更简洁地解决:
private volatile readonly Queue _queue = new Queue();
但是,后者被C#编译器禁止:
'Program.ThreadSafeQueue._queue': a field cannot be both volatile and readonly
鉴于上述似乎是volatile readonly
的合理用例,这种限制是否是语言设计中的一个缺陷?
我知道可以简单地删除readonly
,因为它不会影响类的公共接口。 然而,这是不重要的,因为一般来说,只能readonly
相同的内容。 我也知道现有的问题“ 为什么只读和易变的修饰符互相排斥? “; 然而,这解决了另一个问题。
具体方案 :此问题似乎会影响.NET Framework类库本身的System.Collections.Concurrent
命名空间中的代码。 ConcurrentQueue.Segment
嵌套类有几个只在构造函数中分配的字段: m_array
, m_index
, m_source
和m_source
。 其中,只有m_index
被声明为readonly; 其他人不能 – 尽管他们应该 – 因为他们需要被声明为易变的以满足线程安全的要求。
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 m_source; // should be readonly too internal Segment(long index, ConcurrentQueue 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
字段可以从构造函数的主体完全写入。 实际上,可以使用对readonly
字段的volatile
访问来引起存储器屏障。 我认为,你的情况是这样做的一个很好的例子(它被语言所阻止)。
确实,在ctor完成之后,在构造函数内部进行的写入可能对其他线程不可见。 它们甚至可以以任何顺序显示出来。 这并不为人所知,因为它在实践中很少发挥作用。 构造函数的结尾不是内存障碍(通常从直觉中假设)。
您可以使用以下解决方法:
class Program { readonly int x; public Program() { Volatile.Write(ref x, 1); } }
我测试了这个编译。 我不确定它是否被允许形成一个readonly
字段的ref
,但确实如此。
为什么语言会阻止readonly volatile
? 我最好的猜测是,这是为了防止你犯错误。 大多数时候这都是错误的。 就像在lock
内使用await
一样:有时这是非常安全的,但大部分时间都不是。
也许这应该是一个警告。
Volatile.Write
在C#1.0时不存在,因此将其作为1.0的警告的情况更强。 现在有一个解决方法,因为这个错误很强烈。
我不知道CLR是否禁止readonly volatile
。 如果是,那可能是另一个原因。 CLR具有允许大多数合理实施的操作的风格。 C#比CLR更具限制性。 所以我很确定(没有检查)CLR允许这样做。
ThreadSafeQueue
tsqueue = null; Parallel.Invoke( () => tsqueue = new ThreadSafeQueue (), () => tsqueue?.Enqueue(5));
在您的示例中,问题是tsqueue
以非线程安全的方式发布。 在这种情况下,绝对有可能在ARM等体系结构上获得部分构造的对象。 因此,将tsqueue
标记为volatile
或使用Volatile.Write
方法赋值。
此问题似乎会影响.NET Framework类库本身的System.Collections.Concurrent命名空间中的代码。 ConcurrentQueue.Segment嵌套类具有几个仅在构造函数中分配的字段:m_array,m_state,m_index和m_source。 其中,只有m_index被声明为readonly; 其他人不能 – 尽管他们应该 – 因为他们需要被声明为易变的以满足线程安全的要求。
将字段标记为readonly
只会添加编译器检查的一些约束,并且JIT稍后可能会用于优化(但JIT足够聪明,即使在某些情况下没有该关键字,该字段也是readonly
)。 但是由于并发volatile
将这些特定字段标记为volatile
更为重要。 private
和internal
字段由该库的作者控制,所以在那里省略readonly
是绝对可以的。
首先,它似乎是语言的限制,而不是平台:
.field private initonly class SomeTypeDescription modreq ([mscorlib]System.Runtime.CompilerServices.IsVolatile) SomeFieldName
编译好了,我找不到任何引用声明initonly(readonly)不能与modreq ([mscorlib]System.Runtime.CompilerServices.IsVolatile)
( volatile
)配对。
据我所知,所描述的情况可能来自低级指令交换。 构造对象并将其放入字段的代码如下所示:
newobj instance void SomeClassDescription::.ctor() stfld SomeFieldDescription
正如ECMA所述:
newobj指令分配与ctor关联的类的新实例,并将新实例中的所有字段初始化为0(适当类型)或适当时为null。 然后,它使用给定的参数和新创建的实例调用构造函数。 调用构造函数后,现在初始化的对象引用将被压入堆栈。
所以,据我所知,直到指令没有被交换(这是因为返回创建的对象的地址并填充此对象是存储到不同的位置),你总是看到完全初始化的对象或从读取时的null另一个线程。 使用volatile可以保证这一点。 它会阻止交换:
newobj volatile. stfld
Ps它本身不是答案。 我不知道为什么C#禁止readonly volatile
。