多线程和OOO执行。

时间:2016-07-01 10:07:37

标签: c++ multithreading memory x86

int main() {
    int  f = 0, x=0;

    std::thread *t  = new std::thread([&f, &x](){ while( f == 0); std::cout << x << endl;});
    std::thread *t2 = new std::thread([&f, &x](){ x = 42; f = 1;});

    t->join();
    t2->join();
    return 0;

}

据我所知,理论上可以将stdout等于0反对我们的直觉(因此我们期待42。但是,CPU可以执行无序指令并且在事实上,可以按顺序执行程序:

(我们假设我们的CPU中有> 1个核心)

因此,第二个核心上的thread#2首先执行(因为OOO meachanism)f = 1然后,thread#1在第一个核心上执行第一个程序:while( f == 0); std::cout << x << endl。因此,输出为0

我试图得到这样的输出,但我总是得到42。我运行该程序1000000次,结果总是相同= 42

(我知道它不安全,有数据竞争)。

我的问题是:

  1. 我是对还是错了?为什么?
  2. 如果我是对的,是否可以强制将输出等于0
  3. 如何安全地使用此代码?我知道互斥锁/信号量,我可以使用互斥锁保护f,但我听说过有关内存栅栏的内容,请多说一遍。

1 个答案:

答案 0 :(得分:4)

  

但是,CPU可以执行乱序指令,事实上,可以按顺序执行程序:

无序执行不同于加载/存储何时全局可见的重新排序。 OoOE保留了编程按顺序运行的错觉。没有OoOE就可以重新排序内存。即使是有序的流水线核心也希望缓冲其商店。请参阅parts of this answer, for example

  

如果我是对的,是否可以强制将输出等于0?

不在x86上,which only does StoreLoad reordering,而不是StoreStore重新排序。如果是compiler reorders the stores to x and f at compile time,那么在看到x==0之后,您有时会看到f==1。否则你永远不会看到它。

在生成thread2之前产生thread1之后的短暂睡眠还会确保在修改它之前thread1在x上旋转。那么你不需要thread2,并且实际上可以从主线程中进行存储。

看一下Jeff Preshing的Memory Reordering Caught In The Act,看一下在x86上观察运行时内存重新排序的真实程序,在Nehalem上每6k次迭代一次。

在一个弱有序的架构上,你可能会看到StoreStore在运行时重新排序,就像你的测试程序一样。但是你可能不得不安排变量在不同的缓存行中!而且你需要在一个循环中进行测试,而不是每个程序调用一次。

  

如何安全地使用此代码?我知道互斥锁/信号量,我可以用互斥锁保护f,但我听说过有关内存栅栏的内容,请多说一遍。

使用C++11 std::atomic获取fstd::atomic<uin32t_t> f; // flag to indicate when x is ready uint32_t x; ... // don't use new when a local with automatic storage works fine std::thread t1 = std::thread([&f, &x](){ while( f.load(std::memory_order_acquire) == 0); std::cout << x << endl;}); // or sleep a few ms, and do t2's work in the main thread std::thread t2 = std::thread([&f, &x](){ x = 42; f.store(1, std::memory_order_release);}); 的访问权限。

f = 1

MFENCE之类的默认内存排序是mo_seq_cst,它需要x86上的f,或者其他架构上的等价昂贵的屏障。

在x86上,较弱的内存排序只会阻止编译时重新排序,但不需要任何屏障指令。

std :: atomic还可以阻止编译器将while的加载从thread1中的volatile循环中提取出来,就像@Baum的评论所描述的那样。 (因为atomic有像{{1}}这样的语义,假定存储的值可以异步更改。由于数据争用是未定义的行为,编译器通常可以从循环中提升负载,除非别名分析无法通过指针证明存储循环内部无法修改该值。)。