首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> C++ >> C++ 入門

多線程共享非 volatile 變量:會導致 while 死循環嗎?

     編輯:C++ 入門

背景與核心問題

常識告訴我們:線程間共享變量需用 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 秒後正常退出。說明:

  • x86 MESI 協議保證緩存一致性,核心間修改可見性納秒級
  • 只要代碼真正執行「讀取內存」指令,硬體層面完全沒問題

volatile 的真實作用

加上 volatile 後,匯編變為:

.L5: movl _ZL3vvv(%rip), %eax
     testl %eax, %eax
     setg %al
     testb %al, %al
     jne .L5        # 每次循環真正讀取內存

volatile 阻止編譯器優化,強制每次讀寫內存,但不阻止 CPU 從 Cache 讀取(MESI 保證一致性)。網上「volatile 繞過 Cache 直接讀內存」的說法不準確。

真正的硬體陷阱:CPU 亂序執行

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 即基於此機制。

總結

最佳實踐

  • 正確同步用 std::atomic (C++11) / pthread_mutex / std::mutex
  • volatile 僅用於硬體寄存器映射、信號處理器等特殊場景
  • 無鎖編程極難,優先用標準庫同步原語

參考資源

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved
場景會死循環?原因
編譯器優化移除讀取是編譯器合法優化(單線程語義)
代碼真正讀取內存否MESI 協議保證緩存一致性
需防止指令重排視情況需 memory barrier (lock 指令/原子操作)