Open Liam0205 opened 5 years ago
回复 @longjianjiang 在 https://github.com/Liam0205/liam0205.github.io/issues/283#issue-417713862 中提出的问题。
std::atomic<bool>
和原生的 bool
不同之处不仅在于对 std::atomic<bool>
的读写操作是有原子性保证的,还在于 std::atomic<bool>
提供了内存屏障。其中 std::atomic<bool>
的 operator=
运算符,相当于 std::atomic<bool>::store(bool flag, std::memory_order_seq_cst)
。这里保证了不会乱序。
线程安全问题,是一个「烫手山芋」,你不能狭隘地理解为数据竞争就是线程安全的全部。线程安全的本质,是在单线程下对变量状态的合理假设,在多线程状态下由于编译器优化、CPU 乱序执行、线程调度等因素而被打破。数据竞争只是其中一种可能的结果罢了。
——事实上,你可以直接在文章最下面评论的。close #283
受教了,多谢 @Liam0205 。
对了,我这里访问文章,没有看到可以评论的地方呢。
@longjianjiang
奇怪,大概是 Gitalk 没有正确加载……
我这里看是这样的:
-- 此处说的「读取内存」,包括了读取 CPU 缓存和读取计算机主存。 也就说如果是2个跑在2个cpu的线程,一个线程写,一个线程读,那就算加了volatile,也有可能读写在各自的CPU缓存,而不能马上得到最新的值吗?
@wclin88 那不会的。缓存一致性由缓存一致性协议保证,比如 MESI 协议。跟这里讨论的是两回事。
@Liam0205 @wclin88 那不会的。缓存一致性由缓存一致性协议保证,比如 MESI 协议。跟这里讨论的是两回事。
好的,谢谢。我看你上边的flag的例子是因为需要一定的顺序才会有问题,那如果我有一个线程负责对一个数据写,其他的线程负责读这个数据,至于读的是新数据还是旧数据是无关紧要的,那加volatile应该就可以了,不需要用atomic吧
有点疑惑,例子一中说”在 thread1 中,flag = false 赋值之后,在 while 死循环里,没有任何机会修改 flag 的值“,但是自己实现了个能运行的,并没测试出什么问题,请问是我哪里理解有误呢?
bool flag = false;
using Type=int;
void test_volatile_demo()
{
flag = false;
Type* value = new Type(1);
auto func_thread2 = [](Type* value)->void {
// do some evaluations
cout<<"func_thread2 begin "<<endl;
*value = 222;
flag = true;
};
std::thread t(func_thread2,value);
while (true)
{
if (flag == true)
{
cout<<"apply value:"<<*value<<endl;
break;
}
}
t.join();
if (nullptr != value)
{
delete value;
}
}
int main()
{
for(unsigned int i =0 ;i<100000000;i++)
{
test_volatile_demo();
}
return 0;
}
"编译器仍有可能在优化时将 thread2 中的 update 和对 flag 的赋值交换顺序",请问换成atomic
@omlib-lin
换成 std:;atomic
可以解决这个问题。std::atomic
对内存屏障可以有更精细的控制。
由于对 std::atomic
的操作是原子的,同时构建了良好的内存屏障,因此整个代码的行为在标准下是良定义的。
我想请教以下,换成原子操作之后就可以保证flag=true在update()之后运行么,是否原子操作可以解决cpu运行乱序问题?
class StopExample { public: void Start() { _thrd = std::thread([=](){ while(!_stop) { std::cout << "Running" << std::endl; std::this_thread::sleep_for(std::chrono::seconds(1)); } }); }
void Stop()
{
_stop = true;
_thrd.join();
std::cout << "Stopped" << std::endl;
}
private: std::thread _thrd; bool _stop{false}; }; 你好,我编写了如上的代码。 在gcc7.5和vs2015下在gcc7.5和vs2015下,发现编译器并没有对_stop执行优化(优化到寄存器作为条件测试变量)。是标准有修改么,求大神解惑
std::atomic和std::mutex都是C++11新增的内容,如果是写C++,那么使用C++11是值得推广的。不过文章标题提到的是C和C++,C语言中的具体解法有所不同。
std::atomic和std::mutex都是C++11新增的内容,如果是写C++,那么使用C++11是值得推广的。不过文章标题提到的是C和C++,C语言中的具体解法有所不同。
你讲的对,我回头改改。
std::atomic<bool> flag = false;
这一用法,在clang10下会报错。建议改成std::atomic<bool> flag(false);
@zchrissirhcz
std::atomic<bool> flag = false;
这一用法,在clang10下会报错。建议改成std::atomic<bool> flag(false);
fixed.
@caonann 有点疑惑,例子一中说”在 thread1 中,flag = false 赋值之后,在 while 死循环里,没有任何机会修改 flag 的值“,但是自己实现了个能运行的,并没测试出什么问题,请问是我哪里理解有误呢?
bool flag = false; using Type=int; void test_volatile_demo() { flag = false; Type* value = new Type(1); auto func_thread2 = [](Type* value)->void { // do some evaluations cout<<"func_thread2 begin "<<endl; *value = 222; flag = true; }; std::thread t(func_thread2,value); while (true) { if (flag == true) { cout<<"apply value:"<<*value<<endl; break; } } t.join(); if (nullptr != value) { delete value; } } int main() { for(unsigned int i =0 ;i<100000000;i++) { test_volatile_demo(); } return 0; }
你不用疑惑,是作者自己搞错了。实例代码中的全局变量的if判断,是不会编译优化掉的,因为这是全局变量,在多线程环境下内容不可控,不保证不变,编译器自己无法推断(如果真的优化掉了,那肯定是编译器的bug)。编译优化并不是任何条件都会触发的,需要满足特定的条件(更多内容参考编译原理教课书)。另外,关于作者说的第二点“update和flag赋值顺序”交换问题,多半也是有问题的。虽然我没有验证过,但是可以肯定的是,编译器并不会随便交换函数调用和flag赋值,除非flag是栈变量(也就是线程独占变量)。总之编译优化是有条件的,不是随便生效的。另外,绝大部分情况下,编译优化的设计上是让应用开发程序员不感知的。像这么简单的例子如果都有问题了,那应用程序员就不用写代码了,肯定一写一个错。但是作者的结论应该是正确的,只是例子举得可能没有说服力。
作者给的第一个sample code后面的2点评论,我理解应该是不对的把。
1,关于if条件优化掉的问题,对于全局变量肯定不会发生,不管是在什么编译器版本上,因为编译器无法推断出全局变量的不变性,所以不会无端认为全局变量不变。(想了一下,加个条件,毕竟没有验证过,可以肯定的是:涉及到写的全局变量,肯定不会把if条件优化掉)
2,关于函数调用和变量赋值的乱序问题,也是不对的。对于编译优化,只有当编译器能够确定函数调用和赋值操作无关的情况下,才可能触发交换顺序。而实际上,编译器根本无法推断出函数调用和赋值操作的关系(是否互相影响),所以保守设计上,肯定不会选择交换顺序。就文中的例子而言,因为flag是全局变量,所以肯定不会交换顺序的(因为前面的函数调用显然也可以访问到这个全局变量,从而有机会影响flag的取值,如果编译器还交换顺序的话,那就大错了)。我可以给一个典型的反例:如果,上述代码放在单线程的程序中,那么也会有flag和函数调用的顺序交换吗?这显然是不可能的!如果如此,那么现在世界上的绝大部分的程序都会运行错误。
3,最后提一下cpu的乱序问题。在100%的情况下(除非极其特殊的场合),cpu的乱序不影响程序语义。这是cpu乱序设计的前提。换句话说,作为应用程序开发程序员,不应该考虑cpu乱序问题。同样的反例:如果真的有影响,那么历史上老的程序岂不是都不能正确跑了。显然,任何物理cpu都不会如此设计。
总结:作者给的结论应该是对的,只是举了一个不合适的例子,不知道能不能找到一个真实的例子。
因为像flag检查这样的代码,在程序开发中非常常见,所以,我有个问题,到底能不能这样用?似乎不用volatile程序也可以正常运行。我们到底可不可以这样写代码呢?能不能找一个使这样的代码失效的c++代码啊?作者大佬!
C++ primer (第五版)对volatile的解释是:volatile的确切含义和当前的机器有关,当对象在程序控制或检测之外的可能会被改变时,如果不希望这种改变,可以将变量声明为volatile
帮作者平反一下,最后一段代码其实作者提到的观念没有问题,分为两个部分:
Q1: if (flag == true) 的优化
Q2: flag = true; 执行顺序的优化
这里我需要把代码稍微修改一下比较合适,也可以简化问题:
#include <stdio.h>
#include <thread>
// global shared data
bool flag = false;
void thread2(int* value) {
// do some evaluations
int var = 0;
var++;
*value = var + 1;
flag = true; //Q2
return;
}
void thread1() {
flag = false;
int value = 0;
std::thread t2(thread2, &value);
while (true) {
if (flag == true) { // Q1
printf("%d\n", value);
break;
}
}
t2.join();
return;
}
int main()
{
thread1();
return 0;
}
这段代码使用 g++ -g test.cpp -o test -lpthread
编译一切正常;
但 g++ -g test.cpp -O2 -o test -lpthread
启用优化编译后就会卡死在while回圈里出不来。
针对Q1: if (flag == true)
,编译器的确认为flag在while回圈中被判断一次后再也不会用到,于是就把整段if忽略。
我们可以通过asm代码来分析while那一段。
优化前:
g++ -S -fverbose-asm test.cpp -lpthread
.L15:
# test.cpp:21: if (flag == true) { // Q1
movzbl flag(%rip), %eax # flag, flag.0_1
movzbl %al, %eax # flag.0_1, _2
# test.cpp:21: if (flag == true) { // Q1
cmpl $1, %eax #, _2
jne .L15 #,
# test.cpp:22: printf("%d\n", value);
movl -44(%rbp), %eax # value, value.1_3
movl %eax, %esi # value.1_3,
leaq .LC0(%rip), %rdi #,
movl $0, %eax #,
.LEHB1:
call printf@PLT #
# test.cpp:26: t2.join();
leaq -40(%rbp), %rax #, tmp89
movq %rax, %rdi # tmp89,
call _ZNSt6thread4joinEv@PLT #
优化后:
g++ -S -fverbose-asm test.cpp -O2 -lpthread
.L8:
# test.cpp:21: if (flag == true) { // Q1
cmpb $0, flag(%rip) #, flag
jne .L28 #,
.L25:
jmp .L25 #
.p2align 4,,10
.p2align 3
.L28:
# /usr/include/x86_64-linux-gnu/bits/stdio2.h:107: return __printf_chk (__USE_FORTIFY_LEVEL - 1, __fmt, __va_arg_pack ());
movl 4(%rsp), %edx # value,
leaq .LC0(%rip), %rsi #,
movl $1, %edi #,
xorl %eax, %eax #
.LEHB2:
call __printf_chk@PLT #
# test.cpp:26: t2.join();
movq %rbp, %rdi # tmp112,
call _ZNSt6thread4joinEv@PLT #
可以看到cmp结束后就一直卡在这里跳不出去
.L25:
jmp .L25 #
证实了编译器的优化可能会造成问题。
再来说明一下Q2代码改写的原因
var++;
*value = var + 1;
flag = true; //Q2
因为*value = var + 1;
需要等待var++;
执行完才能执行,在此之前可能会被阻塞,
CPU可能会为了不浪费等待时间而重排序,让flag = true;
先执行,执行完之后也许*value = var + 1;
就得以执行了。
但我在这个案例中没有观察到CPU乱序或编译器乱序的现象,可能还需要改代码。
但我在这个案例中没有观察到CPU乱序或编译器乱序的现象,可能还需要改代码。
在 x86 或者 x64 上,你看不到这种现象。CPU OOO 是有条件的,根据 CPU arch 的不同而不同。x86/x64 不允许这种类型的乱序。
https://liam.page/2018/01/18/volatile-in-C-and-Cpp/
最近在讨论多线程编程中的一个可能的 false sharing 问题时,有人提出加 volatile 可能可以解决问题。这种错误的认识荼毒多年,促使我写下这篇文章。