多线程程序停留在优化模式下,但在-O0下正常运行

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中-O0GC 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)

在coliru上进行现场演示


有人可能会认为“ bool大概是一小部分。这怎么可能是非原子的?(我是从多线程开始的。)

但是请注意,缺乏磨练并不是唯一std::atomic给您带来帮助的东西。它还使来自多个线程的并发读写访问权限得到了明确定义,从而阻止了编译器假设重新读取该变量将始终看到相同的值。

制作bool不受保护的,非原子的会导致其他问题:

  • 编译器可能会决定将变量优化到一个寄存器中,甚至将CSE多次访问优化到一个寄存器中,并减轻循环负担。
  • 该变量可能已为CPU内核缓存。(在现实生活中,CPU具有一致的缓存。这不是一个真正的问题,但是C ++标准足够宽松,无法在非一致共享内存上假设的C ++实现在哪里可以atomic<bool>使用memory_order_relaxed存储/加载,但在哪里volatile不起作用。尽管它实际上可以在实际的C ++实现中使用,但它的可变性将是UB。)

为防止这种情况发生,必须明确告知编译器不要这样做。


对于volatile与该问题潜在关系的不断发展的讨论,我感到有些惊讶。因此,我想花两分钱:

  • @PeterCordes使用`volatile`仍然是UB。您真的不应该假设UB是绝对安全的,只是因为您无法想到它可能会出错并且在您尝试时有效的方法。那使人们一遍又一遍地被烧死。 (5认同)
  • 我看了一下`func()`,以为“我可以优化它了”。优化器根本不关心线程,并且会检测到无限循环,并乐意将其变成“ while(True)”。如果我们查看https://godbolt.org/z/Tl44iN,我们可以看到这一点。如果完成是“ True”,则返回。如果没有,它将在标签“ .L5”处无条件跳回自身(无限循环)。 (4认同)
  • @val号[SO:何时在多线程中使用volatile?](/sf/answers/319062201/) (3认同)
  • @val:基本上没有理由在C ++ 11中滥用`volatile`,因为您可以通过`atomic &lt;T&gt;`和`std :: memory_order_relaxed`获得相同的asm。它确实可以在真正的硬件上运行:缓存是连贯的,因此一旦另一个内核上的存储提交到那里缓存,装入指令就无法继续读取过时的值。(MESI) (2认同)
  • @Damon互斥体具有发布/获取语义。如果互斥锁已被锁定,则不允许编译器优化读取,因此使用`std :: mutex`保护`finished`起作用(没有`volatile`或`atomic`)。实际上,您可以将所有原子替换为“简单”值+互斥锁方案;它仍然可以工作,但速度较慢。允许atomic &lt;T&gt;使用内部互斥体;仅保证“ atomic_flag”是无锁的。 (2认同)

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相差一个。

具有数据结构的更复杂的系统更有可能导致数据损坏或执行不正确。

  • C ++ 11确实使线程和线程感知的内存模型成为语言本身的一部分。这意味着编译器甚至无法在不编写那些非原子变量的代码中发明写操作。例如`if(cond)foo = 1;`不能转换成类似于`foo = cond的asm吗?1:foo;`,因为该加载+存储(不是原子RMW)可能会踩到另一个线程的写操作。编译器已经在避免这样的事情,因为它们希望对编写多线程程序有用,但是C ++ 11正式宣布编译器必须不要破坏2个线程分别写“ a [1]”和“ a [2]的代码]` (3认同)
  • 但是,是的,除了关于编译器如何根本不了解线程的高估之外,您的答案是正确的。Data-race UB可以提升非原子变量的负载,包括全局变量,以及我们希望对单线程代码进行的其他积极优化。电子学上的[MCU编程-C ++ O2优化在循环时中断](//electronics.stackexchange.com/a/387478)。SE是我的解释版本。 (2认同)

Obl*_*ica 5

为了学习曲线的完整性;您应该避免使用全局变量。尽管通过将其静态化,您还是做得很好,因此它对于翻译部门来说是本地的。

这是一个例子:

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)

生活在魔盒上