現(xiàn)模板形式的單例類)
前言單例模式要解決的問題是某個(gè)類型在整個(gè)進(jìn)程中只允許存在一個(gè)實(shí)例并提供一個(gè)全局訪問點(diǎn)。把這件事寫成模板是為了讓Config、Logger、ConnectionPool這類類型復(fù)用同一套“唯一實(shí)例 線程安全初始化 禁止拷貝”的邏輯而不是每個(gè)類各抄一遍。圍繞單例有不少誤解。有人說“單例必須用雙重檢查鎖定double-checked locking”但在 C11 之后函數(shù)內(nèi)靜態(tài)變量的初始化本身由標(biāo)準(zhǔn)保證線程安全絕大多數(shù)場(chǎng)景不需要手寫雙重檢查而手寫版本若忘了把指針聲明為std::atomic反而制造數(shù)據(jù)競(jìng)爭(zhēng)屬于未定義行為undefined behavior, UB。有人以為“用了std::unique_ptr就自動(dòng)線程安全”智能指針只負(fù)責(zé)所有權(quán)與釋放不負(fù)責(zé)首次創(chuàng)建的同步。還有人把std::make_uniqueT()直接寫進(jìn)單例模板結(jié)果因?yàn)闃?gòu)造函數(shù)是私有的而編譯失敗——std::make_unique不是這個(gè)類的友元。本文講三件事單例在 C11 之后的線程安全初始化依據(jù)如何用std::unique_ptr/std::shared_ptr寫出模板形式以及析構(gòu)順序、私有構(gòu)造訪問這些容易翻車的地方。代碼以 C17 為基準(zhǔn)涉及 C17 的inline static會(huì)注明本機(jī)沒有編譯環(huán)境未做編譯驗(yàn)證。一、單例的三條硬要求第一構(gòu)造入口必須受控。構(gòu)造函數(shù)應(yīng)為私有或受保護(hù)拷貝構(gòu)造與拷貝賦值應(yīng)刪除避免出現(xiàn)第二個(gè)實(shí)例或意外復(fù)制。第二首次訪問必須線程安全。多個(gè)線程同時(shí)第一次調(diào)用instance()時(shí)只能構(gòu)造一次且其他線程要能看到完整構(gòu)造后的對(duì)象。第三生命周期要明確。單例什么時(shí)候銷毀、銷毀時(shí)能否安全訪問其他全局對(duì)象必須有約定。很多“單例崩潰”不是創(chuàng)建的問題而是程序退出時(shí)靜態(tài)對(duì)象析構(gòu)順序的問題。C11 標(biāo)準(zhǔn)規(guī)定函數(shù)內(nèi)的靜態(tài)局部變量在控制流第一次經(jīng)過其聲明時(shí)初始化如果初始化正在被另一個(gè)線程執(zhí)行其他線程會(huì)阻塞等待初始化完成后才繼續(xù)。這條保證通常被稱為“magic statics”。它讓下面這種寫法成為最簡(jiǎn)單的線程安全懶漢式// 適用于 C17static T instance() {static T obj;return obj;}前提是編譯器實(shí)現(xiàn)了 C11 的這條規(guī)則。極老的工具鏈例如早期的 MSVC可能提供開關(guān)或行為不一致使用前應(yīng)以工具鏈文檔為準(zhǔn)。對(duì)現(xiàn)代 GCC、Clang、MSVC 而言這是標(biāo)準(zhǔn)要求。二、用 unique_ptr 寫模板單例把上面的局部靜態(tài)對(duì)象換成std::unique_ptrT就得到“智能指針持有”的版本。它的好處是所有權(quán)表達(dá)明確單例對(duì)象由模板內(nèi)部持有外部只能拿到引用同時(shí)可以替換刪除器便于測(cè)試時(shí)注入自定義銷毀邏輯。// 適用于 C17#include memorytemplate typename Tclass Singleton {public:Singleton(const Singleton) delete;Singleton operator(const Singleton) delete;static T instance() {// C11 起局部靜態(tài)的初始化是線程安全的static std::unique_ptrT holder(new T());return *holder;}protected:Singleton() default;~Singleton() default;};使用方這樣寫// 適用于 C17class Config : public SingletonConfig {friend class SingletonConfig; // 允許模板訪問私有構(gòu)造public:int port() const { return port_; }private:Config() : port_(8080) {}int port_ 0;};instance()里的new T()出現(xiàn)在SingletonT的成員函數(shù)中而SingletonT是T的友元所以能調(diào)用私有構(gòu)造函數(shù)。這里不能用std::make_uniqueT()它內(nèi)部執(zhí)行new T(...)的函數(shù)不是友元訪問檢查會(huì)失敗。要么像上面這樣在友元上下文里直接new要么在T里提供一個(gè)公有的靜態(tài)工廠要么把構(gòu)造函數(shù)改為 public 并靠約定約束。析構(gòu)方面std::unique_ptrT的默認(rèn)刪除器會(huì)執(zhí)行delete ptr需要T的析構(gòu)函數(shù)可訪問。上面的Config沒有顯式寫析構(gòu)隱式析構(gòu)是公有的所以沒問題。如果你把~Config()設(shè)為私有就必須讓std::default_deleteConfig成為友元或者給unique_ptr換一個(gè)在友元上下文里定義的自定義刪除器。如果希望單例可以被重置測(cè)試?yán)锍S每梢园殉钟姓吒某深惖撵o態(tài)成員并用std::call_once保證只創(chuàng)建一次。C17 的inline static可以讓靜態(tài)成員的定義留在頭文件里避免為每個(gè)實(shí)例化類型寫一遍類外定義// 適用于 C17#include memory#include mutextemplate typename Tclass ResettableSingleton {public:static T instance() {std::call_once(flag_, ResettableSingleton::create);return *holder_;}static void reset() {std::lock_guardstd::mutex lock(mutex_);holder_.reset();flag_ std::once_flag{}; // 允許下次重新創(chuàng)建}ResettableSingleton(const ResettableSingleton) delete;ResettableSingleton operator(const ResettableSingleton) delete;protected:ResettableSingleton() default;~ResettableSingleton() default;private:static void create() { holder_.reset(new T()); }inline static std::unique_ptrT holder_;inline static std::once_flag flag_;inline static std::mutex mutex_;};reset()只應(yīng)在沒有其他線程使用單例時(shí)調(diào)用否則正在使用對(duì)象的線程會(huì)持有懸垂引用。這類接口適合測(cè)試不適合運(yùn)行期隨意調(diào)用。三、shared_ptr 版本與觀察者有些設(shè)計(jì)希望單例的“所有權(quán)”可以被共享例如插件在初始化階段拿到shared_ptr并在自己的生命周期內(nèi)持有。寫法是把持有者換成std::shared_ptrT// 適用于 C17#include memorytemplate typename Tclass SharedSingleton {public:static std::shared_ptrT instance() {static std::shared_ptrT holder(new T());return holder;}SharedSingleton(const SharedSingleton) delete;SharedSingleton operator(const SharedSingleton) delete;protected:SharedSingleton() default;~SharedSingleton() default;};每次調(diào)用返回一個(gè)新的shared_ptr副本引用計(jì)數(shù)增加靜態(tài)holder在程序退出時(shí)釋放自己那一份若外部還持有對(duì)象會(huì)活得更久。這時(shí)的“單例”語義已經(jīng)變成“進(jìn)程內(nèi)唯一的共享對(duì)象”而不是“嚴(yán)格與進(jìn)程同生命周期”。std::weak_ptr在這里的用途是觀察而非持有外部保存weak_ptr需要時(shí)lock()可以知道單例是否已經(jīng)銷毀。但只要有靜態(tài)holder存在它就不會(huì)提前銷毀所以weak_ptr更多用于測(cè)試或可重置的設(shè)計(jì)。使用shared_ptr要留意循環(huán)引用如果單例對(duì)象內(nèi)部又持有指向自己的shared_ptr引用計(jì)數(shù)永遠(yuǎn)不歸零。需要打破環(huán)時(shí)把其中一條邊改成weak_ptr。四、析構(gòu)順序與“單例依賴單例”函數(shù)內(nèi)靜態(tài)對(duì)象在程序退出時(shí)按構(gòu)造的逆序銷毀。如果Logger的析構(gòu)函數(shù)里調(diào)用Config::instance()而Config的單例已經(jīng)先被銷毀那么訪問的就是一個(gè)生命周期已經(jīng)結(jié)束的對(duì)象屬于 UB。這類問題在單進(jìn)程測(cè)試時(shí)不一定暴露在關(guān)閉路徑復(fù)雜時(shí)很難排查。規(guī)避方式有三類。其一讓單例的析構(gòu)函數(shù)不做任何依賴其他單例的事只釋放自身資源。其二對(duì)確實(shí)需要“退出時(shí)按順序清理”的對(duì)象不要依賴靜態(tài)析構(gòu)改為顯式提供shutdown()在main返回前按確定順序調(diào)用。其三把單例改為由main或應(yīng)用容器持有通過引用傳遞徹底避開全局析構(gòu)順序。單例還常被濫用為“全局變量容器”。它隱藏了依賴關(guān)系讓單元測(cè)試難以替換實(shí)現(xiàn)也讓初始化順序變得隱式。如果一個(gè)類型并非在語義上必須唯一優(yōu)先通過構(gòu)造函數(shù)參數(shù)傳入只有日志器、進(jìn)程級(jí)配置這類確實(shí)唯一的設(shè)施才值得單例。常見坑點(diǎn)? 手寫雙重檢查鎖定卻用普通指針static T* p; if (!p) { lock; if (!p) p new T; }。? 普通指針的讀寫產(chǎn)生數(shù)據(jù)競(jìng)爭(zhēng)是 UB用函數(shù)內(nèi)靜態(tài)對(duì)象或把指針改為std::atomicT*并配合正確的內(nèi)存序。? 在模板單例里寫std::make_uniqueT()而T的構(gòu)造函數(shù)是私有的。?make_unique不是友元在友元上下文里寫new T()或提供公有工廠。? 只刪除拷貝構(gòu)造不刪除拷貝賦值。? 兩個(gè)都 delete否則仍可能賦值出第二個(gè)邏輯實(shí)例。? 把模板靜態(tài)成員的定義放進(jìn).cpp。? 模板在每個(gè)實(shí)例化點(diǎn)都需要定義靜態(tài)成員應(yīng)放在頭文件C17 起可用inline static。? 單例 A 的析構(gòu)函數(shù)里調(diào)用單例 B。? 靜態(tài)析構(gòu)順序不可控析構(gòu)函數(shù)只做自身清理跨對(duì)象清理顯式編排。?instance()返回T*調(diào)用方順手delete。? 返回T或返回shared_ptr明確所有權(quán)不要讓外部拿到可刪除的裸指針。? 繼承SingletonT后忘記在T里聲明friend class SingletonT;。? 私有構(gòu)造無法被模板調(diào)用友元聲明是模板單例成立的前提。? 把單例當(dāng)全局變量用任何地方都能改狀態(tài)。? 限定接口、把可變狀態(tài)收進(jìn)成員函數(shù)并加同步能用依賴注入就不要用單例??偨Y(jié)方案創(chuàng)建時(shí)機(jī)線程安全來源適用場(chǎng)景局部靜態(tài)對(duì)象static T obj;首次調(diào)用C11 靜態(tài)初始化規(guī)則首選簡(jiǎn)單且可靠局部靜態(tài)unique_ptrT首次調(diào)用同上需要自定義刪除器或顯式所有權(quán)call_once 靜態(tài)成員首次調(diào)用std::call_once需要reset或延遲參數(shù)局部靜態(tài)shared_ptrT首次調(diào)用同上外部需要共享所有權(quán)餓漢式全局對(duì)象程序啟動(dòng)靜態(tài)初始化啟動(dòng)期就要用、無依賴模板單例的骨架很短刪除拷貝、私有構(gòu)造、一個(gè)返回引用的靜態(tài)函數(shù)、一個(gè)智能指針持有者。真正決定它是否可靠的是三個(gè)細(xì)節(jié)創(chuàng)建是否只發(fā)生一次、私有構(gòu)造是否對(duì)模板開放、銷毀時(shí)是否還在訪問別的全局對(duì)象。把這三件事處理清楚比記住某種“標(biāo)準(zhǔn)寫法”更有用。