Can DateTime tear in a 64 bit environment?
Asked Answered
P

3

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();
}
Pastern answered 15/2, 2017 at 19:24 Comment(10)
probably the most definitive answer you can get: "I don't think so."Kyte
@MikeNakis I also "don't think so" but ConcurrentDictionary's implementation doesn't treat DateTime as atomic, which makes me wonder: ConcurrentDictionary.IsValueWriteAtomicPastern
@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.Fabrice
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 structs 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-:=Kyte
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.Estrin
@Estrin 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.Pastern
@HansPassant Why is that an issue? What does setting automatic layout cause?Pastern
@Pastern - probably funny things like this. 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.Dregs
I wrote a testable version of it gist.github.com/Flash3001/ec0da534167bd3cdb85bcfc12eff0838Complice
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.Koontz
B
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
}
Bocock answered 15/2, 2017 at 20:52 Comment(11)
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?Pastern
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)Bocock
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.Pastern
IsReadsAndWritesGuaranteedAtomic --> AreReadsAndWritesGuaranteedAtomic? or maybe ReadsAndWritesAreGuaranteedAtomic would be better.Estrin
@Pastern here is the exact code I ran, 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.Bocock
@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.Pastern
@Pastern not at all, feel free.Bocock
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.Logician
@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.Bocock
Right you are on IsAutoLayout my eyes somehow skipped past that.Logician
@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.Bocock
C
13

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

Complice answered 17/2, 2017 at 14:10 Comment(0)
D
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.

Dysprosium answered 15/2, 2017 at 20:54 Comment(1)
True in principle, but we often build types that provide stricter guarantees, so that's not answering the question.Under

© 2022 - 2024 — McMap. All rights reserved.