无序执行和重新排序:我可以在屏障之前看到屏障之后的情况吗?

时间:2015-04-08 16:07:29

标签: multithreading concurrency memory-model

根据维基百科:内存屏障,也称为membar,内存栅栏或栅栏指令,是一种屏障指令,它使中央处理单元(CPU)或编译器对之前发出的内存操作强制执行排序约束在屏障指示之后。 这通常意味着在屏障之前发布的操作可以保证在屏障之后发布操作之前执行。

通常,文章谈论的事情(我将使用监视器而不是membars):

class ReadWriteExample {                                          
    int A = 0;    
    int Another = 0;                                                

    //thread1 runs this method                                    
    void writer () {                                              
      lock monitor1;   //a new value will be stored            
      A = 10;          //stores 10 to memory location A        
      unlock monitor1; //a new value is ready for reader to read
      Another = 20; //@see my question  
    }                                                             

    //thread2 runs this method                                    
    void reader () {                                              
      lock monitor1;  //a new value will be read               
      assert A == 10; //loads from memory location A
      print Another //@see my question           
      unlock monitor1;//a new value was just read              
   }                                                              
}       

但我想知道编译器或cpu是否有可能以一种代码打印20的方式来改变现状?我不需要保证。

即。根据定义,在屏障之前发布的操作不能被编译器推下,但是有可能在屏障之后偶尔会看到屏障之后发出的操作吗? (只是概率)

谢谢

3 个答案:

答案 0 :(得分:2)

我的回答仅涉及Java的内存模型。答案实际上不能用于所有语言,因为每个语言都可能以不同的方式定义规则。

  

但我想知道编译器或cpu是否有可能以一种代码打印20的方式来改变现状?我不需要保证。

您的答案似乎是" A = 20的商店是否可以在解锁显示器上方重新订购?"

答案是,它可以。如果您查看JSR 166 Cookbook,则显示的第一个网格将说明重新排序的工作原理。

writer案例中,第一个操作是MonitorExit,第二个操作将是NormalStore。网格解释说,是的,这个序列被允许重新排序。

这称为Roach Motel排序,即内存访问可以移动到同步块但不能移出


另一种语言怎么样?那么,这个问题太宽泛了,无法回答所有问题,因为每个问题都可能以不同方式定义规如果是这种情况,您需要优化您的问题。

答案 1 :(得分:1)

在Java中,有一个先发生过的概念。您可以在Java Specification中阅读有关它的所有详细信息。 Java编译器或运行时引擎可以重新排序代码,但它必须遵守先前发生的规则。这些规则对于希望详细控制其代码重新排序方式的Java开发人员非常重要。我自己已经被重新排序代码烧毁,结果我通过两个不同的变量引用相同的对象,运行时引擎重新命令我的代码没有意识到操作是在同一个对象上。如果我有一个发生在之前(在两个操作之间)或使用相同的变量,那么就不会发生重新排序。

具体做法是:

  

从以上定义得出:

     

监视器上的解锁发生在该监视器上的每次后续锁定之前。

     

对易失性字段(第8.3.1.4节)的写入发生在每次后续之前   阅读该领域。

     

对线程的start()调用发生在 - 之前的任何操作之前   开始了。

     

线程中的所有操作都会在任何其他线程成功之前发生   从该线程上的join()返回。

     

任何对象的默认初始化发生在任何其他对象之前   程序的动作(默认写入除外)。

答案 2 :(得分:1)

简短回答 - 是的。这是非常依赖编译器和CPU架构的。你在这里有种族条件的定义。调度Quantum不会在指令中间结束(不能对同一位置进行两次写操作)。但是 - 量子可以在指令之间结束 - 加上它们在流水线中无序执行的方式取决于架构(在监视器块之外)。

现在出现了“它依赖”并发症。 CPU保证很少(参见竞争条件)。你也可以看看NUMA(ccNUMA) - 这是一种扩展CPU和CPU的方法。通过将CPU(节点)与本地RAM和组所有者分组来加入内存访问 - 以及节点之间的特殊总线。

监视器不会阻止其他线程运行。它只能阻止它进入监视器之间的代码。因此,当Writer退出监视器部分时,可以自由执行下一个语句 - 无论监视器内是否有其他线程。监视器是阻止访问的门。此外 - 量子可以在A ==语句之后中断第二个线程 - 允许另一个更改值。再次 - 量子不会中断中指令。始终认为线程以完美的并行方式执行。

你如何申请?我现在的英特尔处理器有点过时了(对不起,C#/ Java) - 以及他们的管道如何工作(超线程等)。几年前,我使用了一个名为MIPS的处理器 - 它(通过编译器指令排序)能够执行在分支指令(延迟时隙)之后串行发生的指令。在这个CPU /编译器组合 - 是 - 你所描述的可能发生。如果英特尔提供相同的 - 那么是 - 可能会发生。 Esp与NUMA(英特尔和AMD都有这个,我最熟悉AMD的实现)。

我的观点 - 如果线程在NUMA节点上运行 - 并且访问公共内存位置则可能发生。当然,操作系统会努力在同一节点内安排操作。

你可以模拟这个。我知道MS上的C ++允许访问NUMA技术(我玩过它)。看看你是否可以在两个节点之间分配内存(将A放在一个节点上,将Another放在另一个节点上)。安排线程在特定节点上运行。

此模型中发生的情况是RAM有两条路径。我想这不是你想到的 - 可能只有一个路径/节点模型。在这种情况下,我回到上面描述的MIPS模型。

我假设处理器中断 - 还有一些处理器具有Yield模型。