常識告訴我們:線程間共享變量需用 volatile。但如果不加 volatile,真的會導致線程因緩存原因陷入 while(flag > 0) 死循環嗎?
static int flag = -1;
void thread1() { while(flag > 0) { /* wait */ } }
void thread2() { /* ... */ flag = -1; }
測試代碼(GCC -O3):
static int vvv = 1;
void* thread1(void*) { sleep(2); vvv = -1; return NULL; }
int main() {
pthread_t t; pthread_create(&t, NULL, thread1, NULL);
while(vvv > 0); // 主線程等待
return 0;
}
現象:-O1/-O2/-O3 均導致主線程死循環,CPU 佔滿;-O0 正常退出。
反匯編揭秘:
.L4: movl _ZL3vvv(%rip), %eax # 讀取 vvv
testl %eax, %eax
jle .L5 # 若 ≤0 跳出
.L6: jmp .L6 # 無限循環,不再讀取內存!
編譯器將 while(vvv > 0) 優化為 if(vvv > 0) { for(;;) {} },因為單線程語義下 vvv 在循環內不會被修改。這不是緩存問題,是編譯器合法優化。
手動修改跳轉目標到 .L4(每次循環重新讀取),重新編譯運行 → 2 秒後正常退出。說明:
加上 volatile 後,匯編變為:
.L5: movl _ZL3vvv(%rip), %eax
testl %eax, %eax
setg %al
testb %al, %al
jne .L5 # 每次循環真正讀取內存
volatile 阻止編譯器優化,強制每次讀寫內存,但不阻止 CPU 從 Cache 讀取(MESI 保證一致性)。網上「volatile 繞過 Cache 直接讀內存」的說法不準確。
volatile int flag = -1;
void thread1() { jobA(); flag = 1; }
void thread2() { while(1) if(flag > 0) jobB(); }
CPU 可能將 flag = 1 排在 jobA() 之前執行!導致 jobB 提前看到標誌位。需內存屏障:
// GCC 內建原子操作隱含 memory barrier
while(true) { __sync_fetch_and_add(&vvv, 0); if(vvv < 0) break; }
生成 lock addl $0, vvv(%rip),lock 前綴鎖總線/緩存行,保證順序性與可見性。Java/C# volatile 即基於此機制。
| 場景 | 會死循環? | 原因 |
|---|---|---|
| 編譯器優化移除讀取 | 是 | 編譯器合法優化(單線程語義) |
| 代碼真正讀取內存 | 否 | MESI 協議保證緩存一致性 |
| 需防止指令重排 | 視情況 | 需 memory barrier (lock 指令/原子操作) |