多线程程序卡在优化模式但在-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-O0gcc 中的调试模式下正常运行并打印出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

Live Demo on coliru


有人可能会认为'这是 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