13

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

int global_value = 0;

void foo () {
    ++ global_value;
}

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

我的理解是volatile“旨在”用于指向映射内存和变量的指针,这些指针可以通过信号修改(并且强调不是为了线程安全),但很容易想象它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是经常被忽视的。这不是什么新鲜事


扩展示例

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

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

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

4

5 回答 5

12

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

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

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

序列点包括:

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

你有你的证据。

于 2011-07-28T11:38:09.643 回答
5

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

volatile对任何其他目的都没有用,包括您询问的代码。

于 2011-07-28T11:59:23.763 回答
4

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

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

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

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

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

于 2011-07-28T12:01:24.400 回答
1

不,全局变量不应该总是被声明为 volatile。

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

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

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

于 2011-07-28T11:40:44.270 回答
0

在此示例中不需要易失性。例如,如果 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 在所有情况下都不足以保证线程安全(有时您需要额外注意原子性和内存屏障,但线程间通信变量应该是 volatile...

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

于 2011-07-28T12:07:35.987 回答