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

How to write observable example for instruction reorder?

举个例子:

#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++ 标准 does not impose any requirements 实现。

因此无法"guarantee"特定行为。

但是,我们可以查看汇编输出以确定给定的编译程序可以做什么或不能做什么。我没有成功单独使用重新排序来显示 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(其他输出也是可能的,或者程序可能根本不会终止"unlucky"调度,但没有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++ 代码在 "naive" 解释中始终具有a == 5flag == 1.

while循环当然是为了"burn time"给其他线程一个交错的机会——sleep或者其他系统调用一般会构成内存编译器的障碍,可能会破坏第二个片段的效果。