Should volatile and readonly be mutually exclusive?

只谈情不闲聊 提交于 2019-12-05 06:11:40

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.

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.

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.

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