【问题标题】:Should a pointer to stack variable be volatile?指向堆栈变量的指针应该是易失的吗?
【发布时间】:2017-04-06 20:46:34
【问题描述】:

我知道我应该使用volatile 关键字来告诉编译器不要优化对变量的内存读写。我也知道在大多数情况下它是should only be used to talk to non-C++ memory

但是,我想知道在持有指向某个本地(堆栈)变量的指针时是否必须使用volatile

例如:

//global or member variable
/* volatile? */bool* p_stop;

void worker()
{
    /* volatile? */ bool stop = false;
    p_stop = &stop;
    while(!stop)
    {
        //Do some work
        //No usage of "stop" or p_stop" here
    }
}

void stop_worker()
{
    *p_stop = true;
}

在我看来,具有某种优化级别的编译器可能会看到 stop 是一个局部变量,它永远不会更改,并且可以用 while(true) 替换 while(!stop),从而在什么都不做的情况下更改 *p_stop

那么,在这种情况下是否需要将指针标记为 volatile?

P.S:请不要告诉我为什么不使用它,使用此 hack 的真实代码是出于(复杂到难以解释的)原因。

编辑:

  1. 我没有提到这两个函数在不同的线程上运行。 worker() 是第一个线程的函数,应该使用p_stop 指针从另一个线程停止。

  2. 我不想知道有什么更好的方法来解决这种黑客行为背后的真正原因。我只是想知道这是否是 C++ 中定义的\未定义的行为(11),以及这是否依赖于编译器\平台\等。到目前为止,我看到@Puppy 说每个人都错了,而且这是错误的,但没有参考表明这一点的特定标准。

我知道你们中的一些人对“不要教训我”部分感到冒犯,但请坚持真正的问题 - 我应该使用 volatile 还是不使用?或者这是UB?如果可以,请通过提供完整的答案帮助我(和其他人)学习新知识。

【问题讨论】:

  • 对于线程同步,使用std::atomicvolatile 与线程无关。
  • @ZivS:所以,现在我们讨论的是不同的线程。是的,如果stop可以被不同的线程改变,它应该是volatile
  • 不,真的不应该是volatile,应该是atomic,这是完全不同的东西。
  • 投反对票的人 - 请解释你这样做的原因
  • 另外,您反复说“请不要教训我,我知道 bla bla”,然后说“OP 怎么知道这是 UB?”。大声笑

标签: c++ compiler-optimization volatile


【解决方案1】:

我只是想知道这是否是 C++ 中已定义\未定义的行为(就此而言是 11 个)

Ta-da(来自N3337,“准C++11”)

如果其中一个修改了内存位置 [..] 而另一个访问或修改了相同的内存位置,则两个表达式求值会发生冲突。

§1.10/4

和:

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

§1.10/21

您正在从不同的线程访问对象stop 的(内存位置),这两种访问都不是原子的,因此也没有“发生在之前”的关系。简而言之,您存在数据竞争,因此存在未定义的行为

我不想知道有什么更好的方法来解决这种黑客行为背后的真正原因。

原子操作(由 C++ 标准定义)是唯一(可靠)解决此问题的方法。

【讨论】:

  • 非常感谢。这回答了我的问题。
  • 所以我现在不明白,如果从两个线程访问内存是未定义的行为,那么 volatile 是如何工作的?例如,这里的答案是 UB stackoverflow.com/questions/72552/why-does-volatile-exist 吗?
  • 是的,那是 UB。 volatile 使对相关对象的访问严格按照标准所述进行。基本上,这“禁用”了允许积极优化的标准的as if rule。对于信号处理程序和模板元编程;)
  • @JasonS 否,标准的相关部分:来自N1570(“准 C11”AFAIK)的§5.1.2.4/4 和 §5.1.2.4/25。
  • @JasonS 有stdatomic.h 标头。
【解决方案2】:

那么,在这种情况下是否需要将指针标记为 volatile?

没有。这不是必需的,主要是因为 volatile 在这种情况下甚至无法远程覆盖您需要它执行的操作。您必须使用实际的同步原语,例如原子操作或互斥锁。在此处使用volatile 是未定义的行为,您的程序将爆炸。

volatile 对并发没有用处。它可能对实现并发原语有用,但还远远不够。

坦率地说,您是否想使用实际的同步原语是无关紧要的。如果你想编写正确的代码,你别无选择

