C++和雙檢鎖的風險

本篇文章介紹了爲什麼在c++11之前使用單例模式的雙檢測鎖寫法會出問題。以及在c++11中怎麼解決的這些問題。第一篇論文主要討論了爲什麼會出問題,第二篇論文討論了怎麼利用c++11來解決這些問題。如果有翻譯不確定的地方放上了原文供大家參考。

 


 

C++ 和雙檢鎖的風險(C++ and the Perils of Double-Checked Locking)

 

介紹

在新聞組或網絡上谷歌一下各種設計模式的名稱,你一定會發現其中最常被提及的是Singleton。然而,嘗試將Singleton付諸實踐,你肯定會遇到一個重要的限制:按照傳統的實現方式(正如我們在下面所解釋的那樣),Singleton並不是線程安全的。

 

爲解決這一缺陷,我們已經做出了很多努力。其中最流行的方法是一種設計模式,即雙重檢查鎖定模式(DCLP)。DHCP的設計是爲了高效並且線程安全的去初始化一個共享變量(例如Singleton),但它有一個問題:不可靠。此外,在不對傳統模式實現進行實質性修改的情況下,幾乎沒有任何可移植的方法在C++(或C語言)中使其可靠。更有趣的是,DCLP在單核處理器和多核處理器架構上會因爲不同的原因而失敗。

 

本文解釋了爲什麼Singleton不是線程安全的,DCLP如何試圖解決這個問題,爲什麼DCLP在單處理器和多處理器架構上都可能失敗,以及爲什麼你不能(可移植地)對此做任何事情。同時,闡明瞭源代碼中語句排序、序列點、編譯器和硬件優化以及語句執行的實際順序之間的關係。最後,本文對如何向Singleton(和類似的構造)添加線程安全性提出了一些建議,從而使生成的代碼既可靠又高效。

 

單例模式與多線程

 
單例模式的傳統實現是在第一次請求該對象時,使指針指向一個new出來的新對象。

 

// from the header file
class Singleton {
public:
    static Singleton *instance();

private:
    static Singleton *pInstance;
};

// from the implementation file
Singleton *Singleton::pInstance = 0;

Singleton *Singleton::instance() {
    if (pInstance == 0)                // 14
        pInstance = new Singleton;     // 15
        
    return pInstance;
}

 

在單線程環境中,這通常可以正常工作,儘管中斷可能會有問題。如果你在Singleton::instance中,接收到一箇中斷,並從處理程序中調用Singleton::instance,你可以看到你會如何陷入麻煩。不過,如果沒有中斷,這個實現能在單線程環境中工作的很好。

 

不幸的是,這種實現在多線程環境中不可靠。假設線程A進入實例函數,通過第14行執行(CSDN好像不支持顯示代碼行數,我用註釋把行數打在對應代碼後面了),然後被掛起。在掛起時,它剛剛確定pInstance爲空,即尚未創建Singleton對象。

 

線程B現在進入instanceand執行第14行。它看到pInstance爲空,所以它進入第15行,併爲pInstance new一個單例對象,並使指針pInstance指向這個對象。然後它將pInstance返回給實例的調用方。

 

在稍後的某個時刻,線程A被允許繼續運行,它做的第一件事是移動到第15行,在那裏它new出另一個單例對象並使pInstance指向它。顯然,這違反了單例對象的含義,因爲現在有兩個單例對象。從技術上講,第11行是pInstance初始化的地方,但出於實際目的,是第15行使它指向我們希望它指向的地方,因此在本文的其餘部分中,我們將把第15行視爲pInstance初始化的地方。

 
使經典的單例實現線程安全是很容易的。在測試pInstance是否爲空之前加鎖:
 

Singletonton *Singleton::instance() 
{
    Lock lock; // acquire lock (params omitted for simplicity)
    if (pInstance == 0)
        pInstance = new Singleton;
        
    return pInstance;
} // release lock (via lock destructor)

 

這種解決方案的缺點是代價可能很高昂。每次訪問Singleton都需要獲取一個鎖,但實際上,我們只在初始化pInstance時需要一個鎖。應該只在實例第一次被調用時才加鎖。如果在程序運行過程中調用了n次實例,我們只需要在第一次調用實例時加鎖。當你知道n-1次加鎖是非必須的,那爲什麼還要爲n次加鎖買單呢?DCLP的設計是爲了阻止你這麼做。

 

