【发布时间】:2016-08-17 18:44:57
【问题描述】:
假设我正在设计一个包装内部集合的线程安全类:
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 字段的初始化之前完成(或似乎完成)。
Java 内存模型不能确保构造函数在对新对象的引用分配给实例之前完成。 Java 内存模型在 1.5 版中进行了重新设计,但在没有 volatile 变量(如在 C# 中)的情况下,双重检查锁定仍然被破坏。
可以通过向构造函数添加内存屏障来解决这种竞争风险:
public ThreadSafeQueue()
{
Thread.MemoryBarrier();
}
等效地,可以通过使字段 volatile 更简洁地解决它:
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,因为它不会影响类的公共接口。但是,这无关紧要,因为通常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;
}
// ...
}
【问题讨论】:
-
这个问题怎么可能不是基于意见的?除非您希望 Eric Lippert 插话,否则我看不出其他人如何回答这个问题,而不仅仅是猜测或意见?
-
认为 Eric Lippert 可能会加入并不是没有道理的。如果他还记得有关 C# 的任何事情 ;)
-
这个问题不是推测性的或基于意见的。我要求熟悉 C# 编译器和 .NET 内存模型的人回答上述问题是否真的是语言疏忽,或者我所遗漏的互斥背后是否有原因。诚然,大多数人都没有这些知识,但这并没有超出公共知识的范畴,尤其是在 .NET 最近开源的情况下。除非您的意思是只应在 StackOverflow 上发布琐碎的问题。
-
@Douglas 如果您对 Microsoft 有任何疑问,请询问 Microsoft,不要随便问街上的人。既然你知道这里没有人能回答这个问题,为什么要在这里问这个问题?是的,这个问题是基于意见的。您是否认为这是一个“缺陷”只是一个意见问题,而不是事实。
-
具体场景的更新似乎不太具体。它没有说明它“似乎如何影响代码”。或许我们可以举个例子?
标签: c# .net multithreading concurrency volatile