views:

1821

answers:

7

I read in the MS documentation that assigning a 64-bit value on a 32-bit Intel computer is not an atomic operation; that is, the operation is not thread safe. This means that if two people simultaneously assign a value to a static Int64 field, the final value of the field cannot be predicted.

Three part question:

  • Is this really true?
  • Is this something I would worry about in the real world?
  • If my application is multi-threaded do I really need to surround all my Int64 assignments with locking code?
+1  A: 

Yes
Yes
Yes

Outlaw Programmer
Why all the downvotes? The answer is yes to all 3 of his questions.
Jon Tackabury
Well, to be fair, everyone else has a much better answer than me!
Outlaw Programmer
@Jon T: The third question is: "If my application is multi-threaded do I really need to surround all my Int64 assignments with locking code?" The answer is not "Yes".
Mehrdad Afshari
Well my interpretation was something like 'do I need to surround all of my SHARED Int64 access with locking code." I guess I kind of assumed the OP knew that local variables are implicitly threadsafe.
Outlaw Programmer
I upvoted. Bunch of hater losers.
Andrei Rinea
+14  A: 

This is not about every variable you encounter. If some variable is used as a shared state or something (including, but not limited to some static fields), you should take care of this issue. It's completely non-issue for local variables that are used by a single function (and thus, a single thread) at a time.

Mehrdad Afshari
This is correct, however it might not be clear why. An Int64 is inherited from System.ValueType, which means that the value is stored on the stack. Since each thread gets its own call stack, each thread has its own value, even when calling the same function.
codekaizen
imagine class X { int n; }. Is it reference or value type? Will it be stored in heap or on stack?
DK
DK, I don't think this is a relevant question but classes are reference types and are stored in a heap. If you hold a reference to a class in just a single thread, you still wouldn't need to worry about locking issues.
Mehrdad Afshari
+5  A: 

MSDN:

Assigning an instance of this type is not thread safe on all hardware platforms because the binary representation of that instance might be too large to assign in a single atomic operation.

But also:

As with any other type, reading and writing to a shared variable that contains an instance of this type must be protected by a lock to guarantee thread safety.

eljenso
True, the keyword is **shared variable**.
Mehrdad Afshari
+1  A: 

On a 32-bit x86 platform the largest atomic sized piece of memory is 32-bits.

This means that if something writes to or reads from a 64-bit sized variable it's possible for that read/write to get pre-empted during execution.

  • For example, you start to assign a value to a 64 bit variable.
  • After the first 32 bits are written the OS decides that another process is going to get CPU time.
  • The next process attempts to read the variable you were in the middle of assigning to.

That's just one possible race condition with 64-bit assignment on a 32 bit platform.

However, even with 32 bit variable there can be race conditions with reading and writing therefor any shared variable should be synchronized in some way to solve these race conditions.

Ben S
"On a 32-bit x86 platform the largest atomic sized piece of memory is 32-bits." - That's wrong. You can write 8-byte atomically via fstp / mmx / sse.
Zach Saw
A: 

Is this really true? Yes, as it turns out. If your registers only have 32 bits in them, and you need to store a 64-bit value to some memory location, it's going to take two load operations and two store operations. If your process gets interrupted by another process between these two load/stores, the other process might corrupt half your data! Strange but true. This has been a problem on every processor ever built - if your datatype is longer than your registers, you will have concurrency issues.

Is this something I would worry about in the real world? Yes and no. Since almost all modern programming is given its own address space, you will only need to worry about this if you're doing multi-threaded programming.

If my application is multi-threaded do I really need to surround all my Int64 assignments with locking code? Sadly, yes if you want to get technical. It's usually easier in practice to use a Mutex or Semaphore around larger code blocks than to lock every individual set statement on globally accessible variables.

Mike
+2  A: 

If you do have a shared variable (say, as a static field of a class, or as field of a shared object), and that field or object is going to be used cross-thread, then, yes, you need to make sure that access to that variable is protected via an atomic operation. The x86 processor has intrinsics to make sure this happens, and this facility is exposed through the System.Threading.Interlocked class methods.

For example:

class Program
{
    public static Int64 UnsafeSharedData;
    public static Int64 SafeSharedData;

    static void Main(string[] args)
    {
        Action<Int32> unsafeAdd = i => { UnsafeSharedData += i; };
        Action<Int32> unsafeSubtract = i => { UnsafeSharedData -= i; };
        Action<Int32> safeAdd = i => Interlocked.Add(ref SafeSharedData, i);
        Action<Int32> safeSubtract = i => Interlocked.Add(ref SafeSharedData, -i);

        WaitHandle[] waitHandles = new[] { new ManualResetEvent(false), 
                                           new ManualResetEvent(false),
                                           new ManualResetEvent(false),
                                           new ManualResetEvent(false)};

        Action<Action<Int32>, Object> compute = (a, e) =>
                                            {
                                                for (Int32 i = 1; i <= 1000000; i++)
                                                {
                                                    a(i);
                                                    Thread.Sleep(0);
                                                }

                                                ((ManualResetEvent) e).Set();
                                            };

        ThreadPool.QueueUserWorkItem(o => compute(unsafeAdd, o), waitHandles[0]);
        ThreadPool.QueueUserWorkItem(o => compute(unsafeSubtract, o), waitHandles[1]);
        ThreadPool.QueueUserWorkItem(o => compute(safeAdd, o), waitHandles[2]);
        ThreadPool.QueueUserWorkItem(o => compute(safeSubtract, o), waitHandles[3]);

        WaitHandle.WaitAll(waitHandles);
        Debug.WriteLine("Unsafe: " + UnsafeSharedData);
        Debug.WriteLine("Safe: " + SafeSharedData);
    }
}

The results:

Unsafe: -24050275641 Safe: 0

On an interesting side note, I ran this in x64 mode on Vista 64. This shows that 64 bit fields are treated like 32 bit fields by the runtime, that is, 64 bit operations are non-atomic. Anyone know if this is a CLR issue or an x64 issue?

codekaizen
As Jon Skeet and Ben S pointed out, the race condition might occur between reads and writes, so you can't deduce that the writes were non-atomic.
Mehrdad Afshari
I don't understand... that argument goes either way. As far as I can tell, the data is still wrong.If you run the example, it's obvious that the data ends up wrong due to non-atomic operations.
codekaizen
The issue neither CLR or x64. It's with your code. What you're trying to do is atomic read+add/subtract+write. Whereas in x64 you're guaranteed atomic read/write of int64. Again, this is different from atomic read+add+write.
Zach Saw
+8  A: 

Even if the writes were atomic, chances are you would still need to take out a lock whenever you accessed the variable. If you didn't do that, you'd at least have to make the variable volatile to make sure that all threads saw the new value the next time they read the variable (which is almost always what you want). That lets you do atomic, volatile sets - but as soon as you want to do anything more interesting, such as adding 5 to it, you'd be back to locking.

Lock free programming is very, very hard to get right. You need to know exactly what you're doing, and keep the complexity to as small a piece of code as possible. Personally, I rarely even try to attempt it other than for very well known patterns such as using a static initializer to initialize a collection and then reading from the collection without locking.

Using the Interlocked class can help in some situations, but it's almost always a lot easier to just take out a lock. Uncontested locks are "pretty cheap" (admittedly they get expensive with more cores, but so does everything) - don't mess around with lock-free code until you've got good evidence that it's actually going to make a significant difference.

Jon Skeet