Is Accessing a Variable in C# an Atomic Operation

Is accessing a variable in C# an atomic operation?

For the definitive answer go to the spec. :)

Partition I, Section 12.6.6 of the CLI spec states: "A conforming CLI shall guarantee that read and write access to properly aligned memory locations no larger than the native word size is atomic when all the write accesses to a location are the same size."

So that confirms that s_Initialized will never be unstable, and that read and writes to primitve types smaller than 32 bits are atomic.

In particular, double and long (Int64 and UInt64) are not guaranteed to be atomic on a 32-bit platform. You can use the methods on the Interlocked class to protect these.

Additionally, while reads and writes are atomic, there is a race condition with addition, subtraction, and incrementing and decrementing primitive types, since they must be read, operated on, and rewritten. The interlocked class allows you to protect these using the CompareExchange and Increment methods.

Interlocking creates a memory barrier to prevent the processor from reordering reads and writes. The lock creates the only required barrier in this example.

What operations are atomic in C#?

For something more complete/detailed:

Reads and writes to 32-bit value types are atomic: This includes the following intrinsic value (struct) types: bool, char, byte, sbyte, short, ushort, int, uint, float. The following types (amongst others) are not guaranteed to be atomic: decimal, double, long, ulong.

e.g.

int x;
x = 10; // atomic
decimal d;

d = 10m; // not atomic

Reference assignment is also an atomic operation:

private String _text;
public void Method(String text)
{
_text = text; // atomic
}

Are reference assignment and reading atomic operations?

Yes, everything is guaranteed to be properly aligned unless you deliberately go out of your way to misalign stuff, meaning that reference assignment/reading is guaranteed to be atomic.

Section 12.6.6 of the CLI spec goes on to say this:

Unless explicit layout control (see
Partition II (Controlling Instance
Layout)) is used to alter the default
behavior, data elements no larger than
the natural word size (the size of a
native int) shall be properly
aligned. Object references shall be
treated as though they are stored in
the native word size.

There are also further details about alignment etc in section 12.6.2 of the spec.

Note that in your example code, the read in thread 2 is guaranteed to be atomic, but it's not guaranteed to actually see any changes made by thread 1: without enforcing memory barriers or volatility each thread can use its own "view" of the m_Object field without ever seeing changes made by other threads.

So, for example, thread 1 could be making (atomic) writes into its own view of m_Object, but the data is only ever actually held in a register or CPU cache and never comitted to main memory. Similarly, thread 2 could also be making (atomic) reads of m_Object, but actually reading from a register or CPU cache rather than main memory.

C# Is value type assignment atomic?

Structs are value types. If you assign a struct to a variable/field/method parameter, the whole struct content will be copied from the source storage location to the storage location of the variable/field/method parameter (the storage location in each case being the size of the struct itself).

Copying a struct is not guaranteed to be an atomic operation. As written in the C# language specification:

Atomicity of variable references

Reads and writes of the following data types are atomic: bool, char,
byte, sbyte, short, ushort, uint, int, float, and reference types. In
addition, reads and writes of enum types with an underlying type in
the previous list are also atomic. Reads and writes of other types,
including long, ulong, double, and decimal, as well as user-defined
types, are not guaranteed to be atomic. Aside from the library
functions designed for that purpose, there is no guarantee of atomic
read-modify-write, such as in the case of increment or decrement.



So yes, it can happen that while one thread is in the process of copying the data from a struct storage location, another thread comes along and starts copying new data from another struct to that storage location. The thread copying from the storage location thus can end up copying a mix of old and new data.


