嵌入式與裸機

volatile 修飾詞(正確使用)

/ VOL-uh-tul /

編譯器對於移除它認為無意義的工作極其聰明。若你寫一個迴圈反覆讀同一個變數卻沒改它,編譯器可能只讀一次就重複使用該值;若你寫了一個變數卻從不回讀,編譯器可能刪掉那次寫入。這通常是極佳的最佳化——但當那個「變數」其實是會自行改變值、或每次寫入都有物理效果的硬體暫存器時,這就是場災難。volatile 修飾詞就是你告訴編譯器:在這裡別自作聰明——「完全」按原樣、依序執行此物件的「每一次」讀與「每一次」寫,並且絕不假設值在兩次存取之間維持不變。

精確地說,對某物件套用 volatile 意味著原始碼中對它的每次存取都是編譯器不得新增、移除或合併的可觀察副作用,而且對 volatile 物件的存取彼此之間不會被重排。你把它寫在型別裡:volatile uint32_t *reg = (uint32_t *)0x40020000; ——現在迴圈裡的 *reg 每一輪都會重讀,而 *reg = x 也真的會被發出。有兩種典型、正當的用途。第一是記憶體映射的週邊暫存器,硬體可能改變狀態位元、而寫入會觸發動作。第二是 ISR 與主迴圈(或信號處理常式與 main)之間共享的變數:ISR 可能非同步地改它,所以主迴圈每次都必須重讀,而非信任快取的副本。只標真正需要的那個物件——注意 volatile uint32_t *p 是「指向 volatile 資料的非 volatile 指標」(通常正是暫存器指標所要的)。

它之所以重要,是因為忘記 volatile 會讓你的程式碼在 -O0 能跑、到 -O2 就壞(編譯器這下把存取最佳化掉了),這是最令人困惑的一類嵌入式臭蟲。但同樣重要的誠實警告,也是極常見的誤解:volatile「無關」執行緒安全或原子性。它「不」讓操作變原子、「不」在 volatile 與非 volatile 存取之間提供記憶體屏障,也「不」建立多執行緒程式碼所需的 happens-before 順序。要在 CPU 核心或執行緒之間共享資料,你需要的是原子型別(_Atomic / std::atomic)與正確的記憶體順序,而非 volatile。把 volatile 當窮人版互斥鎖用是個真實又危險的錯誤;volatile 的工作是單一核心上的裝置與中斷可見性,僅此而已。

// 錯誤:編譯器可能只讀一次 STATUS,然後永遠迴圈(或永不)。 uint32_t *status = (uint32_t *)0x40004400; while ((*status & 1u) == 0) { } // 在 -O2 會被最佳化掉! // 正確:volatile 強制每一輪都真正重讀。 volatile uint32_t *status = (volatile uint32_t *)0x40004400; while ((*status & 1u) == 0) { } // 每次都向硬體輪詢

沒有 volatile,最佳化器可能快取狀態讀取,迴圈就永遠看不到位元改變。volatile 強制每次讀取都真正發生。

最大的誤解:volatile「不」是原子的,也「不」能替代互斥鎖或記憶體屏障。它保證存取會發生,而非該存取不可分割、或相對其他執行緒有順序——跨執行緒共享請用原子型別。

又称
volatile keywordvolatile-qualified accessvolatile 限定詞揮發性修飾