【问题标题】:Should ALL global variables be volatile-qualified?所有全局变量都应该是 volatile 限定的吗?
【发布时间】:2011-10-15 01:17:48
【问题描述】:

在这个例子中,正确性是否需要将global_value 声明为volatile

int global_value = 0;

void foo () {
    ++ global_value;
}

void bar () {
    some_function (++global_value);
    foo ();
    some_function (++global_value);
}

我的理解是,volatile 是“用于”pointers to mapped memory and variables which can be modified by signals(并且强调 不是 用于线程安全),但很容易想象bar 可能会编译成这样的东西:

push EAX
mov EAX, global_value
inc EAX
push EAX
call some_function
call foo
inc EAX
push EAX
call some_function
mov global_value, EAX
pop EAX

这显然是不正确的,但即使没有volatile,我相信根据C抽象机它是有效的。我错了还是有效?

如果是这样,在我看来volatile 经常被忽视。这将是nothing new


扩展示例

void baz (int* i) {
    some_function (++*i);
    foo ();
    some_function (++*i);
}

int main () {
    baz (&global_value);
}

即使bar 保证编译成正确的 dont-cache-global_value 实现,baz 是否同样正确,或者是否允许缓存*i 的非易失性值?

【问题讨论】:

  • volatile 的确切语义是依赖于实现的
  • baz 与 volatile 无关,这是编译器知道的标准别名问题。因此,C99 有restrict。避免这些问题的最佳方法是根本不使用全局变量!
  • 不限于全局变量,类成员可以通过其他成员或朋友在同一个 mannar 中进行非局部修改。我想解决方案是“偏执狂或整个程序优化”然后......

标签: c++ c global-variables volatile qualifiers


【解决方案1】:

不,volatile 关键字在这里不是必需的。由于global_value 在函数bar 之外可见,因此编译器不能假定调用另一个函数时它保持不变。

[2011-07-28 更新] 我找到了一个很好的引文来证明这一切。它在 ISO C99, 5.1.2.3p2 中,我懒得在这里完整复制。它说:

在称为sequence points的执行序列中的某些指定点,之前评估的所有副作用都应该是完整的,并且后续评估的副作用应该没有发生。

序列点包括:

  • 在计算参数后调用函数 (6.5.2.2)。
  • 完整表达式的结尾:[...] 表达式语句 (6.8.3) 中的表达式; [...]

你有证据。

【讨论】:

  • 你有引用吗?
  • 不,我不知道。整个代码对我来说似乎很明显(假设你有一个单线程程序),我什至不知道我必须在 C 标准的哪个部分查找它。
  • 您也许可以引用这样一个事实,即从foo 返回时有一个序列点。因此foo 中的代码必须在此之前写入对象。在调用foo 之后main 中的代码必须使用在该序列点建立的global_value 的实际值,而不是一些不同的值。很难说它在标准中的哪个位置说,“当你说global_value 时,这意味着global_value,而不是它的某个先前值,或者就此而言,是其他一些凭空出现的数字”。正如 Roland 所说,这在单线程代码中很明显,这是所有标准地址。
  • 标识符global_value的所有出现都指向同一个对象,并且所有访问都被序列点正确分隔。这可能是一个完整证明的提示。对于语言律师来说,这绝对是一项有趣的任务。
  • @spraff:在所有序列点重新获取所有全局值是不正确的。允许编译器推断哪些副作用是可能的,并且如果该值不可能更改,则可以缓存。
【解决方案2】:

volatile 的唯一用途涉及longjmp、信号处理程序、内存映射设备驱动程序以及编写您自己的低级多线程同步原语。然而,对于最后一次使用,volatile 是不够的,甚至可能没有必要。您肯定还需要 asm(或特定于编译器或 C1x 原子)来进行同步。

volatile 不能用于任何其他用途,包括您询问的代码。

【讨论】:

  • Volatile 已被“认证”用于这些用途,并且是为 MMIO 发明的;但它可用于许多其他目的。
  • @curiousguy:愿意举个例子吗?
  • F.ex. volatile 可用于与 MMIO 非常相似的线程,但问题几乎相同。
  • @curiousguy:我在回答中说,有一些方法可以在线程之间同步使用它,但这既不是必要的也不是充分的,只有你可以使用的成分滚动预 C11 同步。我希望在我的答案所涵盖的内容之外有一个例子。
  • 只要你想避免对对象访问的所有优化,就使用 volatile;它可以用于弱线程间信号,防止在编译期间知道值,在整数加法上获得 CPU 加法(2 compl),确保变量没有被优化,使对象在调试器,能够可靠地看到带有 ptrace 的对象,以确保某些对象遵循 ABI 表示......用途是无限的。
【解决方案3】:

正如 Roland 所说,我不确定该引用标准的哪个部分来说,“如果程序修改了某些东西,这意味着对象在抽象机器中被修改。如果程序使用一个值,这意味着它使用对象在抽象机器中的任何值”。