雙重檢查鎖定模式

 
DCLP的關鍵是觀察到大多數對instance的調用會看到pInstance是非空的,因此甚至不會嘗試初始化它。因此,DCLP在嘗試獲取鎖之前會檢測pInstance是否爲空。只有當監測成功時(即pInstance還沒有被初始化),纔會獲得鎖,之後再進行一次測試,以確保pInstance仍然是空的(因此被稱爲雙重檢查鎖)。第二個測試是必要的,因爲,我們剛剛看到,有可能在pInstance第一次被監測到獲得鎖的這段時間裏,另一個線程碰巧初始化了pInstance。
 
下面是典型的DCLP實現:

 

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

    return pInstance;
}

 

在定義DCLP的論文中討論了一些實現問題(例如,volatile限定singleton指針的重要性(the importance of volatile-qualifying the singleton pointer),以及單獨緩存對多處理器系統的影響,我們在下面討論這兩個問題;以及確保某些讀寫操作的原子性的必要性,我們在本文中不討論這個問題。),但它們沒有考慮一個更基本的問題,即確保DCLP期間執行的機器指令以可接受的順序執行。我們在這裏重點討論的就是這個根本問題。

 

DCLP與指令排序

 

再次考慮初始化pInstance的代碼:

 

pInstance = new Singleton;

 

這句代碼發生了三件事:

1) 分配內存空間以保存單例對象。

2) 在分配的內存中構造一個單例對象。

3) 使pInstance指向分配的內存。

 

至關重要的是觀察到編譯器並不受限制地按照這個順序執行這些步驟!特別是,編譯器有時被允許交換步驟2和3。爲什麼他們會想這麼做,這個問題我們稍後再談。現在,讓我們關注一下如果他們這樣做會發生什麼。

 

考慮下面的代碼,其中我們將pInstance的初始化行擴展爲上面提到的三個組成任務,並將步驟1(內存分配)和步驟3(pInstance分配)合併爲步驟2(單例構造)之前的單個語句。我們的想法不是讓一個人來寫這段代碼。相反,編譯器可能會生成與此等效的代碼,以響應人類編寫的傳統DCLP源代碼(如前所示)。

 

Singletonton *Singleton::instance()
{
    if (pInstance == 0)
    {
        Lock lock;
        if (pInstance == 0)
        {
            pInstance =                          // Step 3
                operator new(sizeof(Singleton)); // Step 1
            new (pInstance) Singleton;           // Step 2
        }
    }

    return pInstance;
}

 

一般來說,這並不是對DCLP源代碼的有效翻譯,因爲在步驟2中調用的Singleton構造函數可能會拋出一個異常,如果拋出異常,重要的是pInstance還沒有被修改。一般來說,這就是爲什麼編譯器不能將步驟3移到步驟2之上。然而,在某些條件下,這種轉變是合法的。也許最簡單的這種情況就是編譯器可以證明Singleton的構造函數不能拋出異常,但這不是唯一的條件。一些可以拋出異常的構造函數也可以將其指令重新排序,這樣就會出現這個問題。

 

鑑於上述原理,考慮以下事件的順序:

 

  • 線程A進入實例,執行pInstance的空值檢測,獲取鎖,並執行了由步驟1(檢空)和步驟3(地址賦值給指針)組成的語句,然後被掛起了。此時pInstance指針非空,在pInstance指針指向的內存中還沒有構造任何Singleton對象。
     
  • 線程B進入實例,檢測到pInstance爲非空,並將其返回給實例調用方。 然後調用者通過指針訪問單例類時就會發現,還沒有被構造出來(The caller then dereferences the pointer to access the Singleton that, oops, has not yet been constructed.)。
     

只有在步驟1和步驟2完成後再執行步驟3,DCLP纔會正常工作,但在C或C++中沒有辦法表達這個約束條件。這就是DCLP的核心匕首:我們需要定義一個相對指令排序的約束,但我們的語言沒有給我們表達這個約束的方法。

 

是的,C和C ++標準確實定義了序列點(sequence points),這些序列點定義了評估順序的約束。 例如,C++標準第1.9節的第7段鼓勵性地指出:

 
在執行序列中的某些指定點,稱爲序列點,之前評價的所有副作用都應完成,後續評價的副作用也應沒有發生(At certain specified points in the execution sequence called sequence points, all side effects of previous evaluations shall be complete and no side effects of subsequent evaluations shall have taken place.)。

 

