# Singleton- 性質討論 2022/07/06 吳宣誼 ## 描述 引述[3]有些類別應當只有一個實體,這種實體在程式開始運行時就存在,並且只有在程式結束時才被清除。這些物件有時是檔案系統,有時是工廠物件,有時是負責追蹤資源的管理器,他們的生命期比一般物件更長,如果生成的物件超過一個就是邏輯上的嚴重錯誤。 ## 目的 GoF著作中這樣描述著singleton「保證一個class只有一個實體(instance)存在,並且為它提供單一存取點(global access point)」,這段描述中只說明了singleton的操作行為,但在實務上singleton仍需考慮一些實務上的議題,包括 * 初始化及釋放問題,特別是當帶有系統資源時 * 初始化多執行緒問題 * 數個Singleton之間相依性問題 本文參考書目中的一些討論,並加以整理,以C++為主,書目中都指出singleton不存在著「最佳」方案 [1]Design Patterns - Elements of Reusable Object-Oriented Software.(中譯本:物件導向設計模式- 可再利用物件導向軟體之要素) [2] Modern C++ Design - Generic Programming and Design Patterns Applied. Andrei Alexandrescu (中譯本:C++設定新思維- 泛型編程與設計範式之應用) [3] Agile Software Development: Principles, Patterns, and Pracetices, Robert C. Martin (中譯本:敏捷軟體開發- 原則、樣式及實務) ## 實作手法 GoF著作中,描述了下列c++手法實現singleton,實作重點主要集中在「產生與管理一個獨立物件」 ```c++ class Singleton { public: static Singleton* getInstance(); private: Singleton(); Singleton(const Singleton&); Singleton& operator=(const Singleton&); ~Singleton(); static Singleton* _instance; }; Singlton* Singleton::_instance = nullptr; Singlton* Singleton::getInstance() { if(_instance == nullptr) // 1 _instance = new Singleton(); // 2 return _instance; // 3 } ``` 透過將建構式(constructor)設為private,客戶端無法產生實體,也就是說Singleton自身管理了物件唯一性的議題,外界只能透過Singleton::getInstance()的方式存取。這裡也指出getInstance()利用了lazy策略,使得Singleton在需要時才被產生,若是singleton的產生成本昂貴,則效果愈明顯。 ## 靜態資料+靜態函式 = Singleton ? Monostate 是另一個達成單一化的方法,即利用靜態成員變數及靜態成員函式,[3]中有著這樣的描述「Singleton樣式強調「結構的單一化」,防止超過一個的實體被建構,而Monostate則強調「行為上的單一化」而不在結構上作限制」,靜態函式一般作為不具副作用的utility函式呼叫,或者靜態資料本身是一個狀態機。Monostate主要的問題是不能成為虛擬函式,如果不開放程式碼就很難新增或改變行為。 ## 多緒問題 假設我們有一個程式,其中兩個執行緒會存取上述的Singleton,因為lazy策略,若第一個執行緒進入檢查條件且未產生instance時,第二個執行緒也跟著進入則會產生第二個實體造成記憶體洩漏,這個一個典型的競速問題(race condition),令人直接會用下面這個方法 ```c++ Singleton* Singleton::getInstance() { Lock guard(class_mutex); if(_instance == nullptr) _instance = new Singleton(); return _instance; } ``` 這個方法正確但缺乏效率,原因為於每次存取getInstance()時都會引發同步物件的加解鎖,無論這個類別級的mutex是如何被實作出來,比起簡單的if條件,這個操作都是[額外的開銷](https://stackoverflow.com/questions/3652056/how-efficient-is-locking-an-unlocked-mutex-what-is-the-cost-of-a-mutex) [2]引用了另一個方法,即雙檢測鎖定 **Double-Checked Locking** ```c++ Singleton* Singleton::getInstance() { if(_instance == nullptr) { Lock guard(class_mutex); if(_instance == nullptr) _instance = new Singleton(); } return _instance; } ``` ## 釋放Singleton Singleton應該在什麼時候摧毀自己?GoF著作中沒有討論這個問題,實際上若Singleton未被刪除其實也不會帶來太大的危害,原因在於現代的作業系統在行程結束時會釋放所有使用的記憶體。比起記憶體洩漏(memory leak),資源洩漏(resource leak)更有害,它可能是網路連結、檔案IO的handle、資料庫存取,外部行程等,需唯一避免的作法是在程式關閉時正確刪除Singleton物件,並且確保刪除後不會有人再去取用它 **另一種實現手法[2](c++, Meyers singleton)** 摧毀Singleton的最簡單方案是利用編譯器機制,使用了一個區域靜態變數 ```c++ Singleton& Singleton::getInstance() { static Singleton instance; return instance; } ``` 跟據[2]的說法,這段程式碼利用了編譯器使用atexit()的技巧,使得解構的順序為先產生的物件後摧 毀,而[2]也利用了類似機制設計了一個Phoenix Singleton 使得物件在摧毀後再復活。 > 在g++與vs2019環境中相同的程式碼都有正確的解構式呼叫順序,但這並不保證所有編譯器都有類似作法 ## 相依性問題 [1][2]都指出了全域物件分散在多個編譯單元中,對於這種「壽命受編譯器控制」的物件,C++並沒有定義這些物件建構的執行順序,所以Singleton之間若存在依存關系則容易出錯。[2]提出了一個「帶壽命的singleton」以建立一個依存管理器追蹤。另一篇文章中則提出利用相依性建立拓蹼排序建構與解構關係 ## 簡單?複雜?以Game Engine為例 在一個大型系統中通常包含了多個子系統,當系統開始時每個子系統也會被以某種順序進行設定或初始化,當結束時,則以相反的順序清除釋放。這裡用一個相對簡單的方法解決相依性問題, 即以另一個更為上位的存在進行手動相依性排除,也就是說將singleton的管理從自身移轉出來,以一個Game Engine為例 ``` c++ class RenderManager // Singleton { public: static RenderManager* getInstance(); protected: friend class Main; RenderManager(){ /*do nothing;*/ } RenderManager(const RenderManager&); virtual ~RenderManager(){ /*do nothing;*/ } virtual void startUp(); virtual void cleanUp(); }; class MemoryManager { /* similar */ }; //... // main.cpp class Main { public: void startUp() { // start systen in cirrect order aMemoryManager->startUp(); aFileSystemManager->startUp(); aRenderManager->startUp(); aPhysicsManger->startUp(); // ... } void runMain() { } void cleanUp() { // clean system in reverse order aPhysicsManger->cleanUp(); aRenderManager->cleanUp(); aFileSystemManager->cleanUp(); aMemoryManager->cleanUp(); // ... } private: RenderManager *aRenderManager; //... }; void main() { Main m; m.startUp(); m.run(); m.cleanUp(); } ``` 這裡解決了一些事,以子系統RenderManager來說 * 保證只有一個實體 (在Main中) * 單一存取點getInstance() * 無初始化執行緒問題 * 子系統相依問題由手動排除 多的好處 * Main宣告在main.cpp中為不可見,即不會有第二個Main實例 * 子系統可以被繼承並利用更加一般化的startUp作初始化 * 避免利用複雜機制解除相依性問題,並且由程式碼本身傳達了這個意圖