首先必須強調(diào)volatile無法用來保證線程安全子库。
volatile的功能是阻止編譯器優(yōu)化揍异,從而直接從內(nèi)存中讀寫變量的值现拒。由于操作系統(tǒng)訪問寄存器的速度遠大于訪問內(nèi)存的速度(兩者之間還有各級cache)蔓腐,所以編譯程序時可能會進行優(yōu)化叮阅,比如這樣的語句
int flag = 1;
while (flag) {}
由于多次對flag進行判斷刁品,所以編譯器可能優(yōu)化為把flag變量的值從內(nèi)存中拷貝到寄存器中,之后每次都從寄存器中讀取浩姥。從代碼的層面看起來沒有問題挑随,但是比如信號處理函數(shù)改變了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選項,最低層次的優(yōu)化下信號處理器都不會對Ctrl+C做出反應(yīng)眯分。
為了防止程序直接從寄存器中讀取變量的值拌汇,需要用volatile來修飾g_iRun變量
static volatile int g_iRun = 1;
修飾之后,即使用-O3選項進行優(yōu)化弊决,仍然可以捕捉信號
$ g++ test.cc -O3
$ ./a.out
^Csigint caught!
另一個典型應(yīng)用就是APUE上圖7-13的示例程序噪舀,C程序使用setjmp
和longjmp
回滾函數(shù)棧幀時,自動變量的值是否回滾是不確定的飘诗。
// 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
和處理信號的示例一樣与倡,用了優(yōu)化選項-O編譯后,自動變量x的值在longjmp
后從2變成了1昆稿。如果用volatile修飾自動變量x纺座,那么longjmp
之后x的值保證為2。
最后說說為什么無法保證線程安全溉潭。
線程安全指在多線程環(huán)境下净响,無論多線程如何交替執(zhí)行少欺,最后的結(jié)果都是預(yù)期值。比如N個線程對變量x執(zhí)行x = x + 1
操作馋贤,最后x的值增加了N狈茉。
舉個經(jīng)典例子,2個線程對volatile變量x(初值為0)執(zhí)行自增操作掸掸,既可能是這樣的執(zhí)行順序
- 線程A從內(nèi)存中讀取x的值(A.x=0)氯庆;
- 線程B從內(nèi)存中讀取x的值(B.x=0);
- 線程A寫入值(A.x+1=1)到x的內(nèi)存中(x此時為1)扰付;
- 線程B寫入值(B.x+1=1)到x的內(nèi)存中(x此時為1)堤撵;
也有可能時這樣的執(zhí)行順序
- 線程A從內(nèi)存中讀取x的值(A.x=0);
- 線程A寫入值(A.x+1=1)到x的內(nèi)存中(x此時為1)羽莺;
- 線程B從內(nèi)存中讀取x的值(B.x=1)实昨;
- 線程B寫入值(B.x+1=2)到x的內(nèi)存中(x此時為2);
兩種不同的執(zhí)行順序?qū)е铝瞬煌慕Y(jié)果盐固,但是兩個線程都是直接從內(nèi)存中讀寫變量x荒给。