54

In C# setting a value to a variable is atomic as long as its size is at most native int (i.e. 4 bytes in a 32-bit runtime environment and 8 bytes on a 64-bit one). In a 64-bit environment that includes all references types and most built-in value types (byte, short, int, long, etc.).

Setting a bigger value isn't atomic and can cause tearing where only part of the memory is updated.

DateTime is a struct that includes only a single ulong field containing all its data (Ticks and the DateTimeKind) and ulong by itself is atomic in a 64-bit environment.

Does that mean that DateTime is atomic as well? Or Can the following code lead to tearing at some point?

static DateTime _value;
static void Main()
{
    for (int i = 0; i < 10; i++)
    {
        new Thread(_ =>
        {
            var random = new Random();
            while (true)
            {
                _value = new DateTime((long)random.Next() << 30 | (long)random.Next());
            }
        }).Start();
    }

    Console.ReadLine();
}
i3arnon
  • 113,022
  • 33
  • 324
  • 344
  • probably the most definitive answer you can get: "I don't think so." – Mike Nakis Feb 15 '17 at 19:43
  • 3
    @MikeNakis I also "don't think so" but `ConcurrentDictionary`'s implementation doesn't treat `DateTime` as atomic, which makes me wonder: [ConcurrentDictionary.IsValueWriteAtomic](https://github.com/dotnet/corefx/blob/master/src/System.Collections.Concurrent/src/System/Collections/Concurrent/ConcurrentDictionary.cs#L87) – i3arnon Feb 15 '17 at 19:46
  • 4
    @i3arnon: The code lists a bunch of integral types with a comment pointing to the CLI reference. The CLI reference only deals with sizes, not types, and I seriously doubt DateTime would ever use an explicitly misaligned field, so I think it's safe to conclude that `DateTime` is atomic. – Stephen Cleary Feb 15 '17 at 19:57
  • 3
    This is quite strange. The standard says that all modifications to values no larger than the native int size shall be atomic, and `DateTime` fits this category on a 64-bit system. However, `ConcurrentDictionary` appears to be much more conservative than the standard: it will not consider no-larger-than-native-int `struct`s as atomic, it will only consider built-in primitive data types as atomic. Conventional wisdom says it is probably a flaw in `ConcurrentDictionary`, but note how I am writing a comment and not an answer! C-:= – Mike Nakis Feb 15 '17 at 20:52
  • 2
    To me, the bigger question is, "Why do you care?" Why spend all this time trying to figure it out instead of just assuming it's *not* safe and writing your code under that assumption? I'm not sure I'd count on MS to follow the specification even if it does say it's safe. ;) So this feels like an XY-problem to me. – jpmc26 Feb 16 '17 at 00:19
  • 6
    @jpmc26 just setting a value instead of taking a lock is extremely beneficial in highly concurrent scenarios. I care because caring enables me to break bottlenecks and improve the performance of my product. – i3arnon Feb 16 '17 at 02:02
  • 1
    @HansPassant Why is that an issue? What does setting automatic layout cause? – i3arnon Feb 16 '17 at 02:05
  • @i3arnon - [probably funny things like this](http://stackoverflow.com/questions/4132533/why-does-layoutkind-sequential-work-differently-if-a-struct-contains-a-datetime). I don't know about other shenanigans. Personally, I rather more dislike `DateTime` et al. and the shape of the API (which unfortunately works just well enough for the simple cases). There's some random gotchas which make certain use cases impossible to solve natively. – Clockwork-Muse Feb 16 '17 at 03:41
  • 1
    I wrote a testable version of it https://gist.github.com/Flash3001/ec0da534167bd3cdb85bcfc12eff0838 – Lucas Teixeira Feb 16 '17 at 12:30
  • No, DateTime would not be atomic? I would expect that since DateTime is a struct that struct rules apply. The fact that its underlying value happens to use native word size is not relevant. Except for when a stuct is allocated/”contructed” shouldn’t I assume it’s subject to concurrency issues across fields? Side Note: I find the built in .net Interlocked Class (System.Threading.Interlocked) very useful in the real world where I know I am using Int64 on thousands of 32 bit runtimes that are on 32 bit kernels. – Sql Surfer Feb 18 '17 at 16:11

3 Answers3

36

From the ECMA specification section "I.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 §I.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 a native int) shall be properly aligned. Object references shall be treated as though they are stored in the native word size.

A native int is a IntPtr in C#.

So long as sizeof(IntPtr) >= sizeof(DateTime) is true for the runtime environment (aka: running as 64 bit), and they don't alter the internal structure to be explicit layout with misaligned bytes instead of the [StructLayout(LayoutKind.Auto)] it currently has, then reads and writes of a DateTime struct (or any other struct that follows those rules) are guaranteed to be atomic by the ECMA specification.

You can verify that by running the following code in a 64-bit environment:

public unsafe static void Main()
{
    Console.WriteLine(sizeof(DateTime)); // Outputs 8
    Console.WriteLine(sizeof(IntPtr)); // Outputs 8
    Console.WriteLine(sizeof(ulong)); // Outputs 8
}
Scott Chamberlain
  • 124,994
  • 33
  • 282
  • 431
  • But the question stands: Is `sizeof(IntPtr) == sizeof(DateTime)`? `DateTime` only contains a `ulong` but does that mean `sizeof(DateTime) == sizeof(ulong) == sizeof(IntPtr) == 8`? – i3arnon Feb 15 '17 at 21:34
  • Yes, all 3 output 8 on a 64 bit environment, therfor are atomic according to the spec. In a 32 bit environment, `DateTime` and `ulong` return 8 but `IntPtr` returns 4 which violates the `sizeof(IntPtr) >= sizeOf(DateTime)` rule so there for are not guaranteed atomic (which is what we expected to happen) – Scott Chamberlain Feb 15 '17 at 22:48
  • Wait, what do you mean all 3 output 8? How do you run `sizeof(DateTime)`? I can't compile that and get "`DateTime` does not have a predefined size". You can use `Marshal.SizeOf` but that test the size when marshalling. – i3arnon Feb 15 '17 at 23:29
  • `IsReadsAndWritesGuaranteedAtomic` --> `AreReadsAndWritesGuaranteedAtomic`? or maybe `ReadsAndWritesAreGuaranteedAtomic` would be better. – jpmc26 Feb 16 '17 at 00:16
  • 3
    @i3arnon [here is the exact code I ran](https://gist.github.com/leftler/e45e6f68a0986408fad468080e7492f4), you just need to mark the assembly as unsafe in the project build settings, however I did update my example function at the end to use unsafe code instead of the marshal class because you did make a good point about the potential size difference. – Scott Chamberlain Feb 16 '17 at 00:40
  • @ScottChamberlain would you mind if I (or you) add that code to the answer? I think being able to run `sizeof(DateTime)` is the most definitive answer we'll get. – i3arnon Feb 17 '17 at 15:36
  • @i3arnon not at all, feel free. – Scott Chamberlain Feb 17 '17 at 15:36
  • The `AreReadsAndWritesAreGuaranteedAtomic` method here doesn't check that there is only one field (a `struct` with two `int` fields need not be written to as a single 64-bit write, but rather it contains two memory locations that can be written to as two writes that are individually atomic but not atomic as a pair) nor checks for the existence of `StructLayoutAttribute`. – Jon Hanna Feb 21 '17 at 15:16
  • @JonHanna `type.IsAutoLayout` does check for `StructLayoutAttribute`, it is a convenience property that does not require you to extract the attribute. However i will give you that the two int example will return a `sizeof(example) == 8` on a 64 bit machine and will not work. I have removed the function from the question. – Scott Chamberlain Feb 21 '17 at 16:26
  • Right you are on `IsAutoLayout` my eyes somehow skipped past that. – Jon Hanna Feb 21 '17 at 16:43
  • @JonHanna thinking about it more, I don't know how the CLR treats your two int situation, it may treat the struct as a single 8 byte object not two 4 byte objects when doing assignment so it may still be atomic. It is not clear based on the specification, I am still going to leave the function off but it might be worth testing. – Scott Chamberlain Feb 21 '17 at 16:44
11

Running some tests and based on the above answer it is pretty safe to say it is atomic today.

I wrote a test to verify how many tears could be found during X iterations over N threads for Int64, DateTime and 3 custom structs of 128, 192 and 256 sizes - none with their StructLayout messed up.

The test consists of:

  1. Adding a set of values to a array so they are known.
  2. Setting up one thread for each array position, this thread will assign the value from the array to a shared variable.
  3. Setting up the same number of threads (array.length) to read from this shared variable to a local.
  4. Check if this local is contained in the original array.

The results are as follows in my machine (Core i7-4500U, Windows 10 x64, .NET 4.6, Release without debug, Platform target: x64 with code optimization):

-------------- Trying to Tear --------------
Running: 64bits
Max Threads: 30
Max Reruns: 10
Iterations per Thread: 20000
--------------------------------------------
----- Tears ------ | -------- Size ---------
          0             Int64 (64bits)
          0             DateTime (64bits)
         23             Struct128 (128bits)
         87             Struct192 (192bits)
         43             Struct256 (256bits)
----- Tears ------ | -------- Size ---------
          0             Int64 (64bits)
          0             DateTime (64bits)
         44             Struct128 (128bits)
         59             Struct192 (192bits)
         52             Struct256 (256bits)
----- Tears ------ | -------- Size ---------
          0             Int64 (64bits)
          0             DateTime (64bits)
         26             Struct128 (128bits)
         53             Struct192 (192bits)
         45             Struct256 (256bits)
----- Tears ------ | -------- Size ---------
          0             Int64 (64bits)
          0             DateTime (64bits)
         46             Struct128 (128bits)
         57             Struct192 (192bits)
         56             Struct256 (256bits)
------------------- End --------------------

The code for the test can be found here: https://gist.github.com/Flash3001/da5bd3ca800f674082dd8030ef70cf4e

Community
  • 1
  • 1
Lucas Teixeira
  • 704
  • 8
  • 11
1

From C# language specification.

5.5 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.

OmariO
  • 506
  • 4
  • 11