3

Is the assignement of a value type considered to be atomic in .Net?

For example, consider the following program:

struct Vector3
{
    public float X { get; private set; }
    public float Y { get; private set; }
    public float Z { get; private set; }


    public Vector3(float x, float y, float z)
    {
        this.X = x;
        this.Y = y;
        this.Z = z;
    }

    public Vector3 Clone()
    {
        return new Vector3(X, Y, Z);
    }

    public override String ToString()
    {
        return "(" + X + "," + Y + "," + Z + ")";
    }
}

class Program
{
    private static Vector3 pos = new Vector3(0,0,0);

    private static void ReaderThread()
    {
        for (int i = 0; i < int.MaxValue; i++)
        {
            Vector3 v = pos;
            Console.WriteLine(v.ToString());
            Thread.Sleep(200);
        }

    }

    private static void WriterThread()
    {
        for (int i = 1; i < int.MaxValue; i++)
        {
            pos = new Vector3(i, i, i);
            Thread.Sleep(200);
        }
    }


    static void Main(string[] args)
    {
        Thread w = new Thread(WriterThread);
        Thread r = new Thread(ReaderThread);

        w.Start();
        r.Start();
    }
}

Can a program like this suffer from a High-Level data race? Or even a Data Race?

What I want to know here is: is there any possibility that v will either contain:

  • Garbage values due to a possible data race
  • Mixed components X, Y or Z that refer to both pos before assignement and pos after assignment. For example, if pos = (1,1,1) and then pos is assigned the new value of (2,2,2) can v = (1,2,2)?
Ben Voigt
  • 277,958
  • 43
  • 419
  • 720
  • As written I'm fairly sure it doesn't even *compile*, `bw` is not defined. That said, value types copy so I'm not sure what it would do (to you) even if its *not* thread safe. – BradleyDotNET Dec 03 '18 at 20:09
  • This is more or less pseudocode, I can change it to full code but does it actually add anything to the question? – André Santos Dec 03 '18 at 20:12
  • 1
    Not all value type assignment is atomic. Take a look at the accepted answer in: https://stackoverflow.com/questions/2433772/are-primitive-data-types-in-c-sharp-atomic-thread-safe. – Flydog57 Dec 03 '18 at 20:17
  • 1
    I'm just trying to figure out exactly what you are asking here. The assignment of `v` won't be affected when `pos` is reassigned, threads or no threads. That operation will take as long as the copy operation takes (which I doubt is any kind of atomic) – BradleyDotNET Dec 03 '18 at 20:18
  • 2
    *"but does it actually add anything to the question".* **Yes.** In two ways: 1) when the reader reads your code, if something stands out as odd, and your code is compilable, then it really is odd. In this case, your code doesn't compile, so it may just be random fluff. 2) If you code compiles, I can copy it, compile it, debug it, etc. – Flydog57 Dec 03 '18 at 20:19
  • Sorry guys, I edited the source and the question to be more clear. Also the code compiles perfectly. Running it seems like it's thread safe, but I would like to have some certainty. – André Santos Dec 03 '18 at 20:33
  • @BradleyDotNET I was under the assumption (and correct me if I am wrong) that, Value-Types that are class fields were **heap** allocated. As such, I don't understand how the copy procedure would work if another thread was modifying it meanwhile. – André Santos Dec 03 '18 at 20:39
  • @AndréSantos, structs are value types. As such if you assign a struct to a variable/field/method parameter, the whole struct content will be copied to the storage location of the variable/field (the storage location in each case being the size of the struct itself). It doesn't matter whether those storage locations are on the stack or part of an object data blob on the heap. –  Dec 03 '18 at 20:52
  • @elgonzo but that copy takes n steps right, where n is the size of a WORD in the CPU correct? Now since there is a writer thread continuously assigning a new value to the struct, can it assign before the copy to the other thread is complete, such that it will cause garbage values to be copied? – André Santos Dec 03 '18 at 20:54
  • Copying (reading/writing) a struct is not atomic. So yes, it can happen that while one thread is in the process of copying the struct data **from** a 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 copy a mix of old and new data... –  Dec 03 '18 at 20:57
  • @elgonzo thank you very much, you should actually write that as the answer as it's exactly what the question was asking! – André Santos Dec 03 '18 at 20:59

1 Answers1

10

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

  • Weird. Someone downvoted. I have no idea why. I guess you cannot make everyone happy ;-) –  Dec 03 '18 at 21:55
  • Thank you for the answer. Unfortunately, I do not wish to use locks at all as that will severely decrease the concurrency. I'll rather use a class (which is passed by reference) and instead work with references. – André Santos Dec 03 '18 at 23:53
  • Even if you use a reference type, you will have to be careful. Just a simple example to illustrate: Lets say the reference type you want to exchange between threads has two properties/fields with relevant data. So, while one thread is doing something like `var a = vectorObject.X; var b = vectorObject.Y;` (okay, i admit, stupid example, but humor me). vectorObject is the "thread-crossing" variable. Keep in mind that each assignment might perhaps be atomic, but both assignments together are **not**. (1/2) –  Dec 04 '18 at 00:01
  • (2/2) Without locking (or other form of synchronization), it could happen that the second thread would replace the object reference in `vectorObject` right after thread one assigned `a` but has not yet pulled the data from `vectorObject.Y` for assignment of `b`. If this happens, you are right where you are now: The first thread working with a (bad) mix of data that partially stems from the old vectorObject object and partially from the new vectorObject. –  Dec 04 '18 at 00:01
  • A simple solution to this would be to have a dedicated exchange reference variable, which is not operated on except for assigning an vector/data object reference from/to local variables which solely belong (are under sole control of) a particular thread. Any access of members of this vector/object then will happen through the variables that are local to their thread... (since reading/writing of reference type values is atomic, you wouldn't need explicit locking/thread-synchronization) –  Dec 04 '18 at 00:20
  • Sure, the reference approach I thought, assumed an imutable object (with imutable reference fields). If any thread wanted to change the object, it would first need to create it and then assign it to the reference. Of course, for multiple threads to write, an additional approach like the compare and set would be needed, but it's still better than locks. – André Santos Dec 04 '18 at 03:26