为什么volatile和MemoryBarrier不会阻止操作重新排序?
如果我正确理解volatile和MemoryBarrier的含义,那么下面的程序永远无法显示任何结果。
它每次运行时都会捕获写操作的重新排序。 如果我在Debug或Release中运行它并不重要。 如果我将它作为32位或64位应用程序运行也没关系。
为什么会这样?
using System; using System.Threading; using System.Threading.Tasks; namespace FlipFlop { class Program { //Declaring these variables as volatile should instruct compiler to //flush all caches from registers into the memory. static volatile int a; static volatile int b; //Track a number of iteration that it took to detect operation reordering. static long iterations = 0; static object locker = new object(); //Indicates that operation reordering is not found yet. static volatile bool continueTrying = true; //Indicates that Check method should continue. static volatile bool continueChecking = true; static void Main(string[] args) { //Restarting test until able to catch reordering. while (continueTrying) { iterations++; var checker = new Task(Check); var writter = new Task(Write); lock (locker) { continueChecking = true; checker.Start(); } writter.Start(); checker.Wait(); writter.Wait(); } Console.ReadKey(); } static void Write() { //Writing is locked until Main will start Check() method. lock (locker) { //Using memory barrier should prevent opration reordering. a = 1; Thread.MemoryBarrier(); b = 10; Thread.MemoryBarrier(); b = 20; Thread.MemoryBarrier(); a = 2; //Stops spinning in the Check method. continueChecking = false; } } static void Check() { //Spins until finds operation reordering or stopped by Write method. while (continueChecking) { int tempA = a; int tempB = b; if (tempB == 10 && tempA == 2) { continueTrying = false; Console.WriteLine("Caught when a = {0} and b = {1}", tempA, tempB); Console.WriteLine("In " + iterations + " iterations."); break; } } } } }
我不认为这是重新订购。
这段代码根本不是线程安全的:
while (continueChecking) { int tempA = a; int tempB = b; ...
我认为这种情况是可能的:
-
int tempA = a;
使用最后一个循环的值执行(a == 2) - Write线程有一个上下文切换
-
b = 10
,循环停止 - Check线程有一个上下文切换
-
int tempB = b;
以b == 10执行
我注意到对MemoryBarrier()的调用增加了这种情况的可能性。 可能是因为它们会导致更多的上下文切换。
你不是在测试之间清理变量,所以(除了第一个之外的所有变量)最初a
是2
, b
是20
– 在 Write
完成任何事情 之前 。
Check
可以获得a
初始值(因此tempA
为2
),然后Write
可以进入,直到将b
更改为10
。
现在Check
读取b
(因此tempB
为10
)。
瞧瞧。 重新订购无需重新订购。
在运行之间将a
和b
重置为0
,我预计它会消失。
编辑:已确认; “按原样”我几乎立即得到了这个问题(<2000次迭代); 但通过添加:
while (continueTrying) { a = b = 0; // reset <======= added this
它然后循环任何时间没有任何问题。
或作为流程:
Write A= B= Check (except first run) 2 20 int tempA = a; a = 1; 1 20 Thread.MemoryBarrier(); b = 10; 1 10 int tempB = b;
结果与重新排序,记忆障碍或易失性无关。 需要所有这些结构以避免编译器或CPU重新排序指令的影响。
但是,即使假设完全一致的单CPU内存模型并且没有编译器优化,该程序也会产生相同的结果。
首先,请注意将并行启动多个Write()
任务。 由于Write()内部的lock(),它们按顺序运行,但是一个Signle Check()
方法可以读取由Write()
任务的不同实例产生的a
和b
。
因为Check()
函数与Write
函数没有同步 – 它可以在两个任意和不同的时刻读取a
和b
。 您的代码中没有任何内容阻止Check()
在一个时刻读取前一个Write()
生成的内容,然后在另一个时刻读取由Write()
生成的b
。 首先,你需要在Check()
同步(锁定),然后你可能 (但可能不在这种情况下)需要内存屏障和volatile以解决内存模型问题。
这就是你所需要的:
int tempA, tempB; lock (locker) { tempA = a; tempB = b; }
-
如果您在
MemoryBarrier
中使用MemoryBarrier
,为什么不在checker
执行此操作? 把Thread.MemoryBarrier();
在int tempA = a;
之前int tempA = a;
。 -
调用
Thread.MemoryBarrier();
这么多次阻止了该方法的所有优点。 在a = 1;
之前或之后只调用一次a = 1;
。