举个例子:
#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的结果。我们如何保证行为或指令可重复地重新排序?如何具体更改代码示例?
发布于 2019-07-16 15:08:19
首先,您是在UB的土地上,因为有一个竞争条件:flag和a都是从不同的线程中写入和读取的,没有适当的同步--这始终是一个数据竞赛。当您为实现提供这样的程序时,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++代码--在“天真”的解释中--总是有a == 5而不是flag == 1。
当然,while循环会“消耗时间”,并给其他线程一个交织的机会-- sleep或其他系统调用通常会构成编译器的内存屏障,并可能破坏第二个片段的效果。
https://stackoverflow.com/questions/57059332
复制相似问题