This is a continuation of the discussion on multithreading issues in C#.
In C , unprotected access to the shared data from multiple threads is an undefined behavior* if there is a write operation involved. What is it in C#? As (the safe part of) C# doesn't contain undefined behaviors, are there any guarantees? C# seems to have a kind of as-if rule as well, but after reading the mentioned part of the standard I fail to see what are the consequences of an unprotected data access from the language point of view.
In particular, it's interesting to know which kind of optimizations including load fusing and invention are prohibited through the language. This prohibition would imply the validity (or the lack thereof) of several popular patterns in C# (including the one discussed in the original question).
[The details of the actual implementation in Microsoft CLR, despite being very interesting, are not the part of this question: only the guarantees given by the language itself (and therefore portable) are here under discussion.]
The normative references are very welcome but I suspect the C# standard has enough information on the topic. Maybe someone from the language team can shed some light on what are the actual guarantees which are going to be included into the standard later but can be relied upon right now.
I suspect that there are some implied guarantees like the absence of pointer reference tearing because this could easily lead to breaking the type safety. But I'm not an expert on the topic.
*Often shortened as UB. Undefined Behavior allows a C compiler to produce literally any code, including formatting the hard disk or whatever, or to crash at compile time.
CodePudding user response:
the .net runtime guarantees that writes to some variable types are atomic
Reads and writes of the following data types shall be 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 shall also be atomic. Reads and writes of other types, including long, ulong, double, and decimal, as well as user-defined types, need not 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.
Not mentioned is IntPtr
, that I believe is also guaranteed to be atomic. Since references are atomic they are guaranteed not to tear. See also C# - The C# Memory Model in Theory and Practice for more information
There should also be a guarantee of memory safety, i.e. that any memory access will reference valid memory and that all memory is initialized before usage. With some exceptions for things like unmanaged resources, unsafe code and stackalloc.
The general rule with regards to optimization is that the compiler/jitter may perform any optimization as long as the result would be identical for a single threaded program. So tearing, fusing, reordering, etc would all be possible, absent any synchronization.
So always use appropriate synchronization whenever there is a possibility that multiple threads use the same memory concurrently for anything except reading. Note that ARM has weaker memory ordering guarantees than x86/x64, further emphasizing the need for synchronization.
CodePudding user response:
As mentioned by @JonasH, the C# spec only guarantees atomic access to values sized 32 bits or smaller.
But, assuming you can rely on C# always being implemented on a runtime conforming to ECMA-335, then you can rely on that spec also. This should be safe, as all implementations of .Net, including Mono and WASM, conform to ECMA-335 (it is not a Microsoft-only spec).
ECMA-335 guarantees access to native-sized values, which includes IntPtr
and object references, as well as 64-bit integers on a 64-bit architecture.
ECMA-335 says: (my bold)
12.6.6 Atomic reads and writes
A conforming CLI shall guarantee that read and write access to properly aligned memory locations no larger than the native word size (the size of type
native int
) is atomic (see §12.6.2) when all the write accesses to a location are the same size. Atomic writes shall alter no bits other than those written. 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 anative int
) shall be properly aligned. Object references shall be treated as though they are stored in the native word size.[Note: There is no guarantee about atomic update (read-modify-write) of memory, except for methods provided for that purpose as part of the class library (see Partition IV). An atomic write of a "small data item" (an item no larger than the native word size) is required to do an atomic read/modify/write on hardware that does not support direct writes to small data items. end note]
You seem to be asking specifically about the atomicity of the code
if (SomeEvent != null) SomeEvent(this, args);
This code is not guaranteed to be thread-safe, either by the C# spec or by the .NET spec. While it is true that an optimizing JIT compiler might generate thread-safe code, it's unsafe to rely on it.
Instead use the better (and more concise) code, this is guaranteed thread-safe.
SomeEvent?.Invoke(this, args);