Singleton 1 1 1
Co je singleton? Definice Proč jedna instance? Proč globální přístup? Třída má právě 1 instanci Existuje globální přístup k této instanci Proč jedna instance? Více instancí může být nežádoucí / zbytečné / nebezpečné Proč globální přístup? Použití z mnoha míst v programu Vhodné použití Logger Databázové spojení 2 2 2
Co je singleton? Definice Traktorista ! Gobálny namespace Třída má právě 1 instanci Existuje globální přístup k této instanci Traktorista ! Gobálny namespace Garden instance = new Garden(); void A() { instance.doPlow(); } void B() { instance.doPlant(); ! Více možných instancí TODO: obrázok traktora TODO: vykríčníky pri boxoch 3 3 3
První pokus – Statická třída class Singleton { Singleton() = delete; public: static void doSomething() { // Useful code } }; static class Singleton { public static void DoSomething() { // Useful code } } C++ C# + Zaručení jedné "instance" Globální přístup - Statické metody jsou nevirtuální 4 4 4
Druhý pokus – statická instance // Singleton.h class Singleton { private: static Singleton instance_; Singleton() {}; public: static Singleton& getInstance() { return instance_; } void doSomething() { // Useful code } }; C++ One Definition Rule // Singleton.cpp #include "Singleton.h" Singleton Singleton::instance_; 5 5 5
Druhý pokus – pořadí inicializace (C++) // Singleton.h class Singleton { private: static Singleton instance_; Singleton() {}; public: static Singleton& getInstance() { return instance_; } int doSomething() { // Useful code } }; Pořadí: global instance_ vs. instance_ global Řešení: Inline static? Meyer's singleton // Singleton.cpp #include "Singleton.h" Singleton Singleton::instance_; // SomeFile.cpp #include "Singleton.h" int global = Singleton::getInstance().doSomething(); 6 6 6
Druhý pokus – statická instance // Singleton.h class Singleton { private: static Singleton instance_; Singleton() {}; public: static Singleton& getInstance() { return instance_; } void doSomething() { // Useful code } }; inline Singleton Singleton::instance_; C++ C++17 One Definition Rule // Singleton.cpp Singleton Singleton::instance_; 7 7 7
Druhý pokus – statická instance // Singleton.h class Singleton { private: static Singleton instance_; Singleton() {}; public: static Singleton& getInstance() { return instance_; } void doSomething() { // Useful code } }; inline Singleton Singleton::instance_; C++ class Singleton { static Singleton instance = new Singleton(); private Singleton() {}; public static Singleton Instance { get { return instance; } } public void doSomething() { // Useful code public static void aux() { } C# - Instance je zkonstruována vždy - Nedefinované pořadí inicializace a destrukce +/- Instance je zkonstruována před prvním přístupem k libovolné položce třídy 8 8 8
Třetí pokus – statický pointer (C++) class Singleton { private: inline static Singleton *instance_ = nullptr; Singleton() {}; public: static Singleton& getInstance() { if (instance_ == nullptr) { instance_ = new Singleton; } return *instance_; } void doSomething() { // Useful code }; + lazy instanciace syntaxe podobná jiným jazykům 9 9 9
Třetí pokus (C#) + Plně lazy instanciace Typ Lazy (.NET 4) class Singleton { private Singleton() {} public static Singleton Instance { get { return Nested.Instance; } } private class Nested { public static Singleton Instance = new Singleton(); public void DoSomething() { // Useful code public static void aux() { // Useful code } class Singleton { private Singleton() {} private static Lazy<Singleton> instance = new Lazy<Singleton> ( () => new Singleton()); public static Singleton Instance { get { return instance.Value; } } public void DoSomething() { // Useful code public static void aux() { // Useful code } + Plně lazy instanciace 10 10 10
Implementace v C++ se vším všudy class Singleton { private: Singleton() { /* … */ } ~Singleton() { /* … */ } inline static Singleton* instance_ = nullptr; 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 }; int main() { Singleton::getInstance().doSomething(); } Privátní konstruktor a destruktor Smazání copy konstruktoru a assignment operatoru 11 11 11
Problémy ALE ! … Základní implementace je jednoduchá Multithreading Jak předíst data race-um Dědičnost Destrukce ALE ! … 12 12 12
Singleton a více vláknové aplikace Fungující implementace C++ Meyersův singleton C# Statická proměnná ve vnořené třídě – statické konstruktory jsou thread-safe Třída Lazy<T> - konstrukce je thread-safe public static Singleton getInstance() { return Nested.instance; } class Nested { public static Singleton instance = new Singleton(); static Lazy<Singleton> instance = new Lazy<Singleton> (() => new Singleton()); 13 13 13
Singleton a více vláknové aplikace 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 race Řešení: Zámky 14 14 14
První pokus (C++ multithreading) class Singleton { private: Singleton() {} inline static Singleton* instance_ = nullptr; static std::mutex lock_; public: static Singleton& getInstance() { lock_.lock(); if (instance_ == nullptr) instance_ = new Singleton(); lock_.unlock(); return *instance_; } }; Zámky fungují, ale … Zamykáme i pro zkonstruování instance Řešení => double-checked locking pattern 15 15 15
Druhý pokus (C++ multithreading) class Singleton { private: Singleton() {} inline static Singleton* instance_ = nullptr; static std::mutex lock_; public: static Singleton& getInstance() { if (instance_ == nullptr) { lock_.lock(); if (instance_ == nullptr) instance_ = new Singleton(); lock_.unlock(); } return *instance_; } }; Problém: Nový data race Zápis do proměnné typu pointer v C++ NENÍ atomický 16 16 16
Třetí pokus (C++ multithreading) Pointer stačí obalit do std::atomic class Singleton { private: Singleton() {} inline static std::atomic<Singleton*> instance_ = nullptr; 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_; } }; 17 17 17
První možnost (C++ dedičnost) class Singleton { protected: Singleton() { }; 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 { private: MagicSingleton() : Singleton() { } friend class Singleton; virtual void doSomething() { /* ... */ } Subclassy potřebují base konstruktor Rodič musí vědet o všech potomcích! Společné rozhraní Bázová třída musí být friend pro přístup k private konstruktoru 18 18 18
Druhá možnost (C++ dedičnost) class Singleton { inline static Singleton* instance_ = nullptr; static std::map<std::string, Singleton*> registry_; protected: Singleton(); static Singleton* lookUp (const std::string & name); void register (const std::string & name, Singleton* s); public: static Singleton* instance() { if (instance_ == nullptr) instance_ = lookUp(Config::SingletonName); return instance_; } }; class MagicSingleton : public Singleton { inline static MagicSingleton instance_; MagicSingleton() { register("MagicSingleton", &instance_); }; Není typesafe Potomek se zaregistruje Potomci se instanciují vždy 19 19 19
Třetí možnost (C++ dedičnost) class Singleton { inline static Singleton* instance_ = nullptr; protected: Singleton(); public: template <typename T> static Singleton& instance() { if (instance_ == nullptr) instance_ = new T(); return *instance_; } }; class MagicSingleton: public Singleton { friend class Singleton; MagicSingleton() {}; void doSomething() { … } int main() { Singleton::instance<MagicSingleton>(). …; } Bázová třída musí být friend pro přístup k private konstruktoru Múžeme zatypedefovat 20 20 20
Destrukce Zodpovědnost za zrušení Kdy je objekt zrušen? mechanizmy jazyka C++ anebo uživatel 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ů 21 21 21
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) class Singleton { private: Singleton() {}; inline static Singleton *instance_ = nullptr; public: static Singleton& getInstance(); void doSomething(); }; Foto: Altrendo Travel, Getty Images 22 22 22
Klíčové slovo static v jazyce C++ Kde žijí statické data? namespace class function int i1 = 0; // constant, implicit static, // load-time class A {} A a1; // implicit static, load-time class C { static A a2; // load-time void f() { static int i2 = 0; // constant, load-time static C instance; // local static variable // initialized first time // control flow passes // through } }; Kdy jsou tato data inicializována? statické konstanty globální statické proměnné lokální statické proměnné Kdo se postará o následnou destrukci? funkce atexit() jakou mají přesně životnost 23 23 23
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 lokální statické proměnné jsou zničeny stejným způsobem, zachovávajícím pořadí 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 24 24 24
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 od C++11 thread-safe t. j. inicializace proběhne jenom jednou 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 25 25 25
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 lazy singletony 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 26 26 26
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 (instance_ == nullptr) { if (destroyed_) onDeadRef(); else create(); } return *instance_; Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; private: static void create() { static Singleton inst; instance_ = &inst; void onDeadRef() { throw /*...*/ } Singleton() { /*...*/ } ~Singleton() { destroyed_ = true; instance_ = nullptr; static bool destroyed_; static Singleton* instance_; }; Detekce problému Inicializace přesunuta do privátní metody Nastavení zrušení Příznak zrušení 27 27 27
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(instance_) Singleton; atexit(killPhoenix); destroyed_ = false; } void Singleton::killPhoenix() { 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! 28 28 28
Prioritní fronta na zabití Dlouhověkost Problémy Fénixe ztráta stavu, uložení, uzavření, ... Dependency Manager nutnost konstrukce všech závislostí 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 class CoolClass { /* ... */ }; class Keyboard { /* ... */ }; class Log { /* ... */ }; CoolClass* global_object(new CoolClass); template <typename T> void setLongevity(T* object, int longevity); int main(int argc, char** argv) { setLongevity(&Keyboard::instance(), 5); setLongevity(&Keyboard::instance(), 6); setLongevity(global_object, 6); /* ... */ } Mechanismus by měl fungovat na jakékoliv (dynamické) objekty Šablona pro nastavování dlouhověkosti Prioritní fronta na zabití Po ukončení programu se objekty zabíjejí v tomto pořadí 29 29 29
Implementace dlouhověkosti Prioritní fronta při stejných prioritách se chová jako zásobník t. j. zachová pořadí volání 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; 30 30 30
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, uživatel si jej může dodefinovat využitím parciální specializace Co bylo dříve - vejce nebo slepice? TrackerArray by se měl chovat jako Singleton kdo vyřeší problému singletonu u TrackerArray? 31 31
Policy Based Design Andrei Alexandrescu “Modern C++ Design” Policy funkční celok nezávislí na ostatných funkčných celkoch (tzv. navzájem orotogonální) Policy class konkrétní implementace policy Konkrétní objekt je pak sestaven z různých policy class template metaprogramming + vícenásobná dědičnost jako Builder pattern s fixním počtem atributů Výhody compile-time lehce rozšířitelné Nevýhody syntaktická správnost != sémantická správnost
Policy Based Design pro Singleton Creation policy new malloc dědičnost Lifetime policy podle pravidel jazyka => LIFO fénix dlouhověkost pštros … Threading model policy jedno vláknové více vláknové neportabilní, systémově specifické řešení // Policies have only static methods T* pObj = Creator<T>::Create(); Creator<T>::Destroy(pObj); void (*pDestructionFunction)(); ... Lifetime<T>::ScheduleDestruction(pDestructionFunction); Lifetime<T>::OnDeadReference();
Policy Based Design pro Singleton template < class T, template <class> class CreationPolicy = CreateUsingNew, template <class> class LifetimePolicy = DefaultLifetime, template <class> class ThreadingModel = SingleThreaded > class SingletonHolder { public: static T& Instance(); private: // Helpers static void DestroySingleton(); // Protection SingletonHolder(); ... // Data typedef ThreadingModel<T>::VolatileType InstanceType; // For threading model purposes static InstanceType* pInstance_; static bool destroyed_; };
Policy Based Design pro Singleton template <...> T& SingletonHolder<...>::Instance() { if (!pInstance_) { typename ThreadingModel<T>::Lock guard; if (destroyed_) { LifetimePolicy<T>::OnDeadReference(); destroyed_ = false; } pInstance_ = CreationPolicy<T>::Create(); LifetimePolicy<T>::ScheduleCall(&DestroySingleton); return *pInstance_;
Policy Based Design pro Singleton class A { /* ... */ }; typedef SingletonHolder<A, CreateUsingMalloc, Phoenix, MultiThreaded> SingleA; // from here on you use SingleA::Instance() class A { /* ... */ }; class Derived : public A { /* ... */ }; template <class T> struct MyCreator : public CreateUsingNew<T> { static T* Create() { return new Derived; } }; typedef SingletonHolder<A, StaticAllocator, MyCreator> SingleA;
Policy Based Design pro Singleton class KeyboardImpl { ... }; class DisplayImpl { ... }; class LogImpl { ... }; inline unsigned int GetLongevity(KeyboardImpl*) { return 1; } inline unsigned int GetLongevity(DisplayImpl*) { return 1; } inline unsigned int GetLongevity(LogImpl*) { return 2; } // The log has greater longevity typedef SingletonHolder<KeyboardImpl, SingletonWithLongevity> Keyboard; typedef SingletonHolder<DisplayImpl, SingletonWithLongevity> Display; typedef SingletonHolder<LogImpl, SingletonWithLongevity> Log;
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) 38 38 38
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 39 39 39
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 40 40 40