Under C# is Int64 use on a 32 bit processor dangerous
Asked Answered
F

6

18

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?
Fennessy answered 27/2, 2009 at 18:44 Comment(1)
For atomic operations on Int64, you can use the InterLocked class (msdn.microsoft.com/en-us/library/…).Harper
B
19

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 not hoisted as a consequence of being closed over in a closure or an iterator transformation and are used by a single function (and thus, a single thread) at a time.

Besant answered 27/2, 2009 at 18:48 Comment(6)
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.Kailey
imagine class X { int n; }. Is it reference or value type? Will it be stored in heap or on stack?Mudslinger
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.Besant
Beware, as Eric Lippert has explained in blogs.msdn.com/b/ericlippert/archive/2010/09/30/…, the comment about stack or heap allocation of value types should be more correctly stated as: "in the Microsoft implementation of C# on the desktop CLR, value types are stored on the stack when the value is a local variable or temporary that is not a closed-over local variable of a lambda or anonymous method, and the method body is not an iterator block, and the jitter chooses to not enregister the value.".Microclimate
@Pablo Can you clarify which comment you're specifically objecting to?Besant
The comment by codekaizen: 'An Int64 is inherited from System.ValueType, which means that the value is stored on the stack.' The fact that it may be stored in the stack, is an implementation detail and, in fact, not true in all circumstances.Microclimate
S
12

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.

Studhorse answered 27/2, 2009 at 19:40 Comment(0)
F
7

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.

Fiddler answered 27/2, 2009 at 18:51 Comment(1)
True, the keyword is shared variable.Besant
K
2

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?

Kailey answered 27/2, 2009 at 19:35 Comment(3)
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.Besant
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.Kailey
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.Manxman
E
1

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.

Enneahedron answered 27/2, 2009 at 18:53 Comment(1)
"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.Manxman
M
0

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.

Mirabelle answered 27/2, 2009 at 18:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.