此外,兩個標準都規定,每個語句的末尾都有一個序列點。所以看來,只要你注意你的語句順序,一切都會落到實處。
 

哦,奧德修斯,不要讓你自己被海妖的聲音所誘惑,因爲有很多麻煩等着你和你的夥伴們!(咳咳。。這句話真的是英文原文中存在的。)

 

這兩個標準都以抽象機的可觀察行爲來定義正確的程序行爲。但並不是這臺機器的一切都可以觀察到。例如,考慮這個簡單的函數。

 

void Foo()
{
    int x = 0, y = 0;       // Statement 1
    x = 5;                  // Statement 2
    y = 10;                 // Statement 3
    printf("%d, %d", x, y); // Statement 4
}

 

這個函數看起來很傻,但它可能是由Foo調用的其他函數內聯的結果。

 
在C和C++中,標準都會保證Foo函數將會打印“5, 10”,所以我們知道這會發生。但是我們所知道的都是關於我們得到保證的程度。我們根本不知道語句1-3是否會被執行,事實上一個好的優化器會去掉它們。如果執行語句1-3,我們知道語句1將在語句2-4之前,並且假設對printf的調用沒有內聯並且結果進一步優化,我們知道語句4將在語句1-3之後,但是我們對語句2和3的相對順序一無所知。編譯器可以選擇先執行語句2,先執行語句3,甚至並行執行它們,前提是硬件有某種方法可以做到這一點。這很有可能。現代處理器有很大的字長和幾個執行單元。兩個或更多的算術單位是常見的。(例如,Pentium 4有三個整數ALU,PowerPC的G4e有四個,安騰有六個。)他們的機器語言允許編譯器生成在一個時鐘週期內並行執行兩個或更多指令的代碼。
 

優化編譯器仔細分析並重新排序代碼,以便一次執行儘可能多的任務(在可觀察行爲的約束範圍內)。在常規的串行代碼中發現和利用這種並行性是重新安排代碼和引入失序執行的最重要原因。但這不是唯一的原因。編譯器(和鏈接器)還可以對指令重新排序,以避免從寄存器溢出數據,保持指令管道滿,執行公共子表達式消除,並減小生成的可執行文件的大小。

 

當執行這些類型的優化時,C和C++的編譯器和鏈接器只受語言標準所定義的抽象機上的可觀察行爲的約束,而且–這是重要的一點–這些抽象機是隱含的單線程的。作爲語言,C和C++都沒有線程,所以編譯器在優化時不用擔心線程程序被破壞。因此,他們有時這樣做不應該讓你感到驚訝。

 


 
譯者注:借用這篇文章的前半部分表達了爲什麼c++單例雙檢測鎖會出現問題,但是該文章接下來討論的內容已被c++11實現,轉到另一篇文章討論怎麼用c++11的方法合法的使用雙檢測鎖。

 

C++11 中的雙重檢查鎖定模式(Double-Checked Locking is Fixed In C++11)

英文版原版網址:https://preshing.com/20130930/double-checked-locking-is-fixed-in-cpp11/

 

雙重檢查鎖定模式(DCLP)是無鎖編程中有點臭名昭著的案例。直到2004年,在Java中還沒有安全的方法來實現它。在C++11之前,沒有安全的方法在可移植的C++中實現它。
 
隨着這種模式因其在這些語言中暴露出的缺點而受到人們的關注,人們開始對它進行寫作。2000年,一羣高知名度的Java開發者聚集在一起,簽署了一份題爲 "雙核鎖定已被打破 "的宣言。2004年,Scott Meyers和Andrei Alexandrescu發表了一篇題爲 "C++和雙重檢查鎖定的危險 "的文章。這兩篇論文都是很好的引子,說明了什麼是DCLP,以及爲什麼,當時那些語言不足以實現它。
 
這些都是過去的事了。Java現在有一個修訂的內存模型,對volatilekeyword有了新的語義,這使得安全地實現DCLP成爲可能。同樣,C++11也有一個閃亮的新內存模型和原子庫,可以實現各種可移植的DCLP實現。C++11又啓發了Mintomic,這是我今年早些時候發佈的一個小庫,它使得在一些舊的C/C++編譯器上也可以實現DCLP。
 
