C 區域性靜態初始化不是執行緒安全

2022-08-24 00:39:12 字數 2388 閱讀 9865

在塊作用域中的靜態變數的規則 (與之相對的是全域性作用域的靜態變數) 是, 程式第一次執行到他的宣告的時候進行初始化. 

檢視下面的競爭條件:

int computesomething()

這段**的意圖是在該函式第一次被呼叫的時候去計算一些費用, 並且把結果緩衝起來待函式將來再被呼叫的時候則直接返回這個值即可.

這個基本技巧的變種,在網路上也被叫做 避免 "static initialization order fiasco". ( fiasco這個詞 在這個網頁上有非常棒的描述,因此我建議大家去讀一讀然後去理解它.)

這段**的問題是非執行緒安全的. 在區域性作用域中的靜態變數是編譯時會在編譯器內部轉換成下面的樣子:

int computesomething()

return cachedresult;

}

現在競爭條件就比較容易看到了.

假設兩個執行緒在同一時刻都呼叫這個函式. 第乙個執行緒在執行 cachedresult_computed = true 後, 被搶占. 第二個執行緒現在看到的 cachedresult_computed 是乙個真值( true ),然後就略過了if分支的處理,最後該函式返回的是乙個未初始化的變數.

現在你看到的東西並不是乙個編譯器的bug, 這個行為 c++ 標準所要求的.

你也能寫乙個變體來產生乙個更糟糕的問題:

class something ;

int computesomething()

同樣的在編譯器內部它會被重寫 (這次, 我們使用c++偽**):

class something ;

int computesomething()

return s.computeit();

}// destruct s at process termination

void destructs()

注意這裡有多重的競爭條件. 就像前面所說的, 乙個執行緒很可能在另乙個執行緒之前執行並且在"s"還沒有被構造前就使用它. 

甚至更糟糕的情況, 第乙個執行緒很可能在s_contructed 條件判定 之後,在他被設定成"true"之前被搶占. 在這種場合下, 物件s就會被雙重構造和雙重析構. 

這樣就不是很好.

但是等等, 這並不是全部, 現在(原文是not,我認為是now的筆誤)看看如果有兩個執行期初始化區域性靜態變數的話會發生什麼: 

class something ;

int computesomething()

上面的**會被編譯器轉化為下面的偽c++**:

class something ;

int computesomething()

static uninitialized something t;

if (!(constructed & 2))

return s.computeit() + t.computeit();

}

為了節省空間, 編譯器會把兩個"x_constructed" 變數放到乙個 bitfield 中. 現在這裡在變數"construted"上就有多個無內部鎖定的讀-改-存操作.

現在考慮一下如果乙個執行緒嘗試去執行 "constructed |= 1", 而在同一時間另乙個執行緒嘗試執行 "constructed |= 2".

在x86平台上, 這條語句會被彙編成

or constructed, 1

...or constructed, 2

並沒有 "lock" 字首. 在多處理機器上, 很有可能發生兩個儲存都去讀同乙個舊值並且互相使用衝突的值進行碰撞(clobber).

在 ia64 和 alpha平台上, 這個碰撞將更加明顯,因為它們麼沒有這樣的讀-改-存的單條指令; 而是被編碼成三條指令:

ldl t1,0(a0)     ; load

addl t1,1,t1 ; modify

stl t1,1,0(a0) ; store

如果這個執行緒在 load 和 store之間被搶占, 這個儲存的值可能將不再是它曾經要寫入的那個值.

因此,現在考慮下面這個有問題的執行順序:

現在, 你可能會認為你能用臨界區 (critical section) 來封裝這個執行期初始化動作:

int computesomething()

因為你現在把這個一次初始化放到了臨界區裡面,而使它執行緒安全.

但是如果從同乙個執行緒再一次呼叫這個函式會怎樣? ("我們跟蹤了這個呼叫; 它確實是來自這個執行緒!") 如果 computesomethingslowly() 它自己間接地呼叫 computesomething()就會發生這個狀況.

結論: 當你看見乙個區域性靜態變數在執行期初始化時, 你一定要小心.

C 靜態成員初始化

在c 類中,靜態成員一般不允許在類宣告中進行初始化,應該在類的外部進行初始化,例如 class a static int a a 0 初始化方式 void main void 但是有乙個例外,可以為靜態成員提供const 整數型別的類內初始值。要求靜態成員必須是字面值常量型別的constexpr 因...

靜態初始化和例項初始化

父類單獨的效果 當父類單獨執行時,靜態初始化塊優先執行,然後是例項初始化塊,最後才是構造器 子類單獨效果 首先執行父類的靜態初始化塊和子類的初始化塊 優先執行靜態 然後執行父類的例項初始化塊和構造器,最後執行子類的例項初始化塊和構造器 父類子類效果1 父在前子在後 先將父類的物件例項出來後,進行子類...

陣列 初始化 只含動態初始化 靜態初始化

首先j a中此處只講靜態初始化 動態初始化 靜態初始化就是提前在陣列中設定好了陣列內容,此內容不做改動,該多長已經在設定內容的時候已經決定 動態初始化就是僅限於new及確定陣列大小長度,裡面的陣列內容沒有,可自由進行填寫,也包含了靜態初始化的內容 示例 package 陣列 public class...