1. 程式人生 > 實用技巧 >好好說說c++記憶體序--以單例模式為例子

好好說說c++記憶體序--以單例模式為例子

1.首先寫一個單例模式,面試中很容易遇見的,一聽到單例,小猿忍不住投去鄙夷的目光,不過他還是挺謹慎的,並沒有立即下筆,思索一番後,決定把自己曾經在公司某久經考驗的框架裡看過的一段程式碼搬運過來:  

template<typename T>
class XX_Singleton {
public:
    typedef T  instance_type;
    typedef volatile T volatile_type;

    static T* getInstance() {
        // 加鎖, 雙check機制, 保證正確和效率
        if(!_pInstance) {
            std::lock_guard<mutex> lock(_tl);
            if(!_pInstance) {
                _pInstance = new T;
            }
        }
        return (T*)_pInstance;
    }

    virtual ~XX_Singleton() {
    }; 
protected:
    static mutex _tl;
    static volatile T* _pInstance;

protected:
    XX_Singleton() {
    }
    XX_Singleton (const TC_Singleton &); 
    XX_Singleton& operator=(const TC_Singleton &);
};

// 靜態成員變數的定義
template <typename T>
volatile T* XX_Singleton<T>::_pInstance = nullptr; 

大部分磚工應該見過類似上面這段程式碼實現的singleton,它採用DCLP(Double-CheckedLockingPattern),期望做到多執行緒安全的同時又兼顧效能。然而,著名 c++ 專家Scott Meyers早在2004年就寫過一篇論文C++ and the Perils of Double-Checked Locking專門討論過上面這段程式碼,文中第 3 節和第 4 節主要從記憶體順序(memory order)角度指出了這種實現存在問題,所以這實際上是一種錯誤的實現。然而糟糕的是,作者指出 c++ 標準(2004年c++11還沒出現)在語言層面並沒有一個可用的機制來獲得這裡需要的 memory order,正確的實現需要依賴系統相關的庫,例如linux系統下的pthread庫。在c++11出現後,這個問題得到了解決,c++11從語言層面提供了編寫跨平臺多執行緒程式所需的基礎元件,例如多執行緒記憶體模型、原子變數、thread等,本文主要討論原子變數(atomic)以及memory order相關的內容,在討論過程中相關的地方會分析這個singleton實現的問題以及正確的做法。  

(1) atomic型別和std::memory_order

  c++11標準在標頭檔案<atomic>裡定義了模板型別atomic<T>,它封裝了原子操作以及memory order相關的特性,並對各種整型(char、short、int、long等)、指標等型別提供了特化版本。需要注意的是,atomic<T>型別既不可copy也不可move,特化版本在atomic<T>的成員函式之外,一般會提供額外的成員函式,例如atomic<int>有額外的成員函式fetch_add、fetch_sub、fetch_and、fetch_or 等。

// 標頭檔案<atomic>
template<class T> struct atomic;

// 標頭檔案<memory>
template<class T>
struct atomic<T*>;

  多執行緒同時訪問(修改或讀取)同一個原子變數,其行為是well-defined(相應的,多執行緒同時讀寫非原子變數,其行為是undefinde behavior)。除此之外,對原子變數的訪問,還能建立執行緒間的同步關係,並對非原子變數型別的記憶體訪問提供一定的順序保證。

原子變數有兩個最基本的成員函式,load讀取原子變數的值,store寫入某個值到原子變數,讀取和寫入都是原子的:

  這些操作都有一個std::memory_order型別的引數,它是一個enum型別,定義了c++11標準裡不同的memory order:

typedef enum memory_order {
    memory_order_relaxed,
    memory_order_consume,
    memory_order_acquire,
    memory_order_release,
    memory_order_acq_rel,
    memory_order_seq_cst
} memory_order;

  這些列舉值在多執行緒的記憶體訪問順序上提供了不同程度的約束,其中memory_order_relaxed是最弱的,最強的是memory_order_seq_cst(它也是所有原子操作的預設引數)。關於這些memory_order值的含義以及它們對記憶體訪問順序的約束,後面會逐步來討論。

c++11對原子變數的訪問可以分為如下幾類:

relaxed operation:

以memory_order_relaxed為引數的load、store、read-modify-write(例如fetch_add)操作稱為relaxed operation,它只保證操作的原子性,不帶來任何memory order的約束。

release operation:

以memory_order_release(或更強的memory order,例如memory_order_seq_cst)為引數的store操作稱為release operation(Mutex型別物件的lock()操作也是release operation,這裡Mutex泛指符合互斥變數要求的型別,例如std::mutex,std::timed_mutex都符合)

acquire operation:

以memory_order_acquire(或更強的memory_order,例如memory_order_seq_cst)為引數的load操作稱為acquire operation(Mutex型別物件的unlock()操作也是release operation,這裡Mutex泛指符合互斥變數要求的型別,例如std::mutex,std::timed_mutex都符合)

consume operation:

以memory_order_consume(或更強的memory_order,例如memory_order_seq_cst)為引數的load操作稱為consume operation。