在這篇文章中,我將重點介紹DCLP的C++實現。

 

什麼是雙重檢查鎖定?

假設你有一個實現了著名的Singleton模式的類,你想讓它成爲線程安全的類。顯而易見的辦法是通過加鎖來保證相互的排他性。如果兩個線程同時調用Singleton::getInstances,那麼只有其中一個線程會創建這個單例。

 

Singleton *Singleton::getInstance()
{
    Lock lock; // scope-based lock, released automatically when the function returns
    if (m_instance == NULL)
    {
        m_instance = new Singleton;
    }
    return m_instance;
}

 

這是完全合法的方法,但是一旦單例被創建,實際上就不再需要鎖了。鎖不一定慢,但是在高併發的條件下,不具有很好的伸縮性。
 
雙重檢查鎖定模式避免了在單例已經存在時候的鎖定。不過如Meyers-Alexandrescu的論文所顯示的,它並不簡單。在那篇論文中,作者描述了幾個有缺陷的用C++實現DCLP的嘗試,並剖析了每種情況爲什麼是不安全的。最後,在第12頁,他們給出了一個安全的實現,但是它依賴於非指定的,特定平臺的內存屏障(memory barriers)。

 

Singleton *Singleton::getInstance()
{
    Singleton *tmp = m_instance;
    ... // insert memory barrier if (tmp == NULL)
    {
        Lock lock;
        tmp = m_instance;
        if (tmp == NULL)
        {
            tmp = new Singleton;
            ... // insert memory barrier m_instance = tmp;
        }
    }
    return tmp;
}

 

這裏,我們可以發現雙重檢查鎖定模式是由此得名的:在單例指針m_instance爲NULL的時候,我們僅僅使用了一個鎖,這個鎖使偶然訪問到該單例的第一組線程繼續下去。而在鎖的內部,m_instance被再次檢查,這樣就只有第一個線程可以創建這個單例了。
 
這與可運行的實現非常相近。只是在突出顯示的幾行漏掉了某種內存屏障。在作者寫這篇論文的時候,還沒有填補此項空白的輕便的C/C++函數。現在,C++11已經有了。

 

用 C++11 獲得與釋放屏障

你可以用獲得與釋放屏障安全的完成上述實現,這個問題我在之前的文章中已經詳細解釋過。然而,爲了使這段代碼真正具有可移植性,你還必須將m_instance包裹在一個C++11的原子類型中,並使用放鬆的原子操作來操作它。這是最終的代碼,獲取與釋放屏障部分高亮了(譯註:沒法高亮,在後面打了註釋)。

 

std::atomic<Singleton *> Singleton::m_instance;
std::mutex Singleton::m_mutex;

Singleton *Singleton::getInstance()
{
    Singleton *tmp = m_instance.load(std::memory_order_relaxed);
    std::atomic_thread_fence(std::memory_order_acquire); // 獲取屏障
    if (tmp == nullptr)
    {
        std::lock_guard<std::mutex> lock(m_mutex);
        tmp = m_instance.load(std::memory_order_relaxed);
        if (tmp == nullptr)
        {
            tmp = new Singleton;
            std::atomic_thread_fence(std::memory_order_release); // 釋放屏障
            m_instance.store(tmp, std::memory_order_relaxed);
        }
    }
    return tmp;
}

 

即使是在多核系統上,它也可以可靠的工作,因爲內存屏障在創建單例的線程與其後任何跳過這個鎖的線程之間,創建了一種同步的關係。Singleton::m_instance充當警衛變量,而單例本身的內容充當有效載荷。

 

在這裏插入圖片描述

 

這就是那些有缺陷的DCLP實現所缺少的。在沒有任何同步(synchronizes-with)關係的情況下,無法保證第一個線程執行的所有寫入–特別是在Singleton構造函數中執行的寫入–對第二個線程是可見的,即使m_instance指針本身是可見的! 第一個線程持有的鎖也無濟於事,因爲第二個線程不需要獲得任何鎖,它可以併發運行(譯註:還是上面說的CPU指令亂序執行的問題,第一個線程還沒調用構造函數就已經將指針返回。第二個線程獲取到的是空指針)。

 

