2014-01-17 190 views
3

假設你有一個多態Singleton類型(在我們的例子中是一個自定義的std::error_category類型)。該類型是無狀態的,所以沒有數據成員,但它確實有一些虛擬功能。在多線程環境中實例化此類型時出現問題。初始化沒有魔術靜態的空多態Singleton類型

實現這一目標是使用C++ 11的magic statics最簡單的方法:

my_type const& instantiate() { 
    static const my_type instance; 
    return instance; 
} 

不幸的是,我們的編譯器(VC11)之一不支持此功能。

  • 我應該期望這會在多線程環境中爆炸嗎?我很確定,就標準而言,所有投注都是關閉的。但是鑑於這種類型不包含任何數據成員和虛擬函數,我應該從VC11等主流實現中期望什麼樣的錯誤?例如,在執行error_category時,Boost.System和VC都沒有采​​取任何預防措施。他們只是粗心大意,還是擔心這裏的比賽是不合理的偏執狂?
  • 以符合標準的方式擺脫數據競爭的最佳方式是什麼?由於這種情況下的類型是error_category,我希望儘可能避免執行堆分配。請記住,在這種情況下,Singleton語義是至關重要的,因爲錯誤類別的等同性是由指針比較確定的。這意味着例如線程本地存儲不是一個選項。

回答

0

該標準沒有提到在多線程上調用函數時如何構造靜態的問題。

gcc使用鎖來使函數級靜態線程安全(可以被標誌禁用)。絕大多數(所有?)版本的Visual C++都沒有線程安全功能級靜態。

建議圍繞變量聲明使用鎖來保證線程安全。

+1

C++ 11 [stmt.dcl/4不是無聲:」 ......這樣的變量是 首次初始化控制穿過它的聲明;這樣的變量被認爲是在 完成初始化如果初始化通過拋出異常退出,則初始化 未完成,因此在下一次控制進入聲明時將再次嘗試初始化。如果控制在初始化變量時並行輸入 ,則併發執行應等待 完成初始化。「 – Casey

1

嘗試#2B:實現自己的std::once_flag當量,與atomic<int>Live at Rextester):

my_type const& instantiate() { 
    static std::aligned_storage<sizeof(my_type), __alignof(my_type)>::type storage; 
    static std::atomic_int flag; 

    while (flag < 2) { 
     // all threads spin until the object is properly initialized 
     int expected = 0; 
     if (flag.compare_exchange_weak(expected, 1)) { 
      // only one thread succeeds at the compare_exchange. 
      try { 
       ::new (&storage) my_type; 
      } catch(...) { 
       // Initialization failed. Let another thread try. 
       flag = 0; 
       throw; 
      } 
      // Success! 
      if (!std::is_trivially_destructible<my_type>::value) { 
       std::atexit([] { 
        reinterpret_cast<my_type&>(storage).~my_type(); 
       }); 
      } 
      flag = 2; 
     } 
    } 

    return reinterpret_cast<my_type&>(storage); 
} 

這僅依賴於編譯器正確零初始化所有靜態存儲持續時間的對象,並且還使用了非標準擴展__alignof(<type>)以正確對齊storage,因爲微軟的編譯團隊不會被打擾添加沒有兩個下劃線的關鍵字。


嘗試#1:在具有 std::once_flagLive demo at Coliru)結合使用 std::call_once

my_type const& instantiate() { 
    struct empty {}; 
    union storage_t { 
     empty e; 
     my_type instance; 
     constexpr storage_t() : e{} {} 
     ~storage_t() {} 
    }; 

    static std::once_flag flag; 
    static storage_t storage; 

    std::call_once(flag, []{ 
     ::new (&storage.instance) my_type; 
     std::atexit([]{ 
      storage.instance.~my_type(); 
     }); 
    }); 

    return storage.instance; 
} 

std::once_flag默認的構造是constexpr,所以它保證恆定初始化期間來構造。我覺得VC正確執行不斷的初始化。編輯:不幸的是,通過VS12的MSVC仍然不支持constexpr,所以這種技術有一些未定義的行爲。我會再嘗試。

+0

比我想要的多一點代碼,但它實現了threadsafety的目標,而不需要在函數定義之外導出任何對象,或者在靜態初始化順序失敗中有任何參與。 – Casey

+0

您應該使用atomic_int而不是原子,因爲在原子中有一個默認構造函數,它在函數運行時將該標誌設置爲0。如果發生在上一個線程剛剛設置爲1時,則會有兩個線程進入臨界區。 atomic_int仍然被初始化爲0,但是這種情況發生在程序啓動時,當競爭條件不可能時。 – Ian

+0

@Ian是的,謝謝,我忘記了MSVC未能正確實施「原子」的簡單默認構造。 – Casey

2

這是一個可能更簡單的版本凱西的答案,它使用原子自旋鎖來防止正常靜態聲明

my_type const& instantiate() 
{ 
    static std::atomic_int flag; 
    while (flag != 2) 
    { 
    int expected = 0; 
    if (flag.compare_exchange_weak(expected, 1)) 
     break; 
    } 
    try 
    { 
    static my_type instance = whatever; // <--- normal static decl and init 

    flag = 2; 
    return instance; 
    } 
    catch (...) 
    { 
    flag = 0; 
    throw; 
    } 
} 

這段代碼也更容易變成三個宏觀的重用,這是很容易#defined到沒事就支持魔靜平臺。

my_type const& instantiate() 
{ 
    MY_MAGIC_STATIC_PRE; 

    static my_type instance = whatever; // <--- normal static decl and init 

    MY_MAGIC_STATIC_POST; 

    return instance; 

    MY_MAGIC_STATIC_SCOPE_END; 
} 
+0

@Casey - 感謝您發現。我現在修好了。 – Ian