首先必须强调volatile无法用来保证线程安全。
volatile的功能是阻止编译器优化,从而直接从内存中读写变量的值。由于操作系统访问寄存器的速度远大于访问内存的速度(两者之间还有各级cache),所以编译程序时可能会进行优化,比如这样的语句
int flag = 1;
while (flag) {}
由于多次对flag进行判断,所以编译器可能优化为把flag变量的值从内存中拷贝到寄存器中,之后每次都从寄存器中读取。从代码的层面看起来没有问题,但是比如信号处理函数改变了flag的值,更新的flag不会立刻(甚至不会)反映到寄存器中,因此读取的flag的值还是旧的值。
给出示例代码
// test.cc
#include <stdio.h>
#include <signal.h>
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程序使用setjmp
和longjmp
回滚函数栈帧时,自动变量的值是否回滚是不确定的。
// test.cc
#include <stdio.h>
#include <setjmp.h>
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)执行自增操作,既可能是这样的执行顺序
- 线程A从内存中读取x的值(A.x=0);
- 线程B从内存中读取x的值(B.x=0);
- 线程A写入值(A.x+1=1)到x的内存中(x此时为1);
- 线程B写入值(B.x+1=1)到x的内存中(x此时为1);
也有可能时这样的执行顺序
- 线程A从内存中读取x的值(A.x=0);
- 线程A写入值(A.x+1=1)到x的内存中(x此时为1);
- 线程B从内存中读取x的值(B.x=1);
- 线程B写入值(B.x+1=2)到x的内存中(x此时为2);
两种不同的执行顺序导致了不同的结果,但是两个线程都是直接从内存中读写变量x。