C++ 下 thread-safe 的 lazy initialization

《clang 避免 non-local static 物件初始化順序的方法》提到可用 static local variable, 然後用 method 傳回的方式避免產生 global static variable (藉此避免不同編譯單元的初始化問題)。以下是一個例子:

const struct Point* center()
{
  static Point* s_center = CreateCenterPoint();
  return s_center;
}

但是, 在 multi-thread 的環境下, s_center 可能被初始化兩次。假設 CreateCenterPoint() 會傳回不同的值, 或是外界可以改變取得的 Point, 有兩份 Point 會造成問題。

好消息是:

所以情況比想像中的安全。

不過, 其它 lazy initialization 的實作方式有可能出錯。更一般化的 lazy initialization 會用 Double-Checked Lock Pattern, 但是這個作法有不易察覺的漏洞

直覺的作法如下:

Singleton* Singleton::instance()
{
  if (pInstance == 0) { // 1st test
    Lock lock;
    if (pInstance == 0) { // 2nd test
      pInstance = new Singleton;
    }
  }
  return pInstance;
}

注意初始化 singleton 是三個步驟組成的:

  1. 配置一塊新記憶體
  2. 初始化新記憶體
  3. 將新記憶體的位置指向目標指標 (即 pInstance)

compiler 有可能更動三者的順序。最壞的情況下, thread A 執行了 1, 3, 還沒執行 2, 這時 thread B 發覺 pInstance 不是 0, 於是回傳尚未初始化的 pInstance。解法是要使用 memory barrier。或是在 C++11 後更可用跨平台的解法。詳情見 Double-Checked Locking Is Fixed In C++11

另外, Java 1.4 以前 Double-Checked Locking 也有問題。Java 1.5 後多了 volatile 表示「取出最新的值」, 才有辦法修正此問題。Effective Java 2/e Item 66 "Synchronize access to shared mutable data" 和 Item 71 "Use lazy initialization judiciously" 有詳細的討論。

留言

這個網誌中的熱門文章

(C/C++ ) 如何在 Linux 上使用自行編譯的第三方函式庫

熟悉系統工具好處多多

virtualbox 使用 USB 裝置