在塊作用域中的靜態(tài)變量的規(guī)則 (與之相對的是全局作用域的靜態(tài)變量) 是, 程序第一次執(zhí)行到他的聲明的時(shí)候進(jìn)行初始化.

察看下面的競爭條件:

int ComputeSomething()
{
   static int cachedResult = ComputeSomethingSlowly();
   return cachedResult;
}

這段代碼的意圖是在該函數(shù)第一次被調(diào)用的時(shí)候去計(jì)算一些費(fèi)用, 并且把結(jié)果緩沖起來待函數(shù)將來再被調(diào)用的時(shí)候則直接返回這個(gè)值即可.

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

這段代碼的問題是非線程安全的. 在局部作用域中的靜態(tài)變量是編譯時(shí)會(huì)在編譯器內(nèi)部轉(zhuǎn)換成下面的樣子:

int ComputeSomething()
{
  static bool cachedResult_computed = false;
  static int cachedResult;
  if (!cachedResult_computed) {
     cachedResult_computed = true;
     cachedResult = ComputeSomethingSlowly();
  }
  return cachedResult;
}

現(xiàn)在競爭條件就比較容易看到了.

假設(shè)兩個(gè)線程在同一時(shí)刻都調(diào)用這個(gè)函數(shù). 第一個(gè)線程在執(zhí)行 cachedResult_computed = true 后, 被搶占. 第二個(gè)線程現(xiàn)在看到的 cachedResult_computed 是一個(gè)真值( true ),然后就略過了if分支的處理,最后該函數(shù)返回的是一個(gè)未初始化的變量.

現(xiàn)在你看到的東西并不是一個(gè)編譯器的bug, 這個(gè)行為 C++ 標(biāo)準(zhǔn)所要求的.

你也能寫一個(gè)變體來產(chǎn)生一個(gè)更糟糕的問題:

class Something { ... };
int ComputeSomething()
{
   static Something s;
   return s.ComputeIt();
}

同樣的在編譯器內(nèi)部它會(huì)被重寫 (這次, 我們使用C++偽代碼):

class Something { ... };
int ComputeSomething()
{
  static bool s_constructed = false;
  static uninitialized Something s;
  if (!s_constructed) {
      s_constructed = true;
      new(&s) Something; // construct it
      atexit(DestructS);
  }
  return s.ComputeIt();
}
// Destruct s at process termination
void DestructS()
{
   ComputeSomething::s.~Something();
}

注意這里有多重的競爭條件. 就像前面所說的, 一個(gè)線程很可能在另一個(gè)線程之前運(yùn)行并且在"s"還沒有被構(gòu)造前就使用它.

甚至更糟糕的情況, 第一個(gè)線程很可能在s_contructed 條件判定 之后,在他被設(shè)置成"true"之前被搶占. 在這種場合下, 對象s就會(huì)被雙重構(gòu)造雙重析構(gòu)

這樣就不是很好.

但是等等, 這并不是全部, 現(xiàn)在(原文是Not,我認(rèn)為是Now的筆誤)看看如果有兩個(gè)運(yùn)行期初始化局部靜態(tài)變量的話會(huì)發(fā)生什么:

class Something { ... };
int ComputeSomething()
{
static Something s(0);
static Something t(1);
return s.ComputeIt() + t.ComputeIt();
}

上面的代碼會(huì)被編譯器轉(zhuǎn)化為下面的偽C++代碼:

class Something { ... };
int ComputeSomething()
{
  static char constructed = 0;
static uninitialized Something s;
if (!(constructed & 1)) {
constructed |= 1;
new(&s) Something; // construct it
atexit(DestructS);
}
static uninitialized Something t;
if (!(constructed & 2)) {
constructed |= 2;
new(&t) Something; // construct it
atexit(DestructT);
}
return s.ComputeIt() + t.ComputeIt();
}

為了節(jié)省空間, 編譯器會(huì)把兩個(gè)"x_constructed" 變量放到一個(gè) bitfield 中. 現(xiàn)在這里在變量"construted"上就有多個(gè)無內(nèi)部鎖定的讀-改-存操作.

現(xiàn)在考慮一下如果一個(gè)線程嘗試去執(zhí)行 "constructed |= 1", 而在同一時(shí)間另一個(gè)線程嘗試執(zhí)行 "constructed |= 2".

在x86平臺(tái)上, 這條語句會(huì)被匯編成

  or constructed, 1
...
or constructed, 2
并沒有 "lock" 前綴. 在多處理機(jī)器上, 很有可能發(fā)生兩個(gè)存儲(chǔ)都去讀同一個(gè)舊值并且互相使用沖突的值進(jìn)行碰撞(clobber).

在 ia64 和 alpha平臺(tái)上, 這個(gè)碰撞將更加明顯,因?yàn)樗鼈兠礇]有這樣的讀-改-存的單條指令; 而是被編碼成三條指令:

  ldl t1,0(a0)     ; load
addl t1,1,t1     ; modify
stl t1,1,0(a0)   ; store

如果這個(gè)線程在 load 和 store之間被搶占, 這個(gè)存儲(chǔ)的值可能將不再是它曾經(jīng)要寫入的那個(gè)值.

因此,現(xiàn)在考慮下面這個(gè)有問題的執(zhí)行順序:

  • 線程A 在測試 "constructed" 條件后發(fā)現(xiàn)他是零, 并且正要準(zhǔn)備把這個(gè)值設(shè)定成1, 但是它被搶占了.
  • 線程B 進(jìn)入同樣的函數(shù), 看到 "constructed" 是零并繼續(xù)去構(gòu)造 "s" 和 "t", 離開時(shí) "constructed" 等于3.
  • 線程A 繼續(xù)執(zhí)行并且完成它的 讀-改-存 的指令序列, 設(shè)定 "constructed" 成 1, 然后構(gòu)造 "s" (第二次).
  • 線程A 然后繼續(xù)去構(gòu)造 "t" (第二次) 并設(shè)定 "constructed" (最終) 成 3.

現(xiàn)在, 你可能會(huì)認(rèn)為你能用臨界區(qū) (critical section) 來封裝這個(gè)運(yùn)行期初始化動(dòng)作:

int ComputeSomething()
{
EnterCriticalSection(...);
static int cachedResult = ComputeSomethingSlowly();
LeaveCriticalSection(...);
return cachedResult;
}

因?yàn)槟悻F(xiàn)在把這個(gè)一次初始化放到了臨界區(qū)里面,而使它線程安全.

但是如果從同一個(gè)線程再一次調(diào)用這個(gè)函數(shù)會(huì)怎樣? ("我們跟蹤了這個(gè)調(diào)用; 它確實(shí)是來自這個(gè)線程!") 如果 ComputeSomethingSlowly() 它自己間接地調(diào)用 ComputeSomething()就會(huì)發(fā)生這個(gè)狀況.

結(jié)論: 當(dāng)你看見一個(gè)局部靜態(tài)變量在運(yùn)行期初始化時(shí), 你一定要小心.