C/C++的volatile关键字应用示例

首先必须强调volatile无法用来保证线程安全。

volatile的功能是阻止编译器优化,从而直接从内存中读写变量的值。由于操作系统访问寄存器的速度远大于访问内存的速度(两者之间还有各级cache),所以编译程序时可能会进行优化,比如这样的语句

int flag = 1;
while (flag) {}

由于多次对flag进行判断,所以编译器可能优化为把flag变量的值从内存中拷贝到寄存器中,之后每次都从寄存器中读取。从代码的层面看起来没有问题,但是比如信号处理函数改变了flag的值,更新的flag不会立刻(甚至不会)反映到寄存器中,因此读取的flag的值还是旧的值。
给出示例代码

// test.cc
#include 
#include 

static int g_iRun = 1;

void sigint_handler(int) { g_iRun = 0; }

int main() {
    if (signal(SIGINT, sigint_handler) == SIG_ERR)
        perror("signal SIGINT");
    while (g_iRun) {}
    printf("sigint caught!\n");
    return 0;
}
$ g++ test.cc 
$ ./a.out 
^Csigint caught!
$ g++ test.cc -O
$ ./a.out 
^C^C^C^C^\Quit (core dumped)

可以看到仅仅加了-O选项,最低层次的优化下信号处理器都不会对Ctrl+C做出反应。
为了防止程序直接从寄存器中读取变量的值,需要用volatile来修饰g_iRun变量
static volatile int g_iRun = 1;
修饰之后,即使用-O3选项进行优化,仍然可以捕捉信号

$ g++ test.cc -O3
$ ./a.out 
^Csigint caught!

另一个典型应用就是APUE上图7-13的示例程序,C程序使用setjmplongjmp回滚函数栈帧时,自动变量的值是否回滚是不确定的。

// test.cc
#include 
#include 

static jmp_buf jmpbuffer;

void func() { longjmp(jmpbuffer, 1); }

int main() {
    int x = 1;
    if (setjmp(jmpbuffer) == 0) {  
        x = 2;
        func();
    } else {  // 从longjmp中返回
        printf("%d\n", x);
    }
    return 0;
}
$ g++ test.cc 
$ ./a.out 
2
$ g++ test.cc -O
$ ./a.out 
1

和处理信号的示例一样,用了优化选项-O编译后,自动变量x的值在longjmp后从2变成了1。如果用volatile修饰自动变量x,那么longjmp之后x的值保证为2。

最后说说为什么无法保证线程安全。
线程安全指在多线程环境下,无论多线程如何交替执行,最后的结果都是预期值。比如N个线程对变量x执行x = x + 1操作,最后x的值增加了N。
举个经典例子,2个线程对volatile变量x(初值为0)执行自增操作,既可能是这样的执行顺序

  1. 线程A从内存中读取x的值(A.x=0);
  2. 线程B从内存中读取x的值(B.x=0);
  3. 线程A写入值(A.x+1=1)到x的内存中(x此时为1);
  4. 线程B写入值(B.x+1=1)到x的内存中(x此时为1);

也有可能时这样的执行顺序

  1. 线程A从内存中读取x的值(A.x=0);
  2. 线程A写入值(A.x+1=1)到x的内存中(x此时为1);
  3. 线程B从内存中读取x的值(B.x=1);
  4. 线程B写入值(B.x+1=2)到x的内存中(x此时为2);

两种不同的执行顺序导致了不同的结果,但是两个线程都是直接从内存中读写变量x。

你可能感兴趣的:(C/C++的volatile关键字应用示例)