在 LWN 看到一篇好文章,討論Linux kernel 中的 ACCESS_ONCE() macro 的用法與意義。文章討論到 multi-thread 程式共用變數所需要注意的一個細節:要使用 volatile 關鍵字去修飾變數以避免編譯器的最佳化造成的問題。
文中以一個在 Linux kernel 的例子來說明,但考慮到這個議題在任何 multi-thread 程式(或任何有 context-switch 的程式中)都會遇到,所以我用簡單的情況來舉例。考慮某個 thread 有如下程式碼片段:
它會依據 global_status 的值來決定程式流程,而這個變數可能會在另一個 thread function 中被修改(也可能在 signal handler 中被修改),所以每次 loop 時,可能會執行 do_something() 也可能不會執行它。問題在於編譯器的最佳化往往都會作在語言沒有清楚定義的地方。在 C 語言中,由於沒有規定 multi-thread 的共享變數的行為(在 C11 前幾乎對此沒有著墨),所以若 for loop 中沒有對 global_status 任何修改,編譯器有可能將上述 loop 中的判斷整個拿掉:
為的是最佳化。Jonathan Corbet 特別在回應中提到,如果只在需要時聲明,便可以只在不須最佳化的地方避免錯誤,而在其他地方依然享有對此變數的最佳化。Linux kernel 為了最佳化,連變數的存取方式都很細緻呢...不過這也進一步要求 programmer 完全掌握 C 語言的各種黑暗角落...也因為如此,該文引來很多激烈的論戰。:-)
文中以一個在 Linux kernel 的例子來說明,但考慮到這個議題在任何 multi-thread 程式(或任何有 context-switch 的程式中)都會遇到,所以我用簡單的情況來舉例。考慮某個 thread 有如下程式碼片段:
- for (;;) {
- ...
- if (global_status == ST_MOVE) {
- do_something();
- }
- ...
它會依據 global_status 的值來決定程式流程,而這個變數可能會在另一個 thread function 中被修改(也可能在 signal handler 中被修改),所以每次 loop 時,可能會執行 do_something() 也可能不會執行它。問題在於編譯器的最佳化往往都會作在語言沒有清楚定義的地方。在 C 語言中,由於沒有規定 multi-thread 的共享變數的行為(在 C11 前幾乎對此沒有著墨),所以若 for loop 中沒有對 global_status 任何修改,編譯器有可能將上述 loop 中的判斷整個拿掉:
- // if global_status is ST_SLEEP
- for (;;) {
- ...
- /*
- if (global_status == ST_MOVE) {
- do_something();
- }*/
- ...
#define ACCESS_ONCE(x) (*(volatile typeof(x) *)&(x))
很清楚地,就是希望以這種方式來存取變數,但問題在於:為何不從一開始就以 volatile 宣告,而是直到要存取時特別聲明呢?為的是最佳化。Jonathan Corbet 特別在回應中提到,如果只在需要時聲明,便可以只在不須最佳化的地方避免錯誤,而在其他地方依然享有對此變數的最佳化。Linux kernel 為了最佳化,連變數的存取方式都很細緻呢...不過這也進一步要求 programmer 完全掌握 C 語言的各種黑暗角落...也因為如此,該文引來很多激烈的論戰。:-)
作者已經移除這則留言。
回覆刪除