尝试在 .NET 中捕获内存重新排序
Attempting to catch memory reordering in .NET
我有一个有点奇怪的程序,我试图在 .NET 中捕获一些内存重新排序:
private static volatile int x, y;
private static int b = -1;
private static int a = -1;
static void Main()
{
Run();
}
public static void Run()
{
Random rand = new Random();
var time = DateTime.Now;
while ((a != 0 || b != 0))
{
int t1StartId = rand.Next(1, 10);
int t2StartId = rand.Next(1, 10);
Thread t1 = new Thread(RunOne);
Thread t2 = new Thread(RunTwo);
for (int i = 0; i <= 10; i++)
{
if (t1StartId == i)
{
t1.Start();
}
if (t2StartId == i)
{
t2.Start();
}
}
t1.Join();
t2.Join();
x = 0;
y = 0;
}
Console.WriteLine("Memory Reordered!!!!!!!!!!!!!!!!!!!!!");
Console.WriteLine("Time Taken: {0}", DateTime.Now - time);
}
static void RunOne()
{
x = 1;
a = y;
}
static void RunTwo()
{
y = 1;
b = x;
}
}
现在从我一直在做的阅读(here, here, here, here)来看,*应该有可能在每个线程完成执行后 a 和 b 都等于 0。有人能证实这一点吗?我的任何操作是否在我不知道的场景下出现了内存障碍,这会阻止重新排序,从而使无限循环继续下去?
我运行在 x86 CPU 上进行此操作,我怀疑这也会对结果产生影响。
编辑:
因此,在 运行 程序多次运行后,它现在正在退出循环:
我仍然不相信这是内存重新排序的结果。
第二个运行:
如此决定性的问题! - 这真的是内存重新排序的结果,还是其他原因?
是的,您的示例演示了内存重新排序。我无法自己复制它,但这可能是我特定设置的结果。
我将使用向下箭头 ↓ 表示易失性读取,使用向上箭头 ↑ 表示易失性写入。将箭头视为推开任何其他读取和写入。只要没有指令通过向下箭头向上和通过向上箭头向下,生成这些内存栅栏的代码就可以自由移动。然而,内存栅栏(箭头)被锁定在它们最初在代码中声明的位置。因此,使用图示的围栏重现您的代码将如下所示。
static void RunOne()
{
↑ // volatile write fence
x = 1;
var register = y;
↓ // volatile read fence
a = register;
}
static void RunTwo()
{
↑ // volatile write fence
y = 1;
var register = x;
↓ // volatile read fence
b = register;
}
所以你可以看到在RunOne
中写入x
和读取y
是可以合法交换的。类似地,在 RunTwo
中,写入 y
和读取 x
也可以合法交换。
您使用 x86 体系结构这一事实对本示例没有影响,因为易失性写入后跟易失性读取是唯一仍然允许在 x86 的强内存模型中交换的安排。
我有一个有点奇怪的程序,我试图在 .NET 中捕获一些内存重新排序:
private static volatile int x, y;
private static int b = -1;
private static int a = -1;
static void Main()
{
Run();
}
public static void Run()
{
Random rand = new Random();
var time = DateTime.Now;
while ((a != 0 || b != 0))
{
int t1StartId = rand.Next(1, 10);
int t2StartId = rand.Next(1, 10);
Thread t1 = new Thread(RunOne);
Thread t2 = new Thread(RunTwo);
for (int i = 0; i <= 10; i++)
{
if (t1StartId == i)
{
t1.Start();
}
if (t2StartId == i)
{
t2.Start();
}
}
t1.Join();
t2.Join();
x = 0;
y = 0;
}
Console.WriteLine("Memory Reordered!!!!!!!!!!!!!!!!!!!!!");
Console.WriteLine("Time Taken: {0}", DateTime.Now - time);
}
static void RunOne()
{
x = 1;
a = y;
}
static void RunTwo()
{
y = 1;
b = x;
}
}
现在从我一直在做的阅读(here, here, here, here)来看,*应该有可能在每个线程完成执行后 a 和 b 都等于 0。有人能证实这一点吗?我的任何操作是否在我不知道的场景下出现了内存障碍,这会阻止重新排序,从而使无限循环继续下去?
我运行在 x86 CPU 上进行此操作,我怀疑这也会对结果产生影响。
编辑:
因此,在 运行 程序多次运行后,它现在正在退出循环:
我仍然不相信这是内存重新排序的结果。
第二个运行:
如此决定性的问题! - 这真的是内存重新排序的结果,还是其他原因?
是的,您的示例演示了内存重新排序。我无法自己复制它,但这可能是我特定设置的结果。
我将使用向下箭头 ↓ 表示易失性读取,使用向上箭头 ↑ 表示易失性写入。将箭头视为推开任何其他读取和写入。只要没有指令通过向下箭头向上和通过向上箭头向下,生成这些内存栅栏的代码就可以自由移动。然而,内存栅栏(箭头)被锁定在它们最初在代码中声明的位置。因此,使用图示的围栏重现您的代码将如下所示。
static void RunOne()
{
↑ // volatile write fence
x = 1;
var register = y;
↓ // volatile read fence
a = register;
}
static void RunTwo()
{
↑ // volatile write fence
y = 1;
var register = x;
↓ // volatile read fence
b = register;
}
所以你可以看到在RunOne
中写入x
和读取y
是可以合法交换的。类似地,在 RunTwo
中,写入 y
和读取 x
也可以合法交换。
您使用 x86 体系结构这一事实对本示例没有影响,因为易失性写入后跟易失性读取是唯一仍然允许在 x86 的强内存模型中交换的安排。