举个例子:
#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的结果。我们如何保证行为或指令可重复地重排?如何专门更改代码示例?
答案 0 :(得分:4)
首先:由于处于竞争状态,因此您位于UB之内:flag
和a
都是从不同线程写入和读取的,没有适当的同步-这始终是数据竞争。当您向实施者提供此类程序时,它们会涉及C ++标准does not impose any requirements。
因此无法“保证”特定行为。
但是,我们可以查看程序集输出以确定给定的编译程序可以做什么或不能做什么。我没有成功地单独使用重排序来显示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中看到的那样,即使线程2的原始C ++代码在“天真”的解释中始终具有{{1},该程序仍将始终输出0(或不终止)。 },而a == 5
。
flag == 1
循环当然是为了“消耗时间”,并为另一个线程提供交织的机会-while
或其他系统调用通常会构成编译器的内存障碍,并且可能会破坏第二个片段的效果。