sz *_*ter 67 c++ multithreading thread-safety data-race
我编写了一个简单的多线程程序,如下所示:
static bool finished = false;
int func()
{
size_t i = 0;
while (!finished)
++i;
return i;
}
int main()
{
auto result=std::async(std::launch::async, func);
std::this_thread::sleep_for(std::chrono::seconds(1));
finished=true;
std::cout<<"result ="<<result.get();
std::cout<<"\nmain thread id="<<std::this_thread::get_id()<<std::endl;
}
Run Code Online (Sandbox Code Playgroud)
它通常表现在调试模式下在Visual Studio中或-O0
在GC c和后打印出的结果1
秒钟。但是它卡住了,在“ 释放”模式或中不打印任何内容-O1 -O2 -O3
。
Sch*_*eff 100
UB这是一个访问非原子,非保护变量的线程finished
。您可以通过finished
类型std::atomic<bool>
来解决此问题。
我的解决方法:
#include <iostream>
#include <future>
#include <atomic>
static std::atomic<bool> finished = false;
int func()
{
size_t i = 0;
while (!finished)
++i;
return i;
}
int main()
{
auto result=std::async(std::launch::async, func);
std::this_thread::sleep_for(std::chrono::seconds(1));
finished=true;
std::cout<<"result ="<<result.get();
std::cout<<"\nmain thread id="<<std::this_thread::get_id()<<std::endl;
}
Run Code Online (Sandbox Code Playgroud)
输出:
#include <iostream>
#include <future>
#include <atomic>
static std::atomic<bool> finished = false;
int func()
{
size_t i = 0;
while (!finished)
++i;
return i;
}
int main()
{
auto result=std::async(std::launch::async, func);
std::this_thread::sleep_for(std::chrono::seconds(1));
finished=true;
std::cout<<"result ="<<result.get();
std::cout<<"\nmain thread id="<<std::this_thread::get_id()<<std::endl;
}
Run Code Online (Sandbox Code Playgroud)
有人可能会认为“ bool
大概是一小部分。这怎么可能是非原子的?(我是从多线程开始的。)
但是请注意,缺乏磨练并不是唯一std::atomic
给您带来帮助的东西。它还使来自多个线程的并发读写访问权限得到了明确定义,从而阻止了编译器假设重新读取该变量将始终看到相同的值。
制作bool
不受保护的,非原子的会导致其他问题:
atomic<bool>
使用memory_order_relaxed
存储/加载,但在哪里volatile
不起作用。尽管它实际上可以在实际的C ++实现中使用,但它的可变性将是UB。)为防止这种情况发生,必须明确告知编译器不要这样做。
对于volatile
与该问题潜在关系的不断发展的讨论,我感到有些惊讶。因此,我想花两分钱:
Bal*_*ckk 42
Scheff的答案描述了如何修复您的代码。我想我会添加一些有关这种情况下实际发生情况的信息。
我使用优化级别1()在Godbolt上编译了您的代码-O1
。您的函数编译如下:
func():
cmp BYTE PTR finished[rip], 0
jne .L4
.L5:
jmp .L5
.L4:
mov eax, 0
ret
Run Code Online (Sandbox Code Playgroud)
那么,这里发生了什么?首先,我们进行比较:cmp BYTE PTR finished[rip], 0
-检查是否finished
为假。
If it is not false (aka true) we should exit the loop on the first run. This accomplished by jne .L4
which jumps when not equal to label .L4
where the value of i
(0
) is stored in a register for later use and the function returns.
If it is false however, we move to
.L5:
jmp .L5
Run Code Online (Sandbox Code Playgroud)
This is an unconditional jump, to label .L5
which just so happens to be the jump command itself.
In other words, the thread is put into an infinite busy loop.
So why has this happened?
As far as the optimiser is concerned, threads are outside of its purview. It assumes other threads aren't reading or writing variables simultaneously (because that would be data-race UB). You need to tell it that it cannot optimise accesses away. This is where Scheff's answer comes in. I won't bother to repeat him.
Because the optimiser is not told that the finished
variable may potentially change during execution of the function, it sees that finished
is not modified by the function itself and assumes that it is constant.
The optimised code provides the two code paths that will result from entering the function with a constant bool value; either it runs the loop infinitely, or the loop is never run.
at -O0
the compiler (as expected) does not optimise the loop body and comparison away:
func():
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-8], 0
.L148:
movzx eax, BYTE PTR finished[rip]
test al, al
jne .L147
add QWORD PTR [rbp-8], 1
jmp .L148
.L147:
mov rax, QWORD PTR [rbp-8]
pop rbp
ret
Run Code Online (Sandbox Code Playgroud)
因此,该函数在未优化的情况下仍能正常工作,这里的原子性不足通常不是问题,因为代码和数据类型很简单。我们可能遇到的最坏的情况可能是该值与应有的值i
相差一个。
具有数据结构的更复杂的系统更有可能导致数据损坏或执行不正确。
为了学习曲线的完整性;您应该避免使用全局变量。尽管通过将其静态化,您还是做得很好,因此它对于翻译部门来说是本地的。
这是一个例子:
class ST {
public:
int func()
{
size_t i = 0;
while (!finished)
++i;
return i;
}
void setFinished(bool val)
{
finished = val;
}
private:
std::atomic<bool> finished = false;
};
int main()
{
ST st;
auto result=std::async(std::launch::async, &ST::func, std::ref(st));
std::this_thread::sleep_for(std::chrono::seconds(1));
st.setFinished(true);
std::cout<<"result ="<<result.get();
std::cout<<"\nmain thread id="<<std::this_thread::get_id()<<std::endl;
}
Run Code Online (Sandbox Code Playgroud)
生活在魔盒上