volatile 控制读取和写入内存的次数和顺序,但即使没有volatile,缓存值作为优化的实现也必须尊重抽象机的行为。这就是“as-if”规则所说的,所以不遵守的优化对我来说并不“容易想象”;-) 你提出的发射代码对我来说显然是错误的,就像说,“写可能会去到内存而不更新或弄脏 L1 缓存,因此未来的读取仍会看到缓存中的旧值”。不是在单个内核上,它不会,因为这样的缓存会被破坏。

如果您调用strcpy,然后检查目标缓冲区的内容,则不允许编译器通过使用存储在寄存器中的该字节的先前值来“优化”。 strcpy 不需要 volatile char *。同样,global_value 也不必是volatile

我想混淆可能是在多线程代码中,“然后”,即读取是否发生在写入“之后”并因此“看到”新值,是由同步原语定义的。在某些实现中,volatile 由于实现特定的保证而与同步有关。

在单线程代码中,在 C 和 C++ 标准中,“然后”是由序列点定义的,在给出的代码中有很多。

【讨论】:

  • 我不认为strcpy 是一个合理的比较,因为将非常量指针传递给函数意味着更改该对象的意图。该论点不适用于我刚刚发布的扩展示例,因为指针没有从baz 传递到foo
  • @spraff:我正在尝试多种方式来告诉你同样的事情。如果其中一个没有单击,那么无论如何:-) 不,在您的扩展示例中,*i 的值无法被缓存。如果编译器知道(在抽象机器中)调用不会修改值,则编译器只能在调用中缓存一个值。这是基本的正确性,与 volatile 无关。
  • @spraff "打算" 拥抱?明确定义的行为与“意图”无关。编译器必须遵循相关标准,不要猜测程序员的意图。您似乎将计算机科学与心理学混淆了。奇怪。
【解决方案4】:

没有。全局变量不应总是声明为 volatile。

只有当它可以被其他线程更改并且可能遭受内存重新排序问题或编译器指令重新排序时,您才真正需要它是易失性的。即使这样,如果你有适当的互斥,你也不需要它。但是,通常情况下,如果您需要对全局变量进行互斥,您的设计可能很糟糕。

编辑:让它成为 volatile 并不意味着全局变量是线程安全的!

其他典型用途可能是以不寻常的方式访问内存 - 例如,如果您在嵌入式微控制器上有一些 DMA 映射内存。

【讨论】:

  • 关于您的第二段:如果您从多个线程执行 ++global_value 并且您没有有适当的互斥,无论它是 volatile 还是不是……
  • 如果那是您的确切观点,我建议您编辑您的答案以进行澄清,因为这不是您所写的:通过说“如果它可以被其他人更改,您只需要它是易变的线程 [etc.] ... 即便如此 如果您有适当的互斥锁,您将不需要它...”您暗示如果您 没有 有适当的互斥,那么你确实需要它是易变的。但是,如果没有互斥锁,您就不需要“需要它是 volatile”:多个线程不能安全地同时执行 ++global_value,无论 global_value 是否为 volatile
  • @Matthew - 公平点 - 不清楚,但你显然是正确的 - 使其易变并不意味着它是线程安全的。
【解决方案5】:

此示例中不需要易失性。例如,如果 some_function() 输出了某些东西,那么 asm 列表似乎会改变 c++ 机器的可观察行为并且违反标准。

我猜这是编译器错误这里是 GCC 汇编器输出:

.cfi_def_cfa_register 5
subl    $24, %esp
.loc 1 67 0
movl    global_value, %eax
addl    $1, %eax
movl    %eax, global_value
movl    global_value, %eax
movl    %eax, (%esp)
call    _Z13some_functioni
.loc 1 68 0
call    _Z3foov
.loc 1 69 0
movl    global_value, %eax
addl    $1, %eax
movl    %eax, global_value
movl    global_value, %eax
movl    %eax, (%esp)
call    _Z13some_functioni
.loc 1 70 0
leave
.cfi_restore 5

global_value 在函数调用之间按预期重新加载

volatiles 用于线程安全,简单的 v-qualifier 不足以在所有情况下都用于线程安全(您有时需要额外注意原子性和内存屏障,但线程间通信变量应该是可变的...

[EDITED]: ...如果它们被重复读取并且可能在读取之间被另一个线程更改。但是,如果使用任何同步锁(互斥锁等),情况不是,因为锁保证变量不会被任何并发活动更改) (感谢 R..)

【讨论】:

  • 最后一段完全错误。所有锁定原语都是完整的编译器屏障。不能跨锁定/解锁调用缓存可以从另一个线程访问的对象。 volatile 与正常的标准(例如 POSIX)一致的线程使用无关。 volatile 唯一对线程有用的时候是在实现您自己的低级同步原语时,这也需要 asm 或编译器特定的同步内存功能。
  • @R.. 我主要考虑最后一种情况,即没有外部同步锁的代码.. 谢谢,已编辑。
猜你喜欢
  • 2021-06-03
  • 2010-12-18
  • 2021-08-06
  • 2012-02-25
  • 2014-09-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-07-10
相关资源
最近更新 更多