volatile和readonly应该互相排斥吗?

时间:2016-08-17 18:44:57

标签: c# .net multithreading concurrency volatile

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

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,多次排队同一个项目,或陷入无限循环。这是因为{/ 1}}调用可能在新实例已分配给本地变量Enqueue后运行,但之前内部初始化tsqueue字段完成(或似乎完成)。

Jon Skeet

  

在将对新对象的引用分配给实例之前,Java内存模型不能确保构造函数完成。 Java内存模型经历了1.5版的重写,但是在没有volatile变量的情况下,双重检查锁定仍然被破坏(在C#中)。

可以通过向构造函数添加内存屏障来解决此种族危险:

_queue

同样地,可以通过使字段变为更简洁来解决:

    public ThreadSafeQueue()
    {
        Thread.MemoryBarrier();
    }

但是,C#编译器禁止后者:

    private volatile readonly Queue<T> _queue = new Queue<T>();

鉴于上述内容似乎是'Program.ThreadSafeQueue<T>._queue': a field cannot be both volatile and readonly 的合理用例,这种限制是否是语言设计中的一个缺陷?

我知道可以简单地删除volatile readonly,因为它不会影响该类的公共接口。然而,这不是重点,因为readonly一般来说也是如此。我也知道现有的问题“Why readonly and volatile modifiers are mutually exclusive?”;然而,这解决了另一个问题。

具体方案:此问题似乎会影响.NET Framework类库本身的readonly命名空间中的代码。 ConcurrentQueue<T>.Segment嵌套类具有几个仅在构造函数中分配的字段:System.Collections.Concurrentm_arraym_statem_index。其中,只有m_source被声明为只读;其他人不可能 - 虽然他们应该 - 因为他们需要被宣布为挥发性的,以满足线程安全的要求。

m_index

3 个答案:

答案 0 :(得分:4)

readonly字段可以从构造函数的主体完全写入。实际上,可以使用volatilereadonly字段的访问来引起内存屏障。我认为,你的情况是这样做的一个好例子(并且它被语言所阻止)。

在ctor完成后,在构造函数内部进行的写入可能对其他线程不可见。它们甚至可以以任何顺序显示出来。这并不为人所知,因为它在实践中很少发挥作用。构造函数的结尾是一个内存障碍(通常从直觉中假设)。

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

class Program
{
    readonly int x;

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

我测试过这个编译。我不确定是否允许refreadonly字段,但确实如此。

为什么语言会阻止readonly volatile?我最好的猜测是,这是为了防止你犯错误。大多数时候这都是错误的。这就像在await中使用lock一样:有时候这是非常安全的,但大部分时间都没有。

也许这应该是一个警告。

在C#1.0时,

Volatile.Write不存在,因此将此警告为1.0的情况更强。现在有一个解决方法就是这个错误很强烈。

我不知道CLR是否禁止readonly volatile。如果是,那可能是另一个原因。 CLR具有允许大多数合理实施的操作的风格。 C#比CLR更具限制性。所以我非常肯定(没有检查)CLR允许这样做。

答案 1 :(得分:1)

ThreadSafeQueue<int> tsqueue = null;

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

在您的示例中,问题是tsqueue以非线程安全方式发布。在这种情况下,在ARM这样的架构上获得部分构造的对象是绝对可能的。因此,将tsqueue标记为volatile或使用Volatile.Write方法指定值。

  

此问题似乎会影响System.Collections.Concurrent中的代码   .NET Framework类库本身的命名空间。该   ConcurrentQueue.Segment嵌套类有几个字段   只在构造函数中分配:m_array,m_state,m_index,   和m_source。其中,只有m_index被声明为readonly;该   其他人不可能 - 尽管他们应该 - 因为他们需要   声明为易变,以满足线程安全的要求。

将字段标记为readonly只会添加编译器检查的一些约束,JIT以后可能会用于优化(但JIT足够聪明,即使在某些情况下没有该关键字,该字段也是readonly )。但是由于并发性,标记这些特定字段volatile更为重要。 privateinternal字段受该图书馆作者的控制,因此可以在那里省略readonly

答案 2 :(得分:0)

首先,它似乎是语言所施加的限制,而不是平台:

.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

P.S。它本身不是答案。我不知道为什么C#禁止readonly volatile