29

在 C++11 标准中,机器模型从单线程机器变为多线程机器。

这是否意味着static int x; void func() { x = 0; while (x == 0) {} }在 C++11 中将不再发生优化输出读取的典型示例?

编辑:对于那些不知道这个例子的人(我很惊讶),请阅读:https ://en.wikipedia.org/wiki/Volatile_variable

EDIT2:好的,我真的希望每个知道什么volatile的人都看过这个例子。

如果您使用示例中的代码,循环中读取的变量将被优化,使循环无限。

解决方案当然是使用volatilewhich 将强制编译器在每次访问时读取变量。

我的问题是这是否是 C++11 中已弃用的问题,因为机器模型是多线程的,因此编译器应考虑对变量的并发访问存在于系统中。

4

2 回答 2

88

它是否被优化完全取决于编译器以及他们选择优化的内容。C++98/03 内存模型无法识别在x它的设置和值的检索之间可能发生变化的可能性。

C++11 内存模型确实认识到x可以更改。然而,它并不在乎。对变量的非原子访问(即:不使用std::atomics 或适当的互斥体)会产生未定义的行为。因此,对于 C++11 编译器来说,假设x写入和读取之间永远不会发生变化是完全可以的,因为未定义的行为可能意味着“函数永远不会看到x变化”。

现在,让我们看看 C++11 对volatile int x;. 如果你把它放在那里,并且你有一些其他的线程混乱x你仍然有未定义的行为。易失性不影响线程行为。C++11 的内存模型没有将读取或写入从/到x原子定义,也不需要正确排序非原子读取/写入所需的内存屏障。volatile与它无关。

哦,你的代码可能有效。但是 C++11 并不能保证

告诉编译器的是volatile它无法优化从该变量读取的内存。但是,CPU 内核具有不同的缓存,并且大多数内存写入不会立即发送到主内存。它们被存储在该核心的本地缓存中,并且可能被写入......最终

CPU 有办法将高速缓存线强制输出到内存中,并在不同内核之间同步内存访问。这些内存屏障允许两个线程有​​效地通信。仅仅从一个内核中读取另一个内核中写入的内存是不够的。写入内存的核心需要发出一个屏障,并且正在读取它的核心需要在读取它之前完成该屏障才能真正获取数据。

volatile不保证这一切。Volatile 与“硬件、映射内存和其他东西”一起工作,因为写入该内存的硬件确保缓存问题得到解决。如果 CPU 内核在每次写入后都会发出内存屏障,那么您基本上可以告别任何性能希望。因此,C++11 有特定的语言来说明何时需要构造来发出障碍。

volatile是关于内存访问(何时读取);线程是关于内存完整性(实际存储在那里的内容)。

C++11 内存模型具体说明了哪些操作会导致一个线程中的写入在另一个线程中变得可见。这是关于内存完整性的,这不是volatile处理的事情。并且内存完整性通常需要两个线程来做某事。

例如,如果线程 A 锁定一个互斥体,执行写入,然后解锁它,则 C++11 内存模型只要求线程 B 稍后锁定它时,该写入对线程 B 可见。在它真正获得那个特定的锁之前,它是不确定的。这些东西在标准的第 1.10 节中有详细的说明。

让我们看一下您引用的代码,就标准而言。第 1.10 节,p8 谈到了某些库调用使线程与另一个线程“同步”的能力。大多数其他段落解释了同步(和其他事情)如何在线程之间建立操作顺序。当然,您的代码不会调用任何 this。没有同步点,没有依赖排序,什么都没有。

如果没有这种保护,没有某种形式的同步或排序,1.10 p21 就会出现:

如果程序的执行在不同的线程中包含两个相互冲突的操作,则该程序的执行包含数据竞争,其中至少一个不是原子的,并且两者都不会在另一个之前发生。任何此类数据竞争都会导致未定义的行为。

您的程序包含两个相互冲突的操作(读取x和写入x)。两者都不是原子的,也不是通过同步命令在另一个之前发生。

因此,您已经实现了未定义的行为。

因此,C++11 内存模型保证多线程行为的唯一情况是使用正确的互斥锁或std::atomic<int> x使用正确的原子加载/存储调用。

哦,你也不需要变得x易变。每当您调用(非内联)函数时,该函数或其调用的东西都可以修改全局变量。所以它不能优化循环x中的读取。while并且每个 C++11 同步机制都需要调用一个函数。这恰好调用了内存屏障。

于 2012-10-14T01:17:30.123 回答
2

Intel developer zone mentions, "Volatile: Almost Useless for Multi-Threaded Programming"

The volatile keyword is used in this signal handler example from cppreference.com

#include <csignal>
#include <iostream>

namespace
{
  volatile std::sig_atomic_t gSignalStatus;
}

void signal_handler(int signal)
{
  gSignalStatus = signal;
}

int main()
{
  // Install a signal handler
  std::signal(SIGINT, signal_handler);

  std::cout << "SignalValue: " << gSignalStatus << '\n';
  std::cout << "Sending signal " << SIGINT << '\n';
  std::raise(SIGINT);
  std::cout << "SignalValue: " << gSignalStatus << '\n';
}
于 2018-08-15T18:30:39.387 回答