如果你想更深入的理解這些屏障爲什麼以及如何使得DCLP具有可信賴性,在我以前的文章中有一些背景信息,就像這個博客早前的文章一樣(譯註:以前的文章:http://preshing.com/20130922/acquire-and-release-fences)。

 

使用 Mintomic 屏障

Mintomic是一個小型的C庫,它提供了C++11的原子庫的一個功能子集,包括獲取和釋放屏障,它可以在舊的編譯器上工作。Mintomic依賴於C++11內存模型的假設–具體來說,就是空中存儲(out-of-thin-air stores)–從技術上講,老的編譯器無法保證這一點,但這是我們在沒有C++11的情況下所能做的最好的事情。 請記住,這些是我們多年來編寫多線程C++代碼的情況。空中存儲經過一段時間的實踐證明是不受歡迎的,好的編譯器往往不會這麼做。

 
這是一個使用Mintomic的獲取和釋放屏障的DCLP實現。基本上相當於前面的例子使用C++11的獲取和釋放屏障(譯註:屏障加了註釋)。

 

mint_atomicPtr_t Singleton::m_instance = {0};
mint_mutex_t Singleton::m_mutex;

Singleton *Singleton::getInstance()
{
    Singleton *tmp = (Singleton *)mint_load_ptr_relaxed(&m_instance);
    mint_thread_fence_acquire(); // get
    if (tmp == NULL)
    {
        mint_mutex_lock(&m_mutex);
        tmp = (Singleton *)mint_load_ptr_relaxed(&m_instance);
        if (tmp == NULL)
        {
            tmp = new Singleton;
            mint_thread_fence_release(); // release
            mint_store_ptr_relaxed(&m_instance, tmp);
        }
        mint_mutex_unlock(&m_mutex);
    }
    return tmp;
}

 

使用 C++11 低級排序約束

C++11的獲取和釋放屏障可以正確地實現DCLP,應該可以在當今大多數多核設備上生成最優的機器代碼(就像Mintomic一樣),但它們並不被認爲是非常流行的。在C++11中,實現同樣效果的首選方法是使用具有低級排序約束的原子操作。正如我之前所展示的那樣,寫-釋放可以與讀-獲取同步(譯註:。。。看不懂,所以註釋只加了1和2)。

 

std::atomic<Singleton *> Singleton::m_instance;
std::mutex Singleton::m_mutex;

Singleton *Singleton::getInstance()
{
    Singleton *tmp = m_instance.load(std::memory_order_acquire); // 1
    if (tmp == nullptr)
    {
        std::lock_guard<std::mutex> lock(m_mutex);
        tmp = m_instance.load(std::memory_order_relaxed);
        if (tmp == nullptr)
        {
            tmp = new Singleton;
            m_instance.store(tmp, std::memory_order_release); // 2
        }
    }
    return tmp;
}

 

從技術上講,這種無鎖同步的形式比使用獨立屏障的形式不那麼嚴格,上述操作只是爲了防止自己周圍的內存重排序,而獨立屏障則是爲了防止所有相鄰操作周圍的某些種類的內存重排序(the above operations are only meant to prevent memory reordering around themselves, as opposed to standalone fences, which are meant to prevent certain kinds of memory reordering around neighboring operations.)。儘管如此,在x86/64、ARMv6/v7和PowerPC架構上,兩種形式的最佳機器代碼是一樣的。例如,在以前的一篇文章中,我展示了C++11低級排序約束如何在ARMv7編譯器上發出dmb指令,這和你使用獨立屏障的預期是一樣的。

 
兩種形式有可能產生不同機器代碼的一個平臺是Itanium。Itanium可以用一條CPU指令ld.acq實現C++11的load(memory_order_acquire),用st.rel實現store(tmp, memory_order_release)。我很想研究這些指令與獨立屏障的性能差異,但沒有機會接觸到Itanium機器。

 

另一個這樣的平臺是最近推出的ARMv8架構。ARMv8提供了ldar和stlr指令,這些指令與Itanium的ld.acq和st.rel指令類似,只是它們還在stlrin指令和任何後續ldar之間執行更重的StoreLoad排序。事實上,ARMv8的新指令是爲了實現C++11的SC原子論,接下來會介紹。

 

使用 C++11 的順序一致原子

C++11提供了一種完全不同的方法來寫無鎖代碼。(我們可以認爲在某些特定的代碼路徑上DCLP是“無鎖”的,因爲並不是所有的線程都具有鎖。)如果在所有原子庫函數上,你忽略了可選的std::memory_order參數,那麼默認值std::memory_order_seq_cst就會將所有的原子變量轉變爲順序一致的(sequentially consistent) (SC)原子。通過SC原子,只要不存在數據競爭,整個算法就可以保證是順序一致的。SC原子Java 5+中的volatile變量非常相似。
 
這裏是使用SC原子的一個DCLP實現。如之前所有例子一樣,一旦單例被創建,第二行高亮將與第一行同步。
 

std::atomic<Singleton *> Singleton::m_instance;
std::mutex Singleton::m_mutex;

Singleton *Singleton::getInstance()
{
    Singleton *tmp = m_instance.load(); // 1
    if (tmp == nullptr)
    {
        std::lock_guard<std::mutex> lock(m_mutex);
        tmp = m_instance.load();
        if (tmp == nullptr)
        {
            tmp = new Singleton;
            m_instance.store(tmp); // 2
        }
    }
    return tmp;
}

 
SC原子被認爲可以使程序員更容易思考。其代價是生成的機器代碼似乎比之前的例子效率要低。

 

使用 C++11 的數據相關性排序

懶的翻譯。。。

In all of the above examples I’ve shown here, there’s a synchronizes-with relationship between the thread which creates the singleton and any subsequent thread which avoids the lock. The guard variable is the singleton pointer, and the payload is the contents of the singleton itself. In this case, the payload is considered a data dependency of the guard pointer.

It turns out that when working with data dependencies, a read-acquire operation, which all of the above examples use, is actually overkill! We can do better by performing a consume operation instead. Consume operations are cool because they eliminate one of thelwsyncinstructions on PowerPC, and one of thedmbinstructions on ARMv7. I’ll write more about data dependencies and consume operations in a future post.

 

使用 C++11 中的靜態初始化器

一些讀者已經知道這篇文章的重點:C ++ 11不需要您跳過以上任何一個步驟即可獲得線程安全的單例。 您可以簡單地使用靜態初始化器。
 
[更新:當心! 正如Rober Baker在評論中指出的那樣,該示例在Visual Studio 2012 SP4中不起作用。 它僅在完全符合C ++ 11標準這一部分的編譯器中工作。

 

Singleton &Singleton::getInstance()
{
    static Singleton instance;
    return instance;
}

 
讓我們回到6.7.6節查看C++11的標準:

如果控制(control)在變量初始化時併發進入聲明,併發執行應等待初始化完成。

 
這要靠編譯器來填充實現細節,而DCLP是顯而易見的選擇。我們不能保證編譯器會使用DCLP,但恰好有些編譯器(也許大多數)會使用。下面是GCC 4.6在使用std=c++0xoption編譯ARM時生成的一些機器代碼。
 
在這裏插入圖片描述
 

Since theSingletonis constructed at a fixed address, the compiler has introduced a separate guard variable for synchronization purposes. Note in particular that there’s nodmbinstruction to act as an acquire fence after the initial read of this guard variable. The guard variable is a pointer to the singleton, and therefore the compiler can take advantage of the data dependency to omit thedmbinstruction.__cxa_guard_releaseperforms a write-release on the guard, is therefore dependency-ordered-before the read-consume once the guard has been set, making the whole thing resilient against memory reordering, just like all the previous examples.

(譯註:這段話是解釋上面那個圖的,本人不太懂。。就不翻譯了)

 

如你所見,我們已伴隨C++11走過了一段漫長的道路。雙重檢查鎖定是一種穩定的模式,而且還遠不止此!
 
就個人而言,我常常想,如果是需要初始化一個單例,最好是在程序啓動的時候做這個事情。但是顯然DCLP可以拯救你於泥潭。而且在實際的使用中,你還可以用DCLP來將任意數值類型存儲到一個無鎖的哈希表。在以後的文章中會有更多關於它的論述。
 
(譯註:無鎖哈希表:http://preshing.com/20130605/the-worlds-simplest-lock-free-hash-table)

 

譯者總結

c++11用很多方式解決了多線程情況下單例模式初始化的問題。但是其中最簡單的就是靜態初始化(最後一個),2020-6-18測試的時候gcc4.8.5編譯器已經支持這種寫法。

 
白了個白。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章