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)
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 允许这样做。
| 归档时间: |
|
| 查看次数: |
449 次 |
| 最近记录: |