As a side note, your code can also suffer from other concurrency problems due to how one of your threads is writing to a variable and how the variable is used by another thread. (An answer by user acelent to another question explains this rather well in technical detail, so i will just refer to it: https://stackoverflow.com/a/46695456/2819245) You can avoid such problems by encapsulating any access of such "thread-crossing" variables in a lock block. As an alternative to lock, and with regard to basic data types, you could also use methods provided by the Interlocked class to access thread-crossing variables/fields in a thread-safe manner (Alternating between both lock and Interlocked methods for the same thread-crossing variable is not a good idea, though).

reference assignment is atomic so why is Interlocked.Exchange(ref Object, Object) needed?

There are numerous questions here. Considering them one at a time:

reference assignment is atomic so why is Interlocked.Exchange(ref Object, Object) needed?

Reference assignment is atomic. Interlocked.Exchange does not do only reference assignment. It does a read of the current value of a variable, stashes away the old value, and assigns the new value to the variable, all as an atomic operation.

my colleague said that on some platforms it's not guaranteed that reference assignment is atomic. Was my colleague correct?

No. Reference assignment is guaranteed to be atomic on all .NET platforms.

My colleague is reasoning from false premises. Does that mean that their conclusions are incorrect?

Not necessarily. Your colleague could be giving you good advice for bad reasons. Perhaps there is some other reason why you ought to be using Interlocked.Exchange. Lock-free programming is insanely difficult and the moment you depart from well-established practices espoused by experts in the field, you are off in the weeds and risking the worst kind of race conditions. I am neither an expert in this field nor an expert on your code, so I cannot make a judgement one way or the other.

produces warning "a reference to a volatile field will not be treated as volatile" What should I think about this?

You should understand why this is a problem in general. That will lead to an understanding of why the warning is unimportant in this particular case.

The reason that the compiler gives this warning is because marking a field as volatile means "this field is going to be updated on multiple threads -- do not generate any code that caches values of this field, and make sure that any reads or writes of this field are not "moved forwards and backwards in time" via processor cache inconsistencies."

(I assume that you understand all that already. If you do not have a detailed understanding of the meaning of volatile and how it impacts processor cache semantics then you don't understand how it works and should not be using volatile. Lock-free programs are very difficult to get right; make sure that your program is right because you understand how it works, not right by accident.)

Now suppose you make a variable which is an alias of a volatile field by passing a ref to that field. Inside the called method, the compiler has no reason whatsoever to know that the reference needs to have volatile semantics! The compiler will cheerfully generate code for the method that fails to implement the rules for volatile fields, but the variable is a volatile field. That can completely wreck your lock-free logic; the assumption is always that a volatile field is always accessed with volatile semantics. It makes no sense to treat it as volatile sometimes and not other times; you have to always be consistent otherwise you cannot guarantee consistency on other accesses.

Therefore, the compiler warns when you do this, because it is probably going to completely mess up your carefully developed lock-free logic.

Of course, Interlocked.Exchange is written to expect a volatile field and do the right thing. The warning is therefore misleading. I regret this very much; what we should have done is implement some mechanism whereby an author of a method like Interlocked.Exchange could put an attribute on the method saying "this method which takes a ref enforces volatile semantics on the variable, so suppress the warning". Perhaps in a future version of the compiler we shall do so.

Are reads and writes to properties atomic in C#?

You need to distinguish between "atomic" and "thread-safe" more closely. As you say, writes are atomic for most built-in value types and references.

However, that doesn't mean they're thread-safe. It just means that if values "A" and "B" are both written, a thread will never see something in between. (e.g. a change from 1 to 4 will never show 5, or 2, or any value other than 1 or 4.) It doesn't mean that one thread will see value "B" as soon as it's been written to the variable. For that, you need to look at the memory model in terms of volatility. Without memory barriers, usually obtained through locking and/or volatile variables, writes to main memory may be delayed and reads may be advanced, effectively assuming that the value hasn't changed since the last read.

If you had a counter and you asked it for its latest value but never received the latest value because of a lack of memory barriers, I don't think you could reasonably call that thread-safe even though each operation may well be atomic.

This has nothing to do with properties, however - properties are simply methods with syntactic sugar around them. They make no extra guarantees around threading. The .NET 2.0 memory model does have more guarantees than the ECMA model, and it's possible that it makes guarantees around method entry and exit. Those guarantees should apply to properties as well, although I'd be nervous around the interpretation of such rules: it can be very difficult to reason about memory models sometimes.



Related Topics



Leave a reply



Submit