如何编写指令重新排序的可观察示例?

How to write observable example for instruction reorder?

本文关键字:观察 排序 指令 何编写 新排序      更新时间:2023-10-16

举个例子:

#include <thread>
#include <iostream>
int main() {
int a = 0;
volatile int flag = 0;
std::thread t1([&]() {
while (flag != 1);
int b = a;
std::cout << "b = " << b << std::endl;
});
std::thread t2([&]() {
a = 5;
flag = 1;
});
t1.join();
t2.join();
return 0;
}

从概念上讲,flag = 1;可以在a = 5;之前重新排序和执行,因此b的结果可以是5或0。

但是,实际上,我无法在我的机器上产生输出 0 的结果。我们如何保证行为或指令的可重复排序?如何具体更改代码示例?

首先:你处于 UB 的土地上,因为有一个竞争条件:flaga都是在没有适当同步的情况下从不同的线程写入和读取的 - 这始终是一场数据竞赛。当您为实现提供这样的程序时,C++标准不会对实现施加任何要求。

因此,没有办法"保证"特定行为。

但是,我们可以查看程序集输出来确定给定的编译程序可以或不能做什么。我没有成功地单独使用重新排序来显示volatile作为同步机制的问题,但下面是使用相关优化的演示。


下面是一个没有数据争用的程序示例:

std::atomic<int> a = 0;
std::atomic<int> flag = 0;
std::thread t1([&]() {
while (flag != 1);
int b = a;
std::cout << "b = " << b << std::endl;
});
std::thread t2([&]() {
a = 5;
int x = 1000000;
while (x-- > 1) flag = 0;
flag = 1;
x = 1000000;
while (x-- > 1) flag = 1;
flag = 0;
a = 0;
});
t1.join();
t2.join();

https://wandbox.org/permlink/J1aw4rJP7P9o1h7h

事实上,该程序的通常输出是b = 5(其他输出是可能的,或者程序可能根本不会因"不幸"调度而终止,但没有 UB)。


如果我们改用不正确的同步,我们可以在程序集中看到此输出不再处于可能性范围内(考虑到 x86 平台的保证):

int a = 0;
volatile int flag = 0;
std::thread t1([&]() {
while (flag != 1);
int b = a;
std::cout << "b = " << b << std::endl;
});
std::thread t2([&]() {
a = 5;
int x = 1000000;
while (x-- > 1) flag = 0;
flag = 1;
x = 1000000;
while (x-- > 1) flag = 1;
flag = 0;
a = 0;
});
t1.join();
t2.join();

第二个螺纹主体的组件,如下所示 https://godbolt.org/z/qsjca1:

std::thread::_State_impl<std::thread::_Invoker<std::tuple<main::{lambda()#2}> > >::_M_run():
mov     rcx, QWORD PTR [rdi+8]
mov     rdx, QWORD PTR [rdi+16]
mov     eax, 999999
.L4:
mov     DWORD PTR [rdx], 0
sub     eax, 1
jne     .L4
mov     DWORD PTR [rdx], 1
mov     eax, 999999
.L5:
mov     DWORD PTR [rdx], 1
sub     eax, 1
jne     .L5
mov     DWORD PTR [rdx], 0
mov     DWORD PTR [rcx], 0
ret

请注意a = 5;是如何完全优化的。在编译的程序中,a没有任何地方有机会将值5

正如您在 https://wandbox.org/permlink/Pnbh38QpyqKzIClY 中看到的那样,程序将始终输出 0(或不终止),即使线程 2 的原始C++代码 - 在"朴素"解释中 - 在flag == 1时总是有a == 5

while循环当然是为了"消耗时间",并给另一个线程一个交错的机会 -sleep或其他系统调用通常会构成编译器的内存屏障,并可能破坏第二个代码片段的效果。