再次不稳定:有必要阻止优化吗?

Once more volatile: necessary to prevent optimization?

我已经阅读了很多关于 'volatile' 关键字的资料,但我仍然没有一个明确的答案。

考虑这段代码:

class A
{
public:
    void work()
    {
        working = true;

        while(working)
        {
            processSomeJob();
        }
    }

    void stopWorking() // Can be called from another thread
    {
        working = false;
    }
private:
    bool working;
}

随着 work() 进入其循环,'working' 的值为真。

这两个是哪个?在谷歌搜索 volatile 的使用时,我发现人们声称它仅在使用 I/O 设备直接写入内存时有用,但我也发现它应该在某个场景中使用像我一样。

Volatile 将使 while 循环在每次检查时重新加载 working 变量。实际上,这通常允许您通过从异步信号处理程序或另一个线程调用 stopWorking 来停止工作函数,但根据标准,这还不够。该标准需要无锁原子volatile sig_atomic_t类型的变量用于sighandler <->常规上下文通信和原子用于线程间通讯.

您的程序优化为无限循环

void foo() { A{}.work(); }

编译为 (g++ with O2)

foo():
        sub     rsp, 8
.L2:
        call    processSomeJob()
        jmp     .L2

该标准定义了假设的抽象机器 将如何处理程序。符合标准的编译器必须编译您的程序,使其在所有可观察行为中都以与该机器相同的方式运行。这被称为 as-if 规则,只要 what 你的程序所做的是相同的,编译器就有自由,而不管 如何.

通常情况下,读取和写入变量并不构成可观察的,这就是编译器可以根据需要省略尽可能多的读取和写入的原因。编译器可以看到 working 没有被分配并优化读取。 volatile 的(经常被误解的)效果恰恰是让它们可观察,这迫使编译器单独留下读取和写入.

但是等你说,另一个线程可能会分配给 working。这就是未定义行为的余地所在。当存在未定义行为时,编译器可能会做 任何事情,包括格式化硬盘驱动器并且仍然符合标准。由于没有同步并且 working 不是原子的,因此写入 working 的任何其他线程都是数据竞争,这是无条件未定义的行为。因此,无限循环唯一错误的情况是存在未定义的行为,编译器由此决定您的程序还不如继续循环。

TL;DR 不要对多线程使用纯 boolvolatile。使用 std::atomic<bool>.

†并非在所有情况下。 void bar(A& a) { a.work(); } 不适用于某些版本。
‡实际上,这里有一些

Now I'm guessing the compiler is allowed to optimize the while(working) to while(true)

可能,是的。但前提是它可以证明 processSomeJob() 不会修改 working 变量,即如果它可以证明循环是无限的。

If this is not the case, that would mean something like this would be quite inefficient ... as the value of someOtherClassMember would have to be loaded each iteration

你的推理很有道理。但是,内存位置可能保留在缓存中,并且从 CPU 缓存中读取不一定会非常慢。如果 doSomething 复杂到足以导致 someOtherClassMember 从缓存中被驱逐,那么我们肯定必须从内存中加载,但另一方面 doSomething 可能非常复杂以至于相比之下,单个内存负载是微不足道的。

Which of these two is the case?

要么。优化器将无法分析所有可能的代码路径;我们不能假设在所有情况下都可以优化循环。但是如果someOtherClassMember在任何代码路径中都被证明没有被修改,那么证明它在理论上是可能的,因此循环在理论上是可以优化的。

but I also find claims that [volatile] should be used in a scenario like mine.

volatile 在这里帮不了你。如果 working 在另一个线程中被修改,则存在数据竞争。而数据竞争意味着程序的行为是未定义的。

为避免数据竞争,您需要同步:使用互斥锁或原子操作来跨线程共享访问。