【问题标题】:Volatile in C++11C++11 中的易失性
【发布时间】:2012-10-04 09:46:02
【问题描述】:

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

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

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

编辑2: 好的,我真的很期待知道volatile 是什么的每个人都看过这个例子。

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

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

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

【问题讨论】:

  • 您的问题标题中有 volatile,但示例代码中没有。
  • @VaughnCato 好吧,这就是代码的重点。 en.wikipedia.org/wiki/Volatile_variable
  • @VaughnCato 不,这绝对不是我要问的,因为答案是否定的。
  • 你的问题和 volatile 有什么关系?
  • @Let_Me_Be:您的意思是链接说,“对 volatile 变量的操作不是原子的,它们也没有为线程建立适当的先发生关系。”再说一遍,这个关于线程的问题和volatile有什么关系?

标签: c++ c++11 volatile


【解决方案1】:

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

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

现在,让我们看看 C++11 对volatile int x; 的看法。如果你把它放在那里,并且你有一些其他线程与x 混淆,你仍然有未定义的行为。 Volatile 不会影响 threading 行为。 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 节中有详细的说明。

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

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

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

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

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

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

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

【讨论】:

  • 嗯。问题是代码不依赖于顺序或原子性。那是另一回事。您能否发布标准的相关部分?
  • @Let_Me_Be:它确实依赖于排序和原子性。来自另一个线程的更改必须对这个线程可见。这意味着它必须在此线程中的某些操作之前 之前进行排序。由于volatile 不处理排序,因此无法保证它会变得可见。
  • 哦哇。我越来越困惑:-D 如果问题出在排序上,那么即使是原子变量也不是吗?我的意思是没有任何东西可以保证第二个线程会真正执行(好吧,操作系统中的调度程序可能会执行,但在 C++ 中没有)。我的意思是,以下代码中未定义的行为来自哪里:gist.github.com/3886984(如果我使用非原子(非)易失性版本)?
  • 啊,最后一个提到可见性问题的答案。当我告诉他们“某个线程向某个 volatile 变量写入一个新值。但在 C++ 中,volatile 本身并不能保证任何其他线程会看到更新的值。”时几乎没有人相信我。
  • @ZanLynx:我的观点是优化器要么看到:1)对无法内联的函数的调用,因此不能假设变量不会被更改,或者 2 ) 一个显式内存屏障(可能是从内联函数导入的),因此它不能假设变量不会被更改。在这两种情况下,优化器都不能假设变量没有改变。因此,如果您的代码直接或间接地正在执行其内存屏障功能,则不需要volatile。如果不是,volatile 对您没有帮助。
【解决方案2】:

英特尔开发者专区提及,"Volatile: Almost Useless for Multi-Threaded Programming"

cppreference.com 的这个信号处理程序示例中使用了 volatile 关键字

#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';
}

【讨论】:

  • 那个“维基百科引用”是错误的(它引用了 MSDN,所以它应该说“根据 MSDN”)。该标准实际上说“对于某些实现,易失性可能表明需要特殊的硬件指令来访问对象。”无论如何,该标准从未说过它“仅”用于“硬件访问”(无论这意味着什么)。
猜你喜欢
  • 2012-02-22
  • 2018-11-01
  • 1970-01-01
  • 2023-03-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多