Apparently, a variable can appear to hold a value that was never assigned to it. I ran the following program on a 64-bit OS, targeting "AnyCPU" and .NET Framework 4.5 (although it turns out it ran in 32-bit mode). One thread assigns a variable to one of two values, and another thread reads the variable:
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using T = System.Int64;
class Program
{
static void Main()
{
T zero = 0;
new Racer(zero, (T)~zero).Run();
Console.Read();
}
}
class Racer
{
readonly T _a;
readonly T _b;
T _current;
public Racer(T a, T b)
{
_a = a;
_b = b;
_current = a;
}
public void Run()
{
Dump(nameof(_a), _a);
Dump(nameof(_b), _b);
Task.Run((Action)SetForever);
Task.Run((Action)GetForever);
}
void SetForever()
{
while (true)
{
_current = _a;
_current = _b;
}
}
void GetForever()
{
var encountered = new HashSet<T>();
while (true)
{
var cu = _current;
if (encountered.Add(cu))
{
Dump(nameof(cu), cu);
}
}
}
static void Dump(string prefix, T value)
{
var numBits = sizeof(T) * 8;
var binaryChars = new char[numBits];
for (var i = 0; i < numBits; i++)
{
binaryChars[i] = (value & ((T)1 << i)) == 0 ? '0' : '1';
}
Console.WriteLine($"{prefix} = {new string(binaryChars)} = {value}");
}
}
Scarily, when I run this program, the console outputs the following:
_a = 0000000000000000000000000000000000000000000000000000000000000000 = 0
_b = 1111111111111111111111111111111111111111111111111111111111111111 = -1
cu = 0000000000000000000000000000000011111111111111111111111111111111 = -4294967296
cu = 1111111111111111111111111111111100000000000000000000000000000000 = 4294967295
cu = 1111111111111111111111111111111111111111111111111111111111111111 = -1
cu = 0000000000000000000000000000000000000000000000000000000000000000 = 0
Even though we only ever assigned the values 0
and -1
, the variable sometimes appears to have the values -4294967296
and 4294967295
!
If I change T
to UInt64
, I get similarly scary results. However, if I change T
to SByte
, Byte
, Int16
, UInt16
, Int32
, or UInt32
, it behaves "properly", i.e., it only ever reads one of the two values that we wrote.
First of all, what's going on here? It looks like the tear is happening on the boundary between the two 32-bit chunks. Is this scary behavior therefore possible for any struct larger than 32 bits? Or is it possible for any struct? Is it even possible for references to value types? That would be even scarier, since we would get the wrong memory location.
Secondly, what should I do about it? I thought the volatile
keyword might help here, but it's not applicable to Int64
or UInt64
, although it is applicable to the types that appear to behave "properly" without it. Should I always use a lock
when accessing a variable that could be set from another thread? Until now, I have only used lock
under more advanced circumstances, e.g., when I need to set two variables at once, or when multiple threads need to increment a variable.