volatile关键字的作用是什么?在c++中它能解决什么问题?

就我而言,我从来没有明知肚明地需要它。


当前回答

我想引用Herb Sutter在GotW #95中的一句话,这有助于理解volatile变量的含义:

C++ volatile variables (which have no analog in languages like C# and Java) are always beyond the scope of this and any other article about the memory model and synchronization. That’s because C++ volatile variables aren’t about threads or communication at all and don’t interact with those things. Rather, a C++ volatile variable should be viewed as portal into a different universe beyond the language — a memory location that by definition does not obey the language’s memory model because that memory location is accessed by hardware (e.g., written to by a daughter card), have more than one address, or is otherwise “strange” and beyond the language. So C++ volatile variables are universally an exception to every guideline about synchronization because are always inherently “racy” and unsynchronizable using the normal tools (mutexes, atomics, etc.) and more generally exist outside all normal of the language and compiler including that they generally cannot be optimized by the compiler (because the compiler isn’t allowed to know their semantics; a volatile int vi; may not behave anything like a normal int, and you can’t even assume that code like vi = 5; int read_back = vi; is guaranteed to result in read_back == 5, or that code like int i = vi; int j = vi; that reads vi twice will result in i == j which will not be true if vi is a hardware counter for example).

其他回答

In the early days of C, compilers would interpret all actions that read and write lvalues as memory operations, to be performed in the same sequence as the reads and writes appeared in the code. Efficiency could be greatly improved in many cases if compilers were given a certain amount of freedom to re-order and consolidate operations, but there was a problem with this. Even though operations were often specified in a certain order merely because it was necessary to specify them in some order, and thus the programmer picked one of many equally-good alternatives, that wasn't always the case. Sometimes it would be important that certain operations occur in a particular sequence.

Exactly which details of sequencing are important will vary depending upon the target platform and application field. Rather than provide particularly detailed control, the Standard opted for a simple model: if a sequence of accesses are done with lvalues that are not qualified volatile, a compiler may reorder and consolidate them as it sees fit. If an action is done with a volatile-qualified lvalue, a quality implementation should offer whatever additional ordering guarantees might be required by code targeting its intended platform and application field, without requiring that programmers use non-standard syntax.

不幸的是,许多编译器并没有确定程序员需要什么样的保证,而是选择提供标准规定的最低限度的保证。这使得volatile远没有它应有的用处。例如,在gcc或clang上,一个程序员需要实现一个基本的“移交互斥量”(一个已经获得并释放互斥量的任务直到另一个任务释放互斥量后才会再次释放互斥量),他必须做以下四件事中的一件:

将互斥量的获取和释放放在编译器不能内联的函数中,并且不能应用整个程序优化。 将互斥锁保护的所有对象限定为volatile——如果所有访问都发生在获得互斥锁之后和释放互斥锁之前,那么就不应该这样做。 使用优化级别0来强制编译器生成代码,就像所有非限定寄存器的对象都是volatile一样。 使用特定于gcc的指令。

相比之下,当使用更适合系统编程的高质量编译器时,例如icc,人们将有另一种选择:

确保在每个需要获取或释放的地方执行volatile-qualified write。

获取一个基本的“传递互斥量”需要一个volatile读取(看看它是否准备好了),并且不应该需要一个volatile写入(另一方在它被交还之前不会试图重新获取它),但是必须执行一个毫无意义的volatile写入仍然比gcc或clang下可用的任何选项都要好。

除了volatile关键字用于告诉编译器不要优化对某些变量(可以由线程或中断例程修改)的访问之外,它还可以用于删除一些编译器错误——是的,它可以是——。

例如,我在一个嵌入式平台上工作,编译器对变量的值做出了一些错误的假设。如果代码没有优化,程序可以正常运行。通过优化(这是非常必要的,因为这是一个关键的例程),代码将无法正常工作。唯一的解决方案(虽然不是很正确)是将“错误的”变量声明为volatile。

除了按预期使用它,volatile还用于(模板)元编程。它可以用来防止意外重载,因为volatile属性(如const)参与了重载解析。

template <typename T> 
class Foo {
  std::enable_if_t<sizeof(T)==4, void> f(T& t) 
  { std::cout << 1 << t; }
  void f(T volatile& t) 
  { std::cout << 2 << const_cast<T&>(t); }

  void bar() { T t; f(t); }
};

这是合法的;这两个重载都可能是可调用的,并且执行几乎相同的操作。在volatile重载中的强制转换是合法的,因为我们知道bar无论如何都不会通过一个非volatile T。不过,volatile版本严格来说更糟糕,所以如果非volatile f可用,则永远不要在重载分辨率中选择。

注意,代码实际上从不依赖于volatile内存访问。

我曾经在20世纪90年代早期开发过一个大型应用程序,其中包含使用setjmp和longjmp进行基于c语言的异常处理。volatile关键字对于那些值需要保存在作为“catch”子句的代码块中的变量是必要的,以免这些变量被存储在寄存器中并被longjmp清除。

所有的答案都很好。但在此之上,我想分享一个例子。

下面是一个小的cpp程序:

#include <iostream>

int x;

int main(){
    char buf[50];
    x = 8;

    if(x == 8)
        printf("x is 8\n");
    else
        sprintf(buf, "x is not 8\n");

    x=1000;
    while(x > 5)
        x--;
    return 0;
}

现在,让我们生成上述代码的程序集(我将只粘贴与此相关的程序集的部分):

生成程序集的命令:

g++ -S -O3 -c -fverbose-asm -Wa,-adhln assembly.cpp

会众是这样。

main:
.LFB1594:
    subq    $40, %rsp    #,
    .seh_stackalloc 40
    .seh_endprologue
 # assembly.cpp:5: int main(){
    call    __main   #
 # assembly.cpp:10:         printf("x is 8\n");
    leaq    .LC0(%rip), %rcx     #,
 # assembly.cpp:7:     x = 8;
    movl    $8, x(%rip)  #, x
 # assembly.cpp:10:         printf("x is 8\n");
    call    _ZL6printfPKcz.constprop.0   #
 # assembly.cpp:18: }
    xorl    %eax, %eax   #
    movl    $5, x(%rip)  #, x
    addq    $40, %rsp    #,
    ret 
    .seh_endproc
    .p2align 4,,15
    .def    _GLOBAL__sub_I_x;   .scl    3;  .type   32; .endef
    .seh_proc   _GLOBAL__sub_I_x

您可以在程序集中看到,没有为sprintf生成程序集代码,因为编译器假定x不会在程序之外发生变化。while循环也是如此。由于优化,循环被完全删除,因为编译器认为它是无用的代码,因此直接将5分配给x(参见movl $5, x(%rip))。

如果外部进程/硬件将x的值更改为x = 8之间的某个值,则会出现问题;和if(x == 8).我们希望else块可以工作,但不幸的是编译器已经删除了这部分。

现在,为了解决这个问题,在assembly。cpp中,让我们改变int x;到volatile int x;并快速查看生成的汇编代码:

main:
.LFB1594:
    subq    $104, %rsp   #,
    .seh_stackalloc 104
    .seh_endprologue
 # assembly.cpp:5: int main(){
    call    __main   #
 # assembly.cpp:7:     x = 8;
    movl    $8, x(%rip)  #, x
 # assembly.cpp:9:     if(x == 8)
    movl    x(%rip), %eax    # x, x.1_1
 # assembly.cpp:9:     if(x == 8)
    cmpl    $8, %eax     #, x.1_1
    je  .L11     #,
 # assembly.cpp:12:         sprintf(buf, "x is not 8\n");
    leaq    32(%rsp), %rcx   #, tmp93
    leaq    .LC0(%rip), %rdx     #,
    call    _ZL7sprintfPcPKcz.constprop.0    #
.L7:
 # assembly.cpp:14:     x=1000;
    movl    $1000, x(%rip)   #, x
 # assembly.cpp:15:     while(x > 5)
    movl    x(%rip), %eax    # x, x.3_15
    cmpl    $5, %eax     #, x.3_15
    jle .L8  #,
    .p2align 4,,10
.L9:
 # assembly.cpp:16:         x--;
    movl    x(%rip), %eax    # x, x.4_3
    subl    $1, %eax     #, _4
    movl    %eax, x(%rip)    # _4, x
 # assembly.cpp:15:     while(x > 5)
    movl    x(%rip), %eax    # x, x.3_2
    cmpl    $5, %eax     #, x.3_2
    jg  .L9  #,
.L8:
 # assembly.cpp:18: }
    xorl    %eax, %eax   #
    addq    $104, %rsp   #,
    ret 
.L11:
 # assembly.cpp:10:         printf("x is 8\n");
    leaq    .LC1(%rip), %rcx     #,
    call    _ZL6printfPKcz.constprop.1   #
    jmp .L7  #
    .seh_endproc
    .p2align 4,,15
    .def    _GLOBAL__sub_I_x;   .scl    3;  .type   32; .endef
    .seh_proc   _GLOBAL__sub_I_x

在这里,您可以看到生成了sprintf、printf和while循环的程序集代码。这样做的好处是,如果某个外部程序或硬件更改了x变量,那么将执行sprintf部分的代码。类似地,while循环也可以用于busy waiting now。