【讨论】:

  • 取决于底层架构...具有强大内存模型的单核微控制器应该能够仅处理volatile,而无需重量级的同步原语。
  • 远程也不行。 C++ 标准定义了 C++ 语言中可接受的内容。您的实现可能会或可能不会实现的硬件是否可以提供更强的保证是无关紧要的。
  • 有趣...我认为您在理论上是对的,即使在实践中某些 C/C++ 编译器可能提供与单核处理器的仅顺序内存访问一致的内存语义。你有关于这个主题的任何信息吗?我找到了这篇文章:preshing.com/20120930/weak-vs-strong-memory-models“正如我之前所演示的,您仍然必须指定正确的内存排序约束,即使只是为了防止编译器重新排序。”但我找不到他在哪里给出了编译器所需的内存排序约束的例子,即使在强大的硬件上也是如此。
  • 创建具体示例很棘手,因为编译器只是简单地发出机器代码,大部分时间都按照您的预期执行,但会以微妙和不确定的方式被破坏。看看aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf。它现在有点过时了,但是这种情况下的情况并没有真正改变——C++11 主要使原子正确成为可能,而不是改变 volatile 完全被破坏的方式(对于这种用法)
【解决方案3】:

P.S:请不要告诉我为什么不使用它,

我不确定我们应该说什么。编译器管理堆栈,因此您对它所做的任何事情在技术上都是未定义的行为,并且在升级到编译器的下一个版本时可能无法正常工作。

您还做出了可能与编译器优化时的假设不同的假设。这是使用(或不使用)volatile 的真正原因;您为编译器提供指导,帮助它确定优化是否安全。使用volatile 告诉编译器它应该假设这些变量可能由于外部影响(其他线程或特殊硬件行为)而改变。

所以是的,在这种情况下,您似乎需要用 volatile 限定符标记 both p_stopstop

(注意:这是必要的但还不够,因为它不会导致在具有宽松内存模型的语言实现中发生适当的行为,该模型需要障碍来确保正确性。请参阅https://en.wikipedia.org/wiki/Memory_ordering#Runtime_memory_ordering

【讨论】:

  • 对不起,如果我的示例需要 volatile,我无法从您的回答中理解
  • 您能否详细说明为什么The compiler manages the stack, so anything you are doing with it is technically undefined behavior
  • 使用volatile,兼容的编译器不能再用while (true) 替换循环,但来自其他线程的更改可能不可见。
  • 好问题。我不是 C 标准方面的专家,所以我无法找到合适的条款。但这里有一件事:仅仅因为你在函数中声明了一个局部变量并不意味着它实际上进入了堆栈。编译器可能会选择将其放入寄存器中,而从不将其放入堆栈。所以寄存器的地址不是一个有意义的概念。 (虽然&stop 可能会强制将其放入堆栈。)
  • @Jarod42,如果编译器不改成while,怎么会不可见?
【解决方案4】:

从所提供的细节中根本无法回答这个问题。

正如问题中所述,这是一种完全不受支持的线程间通信方式。

所以唯一的答案是:

指定您正在使用的编译器版本,并希望有人知道其最黑暗的秘密或参考您的文档。所有的 C++ 标准都会告诉你这是行不通的,任何人都可以告诉你的是“可能工作但不工作”。

没有“哦,伙计们,大家都知道这很有效,我该怎么做作为解决方法?wink wink”的答案。

除非您的编译器不支持原子或适当的并发机制,否则没有正当理由这样做。 “不支持”不是“解释起来很复杂”,所以我会根据该代码片段着迷,以了解没有正确执行此操作的可能原因(除了古老的编译器)。

【讨论】:

  • 很明显可以回答。指针不应该是volatile,因为volatile 甚至无法远程解决问题。
  • Dan,目前有2个答案与此相反,所以我正在寻找一个有说服力的答案,如果答案是否定的,我很乐意解释这个案例跨度>
  • @puppy 但这取决于平台。利用非标准编译器行为实际上并没有错。在这种情况下,这只是(可能)完全没有必要。
  • @DanAllen:如果您的编译器明确保证它是安全的并保证始终这样做,那么这样做并没有错。但是,这不是涉及使用 C++ 语言构造 volatile 的 C++ 问题。
  • @puppy 这是volatile,但不是我们所知道的! (吉姆)。例如,在针对 x86 的 MSVC 上,这将起作用。获取和释放语义是隐含的,volatile 将确保不会重新排序。但在所问问题的范围内,它仍未定义。
猜你喜欢
  • 1970-01-01
  • 2020-01-03
  • 2010-10-13
  • 2020-03-09
  • 2015-05-31
  • 2023-03-09
  • 1970-01-01
  • 1970-01-01
  • 2012-12-13
相关资源
最近更新 更多