Should volatile and readonly be mutually exclusive?

岁酱吖の 提交于 2019-12-07 02:22:35

问题


Suppose that I were designing a thread-safe class that wraps an internal collection:

public class ThreadSafeQueue<T>
{
    private readonly Queue<T> _queue = new Queue<T>();

    public void Enqueue(T item)
    {
        lock (_queue)
        {
            _queue.Enqueue(item);
        }
    }

    // ...
}

Based on my other question, the above implementation is buggy, as race hazards may arise when its initialization is performed concurrently with its usage:

ThreadSafeQueue<int> tsqueue = null;

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

The code above is acceptably non-deterministic: the item may or may not be enqueued. However, under the current implementation, it is also broken, and can give rise to unpredictable behaviour, such as throwing IndexOutOfRangeException, NullReferenceException, enqueueing the same item multiple times, or getting stuck in an infinite loop. This occurs since the Enqueue call might run after the new instance has been assigned to the local variable tsqueue, but before the initialization of the internal _queue field completes (or appears to complete).

Per Jon Skeet:

The Java memory model doesn't ensure that the constructor completes before the reference to the new object is assigned to instance. The Java memory model underwent a reworking for version 1.5, but double-check locking is still broken after this without a volatile variable (as in C#).

This race hazard could be resolved by adding a memory barrier to the constructor:

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

Equivalently, it could be more concisely resolved by making the field volatile:

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

However, the latter is forbidden by the C# compiler:

'Program.ThreadSafeQueue<T>._queue': a field cannot be both volatile and readonly

Given that the above seems like a justifiable use-case for volatile readonly, is this limitation a flaw in the language design?

I'm aware that one could simply remove the readonly, since it does not affect the public interface of the class. However, that is beside the point, since the same could be said for readonly in general. I'm also aware of the existing question “Why readonly and volatile modifiers are mutually exclusive?”; however, that tackled a different issue.

Concrete scenario: This issue seems to affect code in the System.Collections.Concurrent namespace of the .NET Framework Class Library itself. The ConcurrentQueue<T>.Segment nested class has several fields that are only ever assigned within the constructor: m_array, m_state, m_index, and m_source. Of these, only m_index is declared as readonly; the others cannot be – although they should – since they need to be declared as volatile to meet the requirements of thread-safety.

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;
    }

    // ...
}

回答1:


readonly fields are fully writable from the constructor's body. Indeed, one could use volatile accesses to a readonly field to cause a memory barrier. Your case is, I think, a good case for doing that (and it's prevented by the language).

It is true that writes made inside of the constructor might not be visible to other threads after the ctor has completed. They might become visible in any order even. This is not well known because it rarely comes into play in practice. The end of the constructor is not a memory barrier (as often assumed from intuition).

You can use the following workaround:

class Program
{
    readonly int x;

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

I tested that this compiles. I was not sure whether it is allowed to form a ref to a readonly field or not, but it is.

Why does the language prevent readonly volatile? My best guess is that this is to prevent you from making a mistake. Most of the time this would be a mistake. It's like using await inside of lock: Sometimes this is perfectly safe but most of the time it's not.

Maybe this should have been a warning.

Volatile.Write did not exist at the time of C# 1.0 so the case for making this a warning for 1.0 is stronger. Now that there is a workaround the case for this being an error is strong.

I do not know if the CLR disallows readonly volatile. If yes, that could be another reason. The CLR has a style of allowing most operations that are reasonable to implement. C# is far more restrictive than the CLR. So I'm quite sure (without checking) that the CLR allows this.




回答2:


ThreadSafeQueue<int> tsqueue = null;

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

In your example the issue is that tsqueue is published in non-thread safe manner. And in this scenario it's absolutely possible to get a partially constructed object on architectures like ARM. So, mark tsqueue as volatile or assign the value with Volatile.Write method.

This issue seems to affect code in the System.Collections.Concurrent namespace of the .NET Framework Class Library itself. The ConcurrentQueue.Segment nested class has several fields that are only ever assigned within the constructor: m_array, m_state, m_index, and m_source. Of these, only m_index is declared as readonly; the others cannot be – although they should – since they need to be declared as volatile to meet the requirements of thread-safety.

Marking fields as readonly just adds some constraints that compiler checks and that JIT might later use for optimizations (but JIT is smart enough to figure out that field is readonly even without that keyword in some cases). But marking these particular fields volatile is much more important because of concurrency. private and internal fields are under control of the author of that library, so it's absolutely ok to omit readonly there.




回答3:


First of all, it seems to be a limitation imposed by the language, not the platform:

.field private initonly class SomeTypeDescription modreq ([mscorlib]System.Runtime.CompilerServices.IsVolatile) SomeFieldName    

compiles fine, and I couldn't find any quote stating that initonly (readonly) can not be paired with modreq ([mscorlib]System.Runtime.CompilerServices.IsVolatile) (volatile).

As far as I understand, the described situation probably comes from low level instruction swapping. The code that constructs an object and places it into a field looks like:

newobj       instance void SomeClassDescription::.ctor()   
stfld        SomeFieldDescription

And as ECMA states:

The newobj instruction allocates a new instance of the class associated with ctor and initializes all the fields in the new instance to 0 (of the proper type) or null as appropriate. It then calls the constructor with the given arguments along with the newly created instance. After the constructor has been called, the now initialized object reference is pushed on the stack.

So, as far as I understand, until the instructions are not swapped (which is imho possible because returning of the address of created object and filling this object are stores to different locations), you always see either fully initialized object or null when reading from another thread. That can be guaranteed by using volatile. It would prevent swapping:

newobj
volatile.
stfld

P.s. It's not an answer in itself. I don't know why C# forbids readonly volatile.



来源:https://stackoverflow.com/questions/39004125/should-volatile-and-readonly-be-mutually-exclusive

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!