多线程程序卡在优化模式但在-O0下正常运行
Multithreading program stuck in optimized mode but runs normally in -O0
我写了一个简单的多线程程序如下:
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;
}
它在 Visual studio 或 -O0
在 gcc 中的调试模式下正常运行并打印出1
秒后的结果。但它卡住了,在 Release 模式或 -O1 -O2 -O3
.
模式下不打印任何内容
访问非原子、非保护变量的两个线程 U.B. 这涉及 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;
}
输出:
result =1023045342
main thread id=140147660588864
有人可能会认为'这是 bool
– 可能是一位。这怎么可能是非原子的? (我自己开始使用多线程时就这样做了。)
但请注意,不撕裂并不是 std::atomic
给您的唯一东西。它还使来自多个线程的并发读+写访问得到明确定义,阻止编译器假设重新读取变量将始终看到相同的值。
制作 bool
无人看守的非原子会导致其他问题:
- 编译器可能会决定将变量优化到一个寄存器中,甚至将 CSE 多路访问优化到一个寄存器中,并将负载提升到循环之外。
- 变量可能被缓存用于 CPU 内核。 (在现实生活中,CPUs have coherent caches。这不是真正的问题,但 C++ 标准足够宽松,可以涵盖非一致性共享内存上假设的 C++ 实现,其中
atomic<bool>
和 memory_order_relaxed
store/load 会工作,但 volatile
不会。为此使用 volatile 将是 UB,即使它在实际 C++ 实现中工作。)
为防止这种情况发生,必须明确告知编译器不要这样做。
我对有关 volatile
与此问题的潜在关系的不断发展的讨论感到有点惊讶。因此,我愿意花掉我的两分钱:
Scheff 的回答描述了如何修复您的代码。我想我会添加一些关于这种情况下实际发生的信息。
我使用优化级别 1 (-O1
) 在 godbolt 编译了您的代码。你的函数像这样编译:
func():
cmp BYTE PTR finished[rip], 0
jne .L4
.L5:
jmp .L5
.L4:
mov eax, 0
ret
那么,这里发生了什么?
首先,我们进行比较:cmp BYTE PTR finished[rip], 0
- 这会检查 finished
是否为假。
如果它是 not false(又名 true),我们应该在第一个 运行 时退出循环。这是通过 jne .L4
实现的,当 not e 等于标签 .L4
其中 i
(0
) 的值存储在寄存器中供以后使用,函数 returns.
如果 是 false,我们将移至
.L5:
jmp .L5
这是一个无条件跳转,标签.L5
恰好是跳转命令本身。
换句话说,线程进入了无限忙循环。
为什么会这样?
就优化器而言,线程不在其权限范围内。它假设其他线程没有同时读取或写入变量(因为那将是数据争用 UB)。您需要告诉它它无法优化访问。这就是 Scheff 的答案所在。我不会重复他的话。
因为优化器没有被告知 finished
变量在函数执行期间可能会发生变化,所以它认为 finished
没有被函数本身修改,并假定它是常量。
优化后的代码提供了两个代码路径,它们将通过使用常量 bool 值输入函数而产生;要么 运行 无限循环,要么循环永远不会 运行.
在 -O0
编译器(如预期的那样)没有优化循环体和比较:
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
因此,函数在未优化时确实有效,这里缺乏原子性通常不是问题,因为代码和数据类型很简单。我们可能 运行 最糟糕的是 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;
}
生活在 wandbox
我写了一个简单的多线程程序如下:
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;
}
它在 Visual studio 或 -O0
在 gcc 中的调试模式下正常运行并打印出1
秒后的结果。但它卡住了,在 Release 模式或 -O1 -O2 -O3
.
访问非原子、非保护变量的两个线程 U.B. 这涉及 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;
}
输出:
result =1023045342
main thread id=140147660588864
有人可能会认为'这是 bool
– 可能是一位。这怎么可能是非原子的? (我自己开始使用多线程时就这样做了。)
但请注意,不撕裂并不是 std::atomic
给您的唯一东西。它还使来自多个线程的并发读+写访问得到明确定义,阻止编译器假设重新读取变量将始终看到相同的值。
制作 bool
无人看守的非原子会导致其他问题:
- 编译器可能会决定将变量优化到一个寄存器中,甚至将 CSE 多路访问优化到一个寄存器中,并将负载提升到循环之外。
- 变量可能被缓存用于 CPU 内核。 (在现实生活中,CPUs have coherent caches。这不是真正的问题,但 C++ 标准足够宽松,可以涵盖非一致性共享内存上假设的 C++ 实现,其中
atomic<bool>
和memory_order_relaxed
store/load 会工作,但volatile
不会。为此使用 volatile 将是 UB,即使它在实际 C++ 实现中工作。)
为防止这种情况发生,必须明确告知编译器不要这样做。
我对有关 volatile
与此问题的潜在关系的不断发展的讨论感到有点惊讶。因此,我愿意花掉我的两分钱:
Scheff 的回答描述了如何修复您的代码。我想我会添加一些关于这种情况下实际发生的信息。
我使用优化级别 1 (-O1
) 在 godbolt 编译了您的代码。你的函数像这样编译:
func():
cmp BYTE PTR finished[rip], 0
jne .L4
.L5:
jmp .L5
.L4:
mov eax, 0
ret
那么,这里发生了什么?
首先,我们进行比较:cmp BYTE PTR finished[rip], 0
- 这会检查 finished
是否为假。
如果它是 not false(又名 true),我们应该在第一个 运行 时退出循环。这是通过 jne .L4
实现的,当 not e 等于标签 .L4
其中 i
(0
) 的值存储在寄存器中供以后使用,函数 returns.
如果 是 false,我们将移至
.L5:
jmp .L5
这是一个无条件跳转,标签.L5
恰好是跳转命令本身。
换句话说,线程进入了无限忙循环。
为什么会这样?
就优化器而言,线程不在其权限范围内。它假设其他线程没有同时读取或写入变量(因为那将是数据争用 UB)。您需要告诉它它无法优化访问。这就是 Scheff 的答案所在。我不会重复他的话。
因为优化器没有被告知 finished
变量在函数执行期间可能会发生变化,所以它认为 finished
没有被函数本身修改,并假定它是常量。
优化后的代码提供了两个代码路径,它们将通过使用常量 bool 值输入函数而产生;要么 运行 无限循环,要么循环永远不会 运行.
在 -O0
编译器(如预期的那样)没有优化循环体和比较:
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
因此,函数在未优化时确实有效,这里缺乏原子性通常不是问题,因为代码和数据类型很简单。我们可能 运行 最糟糕的是 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;
}
生活在 wandbox