Singleton 1 1 1
Co design pattern řeší GoF Proč jedna instance? Proč global access? Singleton – ensure a class only has one instance, and provide a global point of access to it Proč jedna instance? mít více instancí může být nežádoucí / zbytečné / nebezpečné Proč global access? časté použití z mnoha míst v programu Vhodné použití Database Connection Pool Logger Keyboard Factories ze vzoru Abstract Factory Pattern Herní prostředí – např. mapa ve hře 2 2 2
ALE ! … Obsah Základní implementace je nesmírně jednoduchá class Singleton { static Singleton instance = new Singleton(); Singleton() {} public static Singleton getInstance() { return instance; } public void doSomething() { //doing something Základní implementace je nesmírně jednoduchá Jak správně napsat Singleton implementace v C# / C++ se trochu liší kvůli specifikům těchto jazyků Více vláknové aplikace jak v programu s více vlákny zajistit jednu instanci Dědičnost jak se postavit k dědičnosti Destrukce jak vyřešit problém destrukce Závislosti a rozšiřitelnost jaké další problémy souvisejí s tímto vzorem ALE ! … 3 3 3
Jak může Singleton vypadat? První pokus + zaručení „jedné“ instance globální přístup - statická metoda nemůže být virtuální class Singleton { Singleton() = delete; public: static void doSomething() { //doing something }; static class Singleton { public static void doSomething() { //doing something } C++ C# 4 4 4
Třída se o jedinečnost své instance bude starat sama Jak může Singleton vypadat? Druhý pokus Třída se o jedinečnost své instance bude starat sama class Singleton { static Singleton instance_; Singleton() {}; public: static Singleton& getInstance() { return instance_; } void doSomething() { //doing something }; class Singleton { static Singleton instance = new Singleton(); Singleton() {} public static Singleton getInstance() { return instance; } public void doSomething() { //doing something C++ C# - instance je zkonstruována vždy nedefinované pořadí inicializace a destrukce +/- instance je zkonstruována až před prvním přístupem do třídy (ale bohužel i v případě, že například referencujeme jinou položku) 5 5 5
konstruktor Singletonu je private, musíme předat lambdu Jak může Singleton vypadat? Třetí pokus (C#) typ Lazy od .NET 4 konstruktor Singletonu je private, musíme předat lambdu class Singleton { Singleton() {} public static Singleton getInstance(){ return Nested.instance; } class Nested { public static Singleton instance = new Singleton(); public void doSomething() { //doing something class Singleton { static Lazy<Singleton> instance = new Lazy<Singleton> ( () => new Singleton()); public static Singleton getInstance() { return instance.Value; } public void doSomething() { //doing something + plně lazy instanciace všechny předchozí vlastnosti, které jsme po singletonu vyžadovali 6 6 6
Jak může Singleton vypadat? Třetí pokus (C++) instance zkonstruována až při prvním průchodu funkcí varianta preferovaná v literatuře (najdete také v GoF) Meyersův Singleton class Singleton { Singleton() {}; public: static Singleton& getInstance() { static Singleton instance; return instance; }; void doSomething() { //doing something class Singleton { Singleton() {}; static Singleton* instance_; public: static Singleton* getInstance() { if (instance_ == nullptr) instance_ = new Singleton(); return instance_; }; void doSomething() { //doing something + lazy instanciace + lazy instanciace řízení destrukce objektu syntaxe víceméně stejná jako u jiných jazyků 7 7 7
Implementace v C++ se vším všudy class Singleton { private: Singleton() { /* … */ }; ~Singleton() { /* … */ }; static Singleton* instance_; public: static Singleton* getInstance() { if (instance_ == nullptr) instance_ = new Singleton(); return instance_; }; Singleton (const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; void doSomething() { //doing something Singleton* Singleton::instance_(nullptr); int main (int argc, int ** argv) { Singleton::getInstance()->doSomething(); /* … */ } konstruktor i destruktor jsou privátní instanci si spravuje třída sama lazy instanciace nezapomenout smazat copy assignment a copy constructor nezapomenout na statickou inicializaci uvnitř .cpp!! (One Definition Rule) jednoduchý příklad použití 8 8 8
Singleton a více vláknové aplikace Řešení: Zámky class Singleton { Singleton() {}; static Singleton* instance_; public: static Singleton* getInstance() { if (instance_ == nullptr) instance_ = new Singleton(); return instance_; }; void doSomething() { //doing something Problém: Data races 9 9 9
Singleton a více vláknové aplikace Některé předchozí implementace tento problém nemají Meyersův singleton – v c++ máme zajištěno, že se statická proměnná inicializuje vždy v jednom vlákně C# statická proměnná ve vnořené třídě – opět máme zajištěnou konstrukci pouze jedním vláknem třída Lazy<T> - opět konstrukce se provádí v jednom vlákně static Singleton& getInstance() { static Singleton instance; return instance; }; public static Singleton getInstance(){ return Nested.instance; } class Nested { public static Singleton instance = new Singleton(); static Lazy<Singleton> instance = new Lazy<Singleton> (() => new Singleton()); 10 10 10
Singleton a více vláknové aplikace – první pokus Zámky fungují, ale … zamykáme i po zkonstruování instance řešení => double-checked locking pattern class Singleton { Singleton() {}; static Singleton* instance_; static std::mutex lock_; public: static Singleton* getInstance() { lock_.lock(); if (instance_ == nullptr) instance_ = new Singleton(); lock_.unlock(); return instance_; }; void doSomething() { //doing something 11 11 11
Double Checked Locking Pattern Singleton a více vláknové aplikace – druhý pokus Už správně? bohužel, C++ nám negarantuje, že je zápis do proměnné typu pointer atomický (jiné vlákno v ní může vidět libovolný mezistav) class Singleton { Singleton() {}; static Singleton* instance_; static std::mutex lock_; public: static Singleton* getInstance() { if (instance_ == nullptr) { lock_.lock(); if (instance_ == nullptr) instance_ = new Singleton(); lock_.unlock(); } return instance_; }; void doSomething() { //doing something Double Checked Locking Pattern Problém: Nový data race 12 12 12
Pro odstranění data race stačí pointer zabalit do std::atomic Singleton a více vláknové aplikace – třetí pokus class Singleton { Singleton() {}; static std::atomic<Singleton*> instance_; static std::mutex lock_; public: static Singleton* getInstance() { if (instance_.load() == nullptr) { lock_.lock(); if (instance_.load() == nullptr) instance_.store(new Singleton()); lock_.unlock(); } return instance_; }; void doSomething() { //doing something Pro odstranění data race stačí pointer zabalit do std::atomic 13 13 13
Singleton a dědičnost – první možnost class Singleton { /* constructors, destructors, etc. */ public: static Singleton* instance() { if (instance_ == nullptr) { if (Config::SingletonName == "MAGIC") { instance_ = new MagicSingleton(); } else { instance_ = new MuggleSingleton(); return instance_; virtual void doSomething() = 0; }; class MagicSingleton : public Singleton { friend class Singleton; virtual void doSomething() { /* ... */ } class MuggleSingleton : public Singleton { /* different implementation */ Vytvoří se požadovaná instance podle konfigurace programu Problém: rodič musí vědět o všech potomcích Společné rozhraní, které musí všichni potomci implementovat Aby mohla bázová třída vytvářet instance, musí být friend Implementace rozhraní 14 14 14
Singleton a dědičnost – druhá možnost Varianta uvedená také v GoF class Singleton { static Singleton* instance_; static std::map<const char*, Singleton*> registry_; protected: Singleton(); static Singleton* lookUp (const char* name); void register (const char* name, Singleton* s); public: static Singleton* instance() { if (instance_ == nullptr) { instance_ = lookUp(Config::SingletonName); } return instance_; }; class MagicSingleton: public Singleton { static MagicSingleton instance_; MagicSingleton() { /* … */ register("MagicSingleton", &instance_); Registr udržuje názvy potomků a pointery na jejich instance Registruje instanci potomka pod zadaným jménem Nalezne a vrátí instanci podle aktuální konfigurace programu Problém: pro každého potomka existuje instance, používáme však jen jednu z nich. Potomek se při vytvoření zaregistruje u rodiče 15 15 15
Singleton a dědičnost – třetí možnost class Singleton { static Singleton* instance_; protected: Singleton(); public: template <typename T> static Singleton* instance() { if (instance_ == nullptr) { instance_ = new T(); } return instance_; virtual void doSomething() = 0; }; class MagicSingleton: public Singleton { friend class Singleton; MagicSingleton() {}; void doSomething() { … }; int main() { Singleton::instance<MagicSingleton>()-> …; Skutečná instance se určí podle parametru šablony Konstruktor je private, opět musí být třída Singleton friend Problém: hardcoded Jednoduché použití 16 16 16
Destrukce Zodpovědnost za zrušení Kdy je objekt zrušen? jak se správně postarat o zrušení objektu pozor na memory, resource leaks Kdy je objekt zrušen? jaké je správně pořadí rušení objektů Některé objekty je potřeba mít přístupné vždy. jak vyřešit situaci kdy už byl odstraněn ještě potřebný objekt 17 17 17
Destrukce pštrosem Pštrosí řešení (leaking singleton, ostrich singleton) problém destrukce ignorovat statická paměť se uvolní automaticky při ukončení procesu jako každá kulturní třída by měl mít destruktor zápis do logu, uzavření spojení, odhlášení, ... lze vést diskuze o tom co je a co není leak (memory leak, resource leak) V čem je problém? destruktor se nikdy nezavolá ukládáme statický ukazatel, ne statický objekt Foto: Altrendo Travel, Getty Images 18 18 18
Destrukce Killer Idea: class SingletonKiller { public: void setSingleton(Singleton* instance) { m_instance = instance; } Singleton* getSingleton() { return m_instance; ~SingletonKiller() { delete m_instance; private: static Singleton* m_instance; }; class Singleton { static Singleton& instance(); Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; Singleton(); ~Singleton(); static SingletonKiller singletonKiller; Singleton* SingletonKiller::m_instance = nullptr; SingletonKiller Singleton::singletonKiller; Idea: Ukazatel na Singleton zabalíme do třídy starající se o jeho destrukci. Kompilátor zruší statický objekt singletonKiller, který ve svém destruktoru zabíjí instanci objektu Singleton. 2. Destruktor killera Killer obsahuje ukazatel na náš singleton 3. Destruktor singletonu 1. Destrukce statické proměnné Singleton& Singleton::instance() { if (!singletonKiller.getSingleton()) { singletonKiller.setSingleton( new Singleton); } return *singletonKiller.getSingleton(); 19 19 19
Scott Meyers Scott Meyers místo operátoru new se použije statická lokální proměnná instanci nedržíme ve statickém ukazateli funkce vracející referenci na statický objekt ve funkci class Singleton { public: static Singleton& instance() { static Singleton inst; return inst; } Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; private: Singleton() { /* ... */ } ~Singleton() { /* ... */ } }; int main(int argc, char** argv) { Singleton& s = Singleton::instance(); /* ... */ 2. Inicializace statického objektu pouze při prvním průchodu 4. Návrat zkonstruovaného objektu 3. Konstruktor objektu 6. Destruktor objektu 1. Zavolá se metoda 5. Konec programu, destrukce statických proměnných 20 20 20
Klíčové slovo static v jazyce C++ Kde žijí statické data? jakou mají přesně životnost Kdy jsou tato data inicializována? pořadí inicializace lokální statické proměnné Kdo se postará o následnou destrukci? 21 21 21
Funkce atexit( ) Odstraňování statických proměnných: LIFO – nejdříve se odstraní naposledy inicializované int atexit(void*(exit_function)()); při vytváření objektu se zaregistruje funkce pro zrušení při ukončení programu se postupně zavolají registrované funkce Singleton& Singleton::instance() { extern void __constructSingleton(void* memory); extern void __destroySingleton(); static bool __initialized = false; static char __buffer[sizeof(Singleton)]; if (!__initialized) { __constructSingleton(__buffer); atexit(__destroySingleton); __initialized = true; } return *reinterpret_cast<Singleton*>(__buffer); Funkce generované kompilátorem Proměnné generované kompilátorem __buffer obsahuje Singleton Volání funkce __constructSingleton() zavolá konstruktor na paměti __buffer zaregistruje destrukci 22 22 22
Pořadí destrukce Dead reference problem: při nevhodném pořadí mohou vzniknout reference na neexistující objekty příklad: singletony Keyboard, Display, Log vytvoření instance Log pouze při chybě destrukce Logu by měla následovat až po destrukcích ostatních singletonů nebo aspoň poznat problém a slušně umřít Inicializace Keyboard Inicializace Display s chybou Inicializace Log a zapsání chyby Konec programu Destrukce Log Destrukce Display Destrukce Keyboard s chybou Reference neexistujícího objektu Log 23 23 23
Inicializace přesunuta do privátní metody Detekce mrtvé reference Detekce destruovaného objektu přidáme statický příznak, který změníme při destrukci class Singleton { public: static Singleton& instance() { if (!m_instance) { if (m_destroyed) throw /* ... */ else create(); } return *m_instance; Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; private: static void create() { static Singleton inst; m_instance = &inst; Singleton() {...} ~Singleton() { m_destroyed = true; m_instance = nullptr; static bool m_destroyed; static Singleton* m_instance; }; Singleton* Singleton::m_instance = 0; bool Singleton::m_destroyed = false; Detekce problému Inicializace přesunuta do privátní metody Nastavení zrušení Příznak zrušení 24 24 24
Fénix Detekce někdy nestačí co když chceme přistupovat k singletonu kdykoliv? znovuvytvoření při detekci zrušeného objektu příklad: Keyboard a Display obyčejné singletony, Log Fénix C++: paměť statických objektů zůstane alokována až do konce běhu programu problém: stav starého mrtvého Singletonu je navždy ztracen class Singleton { /* ... */ void killPhoenix(); }; void Singleton::onDeadRef() { create(); new(m_instance) Singleton; atexit(killPhoenix); m_destroyed = false; } void Singleton::killPhoenix() { m_instance->~Singleton(); zbytek třídy nezměněn při detekci zrušení se uloží reference na paměť zrušeného objektu placement new zavolání konstruktoru na daném místě v paměti registrace destruktoru fénixu explicitní zavolání destruktoru nelze volat delete! 25 25 25
Prioritní fronta na zabití Dlouhověkost Problémy Fénixe ztráta stavu, uložení, uzavření, ... nejasnost C++ standardů ohledně funkce atexit() Singleton s dlouhověkostí při vytváření singletonu priorita destrukce log bude mít větší dlouhověkost explicitní mechanismus destrukce objektů nelze použít destrukci řízenou kompilátorem - pouze dynamické objekty Mechanismus by měl fungovat na jakékoliv (dynamické) objekty class MagicSingleton { /* ... */ }; class CoolClass { /* ... */ }; CoolClass* m_global_object(new CoolClass); template <typename T> void setLongevity(T* object, int longevity); int main(int argc, char** argv) { setLongevity(&MagicSingleton::instance(), 5); setLongevity(m_global_object, 6); /* ... */ } Šablona pro nastavování dlouhověkosti Prioritní fronta na zabití Po ukončení programu se objekty zabíjejí v tomto pořadí 26 26 26
Implementace dlouhověkosti Prioritní fronta při stejných prioritách se chová jako zásobník C++ pravidlo: dříve inicializované objekty se destruují později neexistuje společný předek, registrační funkce je šablona ukazatel na abstraktního předka šablon obsahujících ukazatel na objekt Virtuální držák Virtuální destruktor LifeTimeTracker virtual ~LifeTimeTracker() longevity Šablona instanciována a objekt vytvořen šablonou SetLongevity ConcreteLTT<T> ~ConcreteLTT() T ~T() delete obj; 27 27 27
vlastní fronta na zabití Implementace dlouhověkosti class LifetimeTracker { public: LifetimeTracker(unsigned int x): longevity(x) {} virtual ~LifetimeTracker() = 0; friend inline bool Compare( unsigned int longevity, const LifetimeTracker* p) { return p->longevity_ > longevity; } private: unsigned int longevity; }; inline LifetimeTracker::~LifetimeTracker() {} typedef LifetimeTracker** TrackerArray; extern TrackerArray pTA; extern unsigned int elements; template <typename T> struct Deleter { static void DeleteIt(T* pObj) { delete pObj; virtuální držák umí zabíjet... ... a porovnávat stáří vlastní fronta na zabití způsob zabití defaultně delete lze i free, ... Co bylo dříve - vejce nebo slepice? TrackerArray by se měl chovat jako Singleton kdo vyřeší problému singletonu u TrackerArray? 28 28
Použití Možné použití Ale hlavně používat s rozumem: po celou dobu běhu může existovat pouze jedna instance daného objektu zajištění přístupu k sdílenému zdroji, ke kterému: může být přistupováno z různých častí systému může být přistupováno i z vícerých vláken Ale hlavně používat s rozumem: singleton reprezentuje stav, který je globální, sdílený a mutable – to se sebou přináší různé problémy (například u testování) bezpečné místo použití je proto u třídy, která neobsahuje stav přímo související se samotnou aplikací (například už vzpomínaný Logger) 29 29 29
Příklad problému Máme jedno připojení k databázi Ale přijde šéf... zatím všechno v pořádku void saveOrder(const Order& o) { // We will use our Singleton Database::instance().storeOrder(o); } void saveOrder(const Order& o) { if (order.local()) { Database::instance().storeOrder(o); } else { Database2::instance().storeOrder(o); Ale přijde šéf... „Ukládejte zahraniční objednávky jinam.“ co teď? Singleton se velmi špatně rozšiřuje téměř každý pokus o řešení vede na tvorbu další instance možné řešení: abstrahujeme připojení k databázím do jiného objektu Singleton schovává závislosti nevíme jaké objekty ve skutečnosti používá částečné řešení: Dependency injection 30 30 30
Související návrhové vzory Abstract Factory, Builder, Prototype častá implementace pomocí singletonu Facade v případě potřeby pouze jednoho vstupu do systému State stavové objekty jsou často singletony 31 31 31