340 likes | 494 Views
Singleton. Jasná i temnější zákoutí jazyka C++. Všichni patrně znají…. ….klíčové slovo static a jeho význam pro členy třídy. ….rozdíl mezi statickou a dynamickou alokací (operátor new ). Někdo už možná pozapomněl, ale každý by asi domyslel, že….
E N D
Jasná i temnější zákoutí jazyka C++ • Všichni patrně znají…. • ….klíčové slovo static a jeho význam pro členy třídy. • ….rozdíl mezi statickou a dynamickou alokací (operátor new). • Někdo už možná pozapomněl, ale každý by asi domyslel, že…. • ....není definováno pořadí inicializace statických objektů v různých .cpp souborech. • ….pořadí odstraňování těchto objektů je obrácené než pořadí inicializace. • Avšak toto pořadí není možné za běhu programu nijak rozumně a hezky zjistit. • Málokdo ví…. • ….jak přesně funguje funkce atexit().
Singleton – obsah • Motivace • Základní myšlenka • Implementace • Problémy • Vícevláknovost • dědičnost • destrukce • Shrnutí
Singleton – motivace • Co mají následující příklady společného? • DatabaseConnectionPool – přiděluje spojení do DB jednotlivým částem aplikace • WindowManager – v okenním systému existuje jeden objekt spravující okna • Keyboard, display, log – (KDL) • PrinterSpooler – více tiskáren, jeden manažer • FileSystem • System clock • Herní prostředí – např. mapa ve hře • Odpověď: • Třídy řešící tyto problémy vyžadují jedinou instanci. Mít více instancí může být zbytečné, nežádoucí, nebezpečné nebo nemožné. • Měly by být lehce přístupné z libovolného místa.
Singleton – naivní implementace → základní myšlenka • Globální proměnná • + zaručuje globální přístupový bod • - znepřehledňuje namespace a kód • - nedokáže zaručit jedinou instanci objektu Statická data + statické metody • + zaručuje globální přístupový bod • + zaručuje jedinou instanci • - problém rozšiřitelnosti – statické metody nemohou být virtuální • - problém inicializace (závislosti) • - problém úklidu (kdo to má dělat) • Lepší řešení • třída se o jedinečnost své instance stará sama • inicializace: konstruktor • úklid: destruktor
Singleton – požadavky • Zaručení jediné instance • zakázat vytvoření více instancí • zakázat kopírování existující instance • zakázat zrušení instance • zakázat deserializaci • Globální přístupový bod • použití na libovolném místě kódu • přístup poskytován samotnou třídou • Navíc bychom rádi zachovali • možnost rozšiřitelnosti bez nutnosti zásahu do kódu předka • všechny nástroje objektového programování • virtuální metody, abstraktní metody, přetěžování metod • možnost rozšiřitelnosti pomocí šablon
Singleton – návrh řešení • třída si řídí vytváření instance a přístup k ní sama • je schopná zajistit jedinečnost instance Veřejná statická přístupová metoda. Statická reference na jedinou instanci. Metody nad instancí singletonu.
Singleton – Lazy instantiation • V C++ nelze použít statickou inicializaci • nelze definovat pořadí konstruktorů statických tříd • navíc objekt může být vytvořen zbytečně class Singleton { private: static Singleton instance; public: static Singleton * instance() { return &instance; } } Na první pohled správné, ale kvůli nedefinovanému pořadí inicializace špatné! • Řešení - objekt bude vytvořen, až když o něj bude skutečně požádáno • lze snadno ovlivnit pořadí konstrukce • nikdy nebude vytvořen zbytečně • lze dosáhnout malou úpravou statické funkce vracející instanci objektu • analogická implementace funkční ve všech rozumných jazycích static Singleton instance() { if (instance == 0){ instance = new Singleton; } return instance; } Instance se vytvoří při prvním volání instance() – až když je to potřeba.
Singleton – implementace C++ Statická metoda pro přístup k jediné instanci. class Singleton { public: static Singleton &instance() { if (pInstance == 0) { pInstance = new Singleton; } return pInstance; } void doSomethingUseful() {...} private: Singleton() {...} Singleton(const Singleton&); Singleton& operator=(const Singleton&); ~Singleton() {...} static Singleton* pInstance; }; Singleton* Singleton::pInstance = 0; int main(int argc, char** argv) { Singleton::instance().doSomethingUseful(); ... } Instance se vytvoří při prvním volání instance() – až když je to potřeba. Konstruktor nesmí být public. Inicializuje třídu. Volán jenom zevnitř třídy. Kopírovací konstruktor a operátor přiřazení se snaží kompilátor generovat sám. Je nutné mu v tom zabránit. Destruktor nesmí být public, někdo by mohl smazat instanci. Ukazatel na instanci si spravuje třída sama. Statická inicializace. Jednoduchý příklad použití.
Singleton – implementace C# (a Java) Statická metoda pro globální přístup k jediné instanci. class Singleton { public static Singleton Instance() { return instance; } void doSomethingUseful() {...} private Singleton() {...} privatestatic Singleton instance = new Singleton(); } Konstruktor nesmí být public. Inicializuje třídu. Volán jenom zevnitř třídy. Referenci na instanci si spravuje třída sama. Statická inicializace. • C# a Java v tomto případě zajistí vždy korektní konstrukci a téměř nikdy nevytvoří Singleton zbytečně
Singleton – implementace C# (a obdobně Java) Detailnější pohled – ještě línější implementace • C# a Java inicializují statické položky třídy před prvním přístupem • Navíc tím získáme thread-safe řešení publicclass Singleton { private Singleton () {} class SingletonCreator { private static SingletonCreator () {} internalstaticreadonly Singleton instance = new Singleton(); } publicstatic Singleton Instance() { return SingletonCreator.instancee; } } Vnitřní třída pro línou inicializaci. Viditelnost internal, aby vnější třída mohla k této položce přistoupit. Pro vnější svět je stále privátní. Privátní objekt inicializovaný privátním konstruktorem.
Singleton – Java problémy a enum implementace • Problém serializace, pokud ji požadujeme – vrací novou instanci => je nutné mít všechny položky transient a přetížit readResolve • Problém s reflection – lze přistoupit k privátnímu konstruktoru => měl by házet výjimku, pokud už instance existuje • Jiné řešení – enum Singleton publicenum Singleton { INSTANCE; public void doSomethingUseful() { ... } } ... Singleton.INSTANCE.doSomethingUseful() • Řeší problém s reflection, serializací
Singleton – problémy • GoF '95 • relativně jednoduchý vzor • od té doby rozsáhlé diskuse • Vlissides '98: • “I used to think it was one of the more trivial of our patterns ... Boy, I was wrong!” • Problém vícevláknových aplikací • Dědičnost • Problém destrukce (lifetime)
Singleton – více vláken a procesorů • V prostředí více vláken nesplňuje definici • může vytvořit více instancí public static Singleton Instance() { if (instance == null){ instance = new Singleton(); } return instance; } sem se může dostat více vláken • Řešení - zámky
Singleton – více vláken a procesorů, zamykání (C#) • Řešení - zámky • zaručují jednu instanci • na multiprocesorech nezbytná kombinace s volatile – cache coherency problem • funguje ve všech jazycích • režie může být nepřijatelně vysoká private static volatile Singleton instance; private static object lockObj = new Object(); publicstatic Singleton Instance() { lock (lockObj) { if (instance == null) { instance = new Singleton(); } } return instance; } uzamknutí zajistí přístup jen jednomu vláknu • Jde to lépe? • referenci (či pointer) si můžeme ukládat – nevolat opakovaně Instance() • double-checked locking pattern
Singleton – double-checked locking • Pozorování • referenci získáváme často • konstrukci provádíme (doufejme) jen jednou • stačí zamykat jen při konstrukci • Double-Checked Locking Pattern private static volatile Singleton instance; private static object lockObj = new Object(); publicstatic Singleton Instance() { if (instance == null) { lock (lockObj) { if (instance == null) { instance = new Singleton(); } } } return instance; } Za první podmínku se může dostat více vláken. Za zámek už jen jedno vlákno, znovu ověření podmínky, jinak by později prolezlo případné další čekající vlákno. Zde mi to už nikdo nezmění. • Není to příliš jednoduché řešení pro tak složitý vzor?
Singleton – double-checked locking • C++ problémy • norma pro vícevláknové aplikace mnoho nezaručuje • dovoluje prohazovat instrukce. Instance nemusí být null, ale přesto nemusí být inicializována • Java, C# problémy • dovolují při optimalizaci prohazovat instrukce. Instance nemusí být null, ale přesto nemusí být inicializována • až do Javy 1.2 chyba v garbage collectoru • až do Javy 1.4 nepříliš jasné a správné chování volatile • nyní už double-checked locking s volatile funguje správně • Java, C# – volatile • vlákna přistupují k proměnné „jako by byla celá v zamčené sekci“ • vlákno čte vždy aktuální necachovanou hodnotu
Singleton – dědičnost • Singleton vydává za svou instanci instanci některého z potomků • existuje maximálně jedna instance přes všechny potomky • v C++ musí potomek být spřátelený s bázovou třídou nebo být vnořenou třídou • v Javě nebo C# musí být potomek vnořenou třídou • Několik singletonů implementuje společné rozhraní • existuje maximálně jedna instance pro každého z potomků
Singleton – dědičnost (1. případ) class Singleton { public: static Singleton &instance() { if (pInstance == 0) { if (getEnv(“NAZEV”) == “PRVNI”) pInstance = new Prvni; else pInstance = new Druhy; } return pInstance; } virtualvoid doSomethingUseful() = 0; private: /* konstruktory, destruktory, atd. */ }; class Prvni : public Singleton { public: virtualvoid doSomethingUseful() {...} private: /* konstruktory, destruktory, atd. */ friend class Singleton; }; class Druhy : public Singleton { /* podobně jako třída Prvni */ }; Vytvoří se taková instance, která je požadována. Společné rozhraní, které musí všechny odděděné třídy implementovat. Implementace rozhraní. Aby mohla bázová třída vytvářet instance.
Singleton – dědičnost (1. případ) Registruje instanci singletonu pod zadaným jménem. class Singleton { public: static void Register(const char * name, Singleton *); static Singleton * Instance() { if (_instance == 0) { const char * singletonName = getenv("SINGLETON"); _instance = Lookup(singletonName); } return _instance; } protected: static Singleton * Lookup(const char * name); private: static Singleton * _instance; static list<NameSingletonPair> * registry; }; class MySingleton : public Singleton { private: static MySingleton theSingleton; MySingleton() { ... Singleton::Register("MySingleton", this); } }; Nalezne se a vrátí taková instance, která je požadovaná. Najde instanci podle jména. Udržuje dvojici název odvozeného singletonu a jeho instanci. Singleton se při vytvoření registruje rodiči. Instance musí před jejím hledáním existovat! Problém: Je jedna „pravá“ instance přes všechny potomky, ale ostatní stejně existují
Singleton – dědičnost (2. případ) #include <iostream> template<typename T> class Base { public: static T &instance() { static T inst; return inst; } virtual void echo () = 0; protected: Base() {} virtual ~Base() {} private: Base(const Base &); Base& operator=(const Base &); }; class Derived: public Base<Derived> { friend class Base<Derived>; public: void echo () { std::cout << “I’m Derived" << std::endl; } private: Derived() {} virtual ~Derived() {} }; int main(int argc, char **argv) { Derived &derived = Derived::instance(); derived.echo(); } Dědičnost implementována pomocí šablon. Možnost šablonování metody instance(), místo třídy. Možnost vytváření abstrakních metod. Třída Base<Derived> musí být náš friend (volání konstruktoru ze statické metody instance()). Jednoduché použití.
Singleton – destrukce • Zodpovědnost za zrušení • je nutné uvolnit paměť? • je nutné uvolnit používané prostředky • Kdy je objekt zrušen • nutné dodržet určité pořadí rušení • Některé objekty je potřeba mít přístupné vždy • log
Singleton – destrukce Ostrich • 'Ostrichovo' řešení (leaking 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 nezavolá • ukládáme statický ukazatel, ne statický objekt
Singleton – destrukce killer • Idea • ukazatel zabalit do třídystarajíci se o destrukci • kompilátor se stará o zrušení statického objektu singletonKiller, který ve svém destruktoru instanci Singleton zabíjí class SingletonKiller { public: void setSingleton(Singleton* _instance) { instance = _instance; } Singleton* getSingleton() { return instance; } ~SingletonKiller() { delete instance; } private: static Singleton* instance; }; class Singleton { public: static Singleton* instance(); private: Singleton(); ~Singleton(); Singleton(const Singleton&); Singleton& operator=(const Singleton&); static SingletonKiller singletonKiller; }; Singleton* SingletonKiller::instance = 0; SingletonKiller Singleton::singletonKiller; 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(); }
Singleton – Meyers • Scott Meyers • místo operátoru new, statická lokální proměnná • instanci nedržíme ve statickém ukazateli • funkce vracející referenci na statický objekt ve funkci 2. inicializace statického objektu pouze při prvním průchodu registrace atexit class Singleton { public: static Singleton& instance() { static Singleton inst; return inst; } private: Singleton() {...} Singleton(const Singleton&); Singleton& operator=(const Singleton&); ~Singleton() {...} }; int main(int argc, char** argv) { Singleton& s = Singleton::instance(); ... } 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
Singleton – funkce atexit • Odstraňování statických proměnných • LIFO – nejdříve se odstraní naposledy inicializované • int atexit(void*(pFunction)()); • 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
Singleton – 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
Singleton – detekce mrtvé reference • Detekce destruovaného objektu • přidání statické proměnné, její nastavení v destruktoru class Singleton { public: static Singleton& instance() { if (!pInstance) { if (destroyed) throw ...; else create(); } return *pInstance; } private: static void create() { static Singleton inst; pInstance = &inst; } Singleton() {...} Singleton(const Singleton&); Singleton& operator=(const Singleton&); ~Singleton() { destroyed = true; pInstance = NULL; } staticbool destroyed; static Singleton* pInstance; }; Singleton* Singleton::pInstance = 0; bool Singleton::destroyed = false; detekce problému inicializace přesunutado privátní metody nastavení zrušení příznak zrušení
Singleton – Fénix • Detekce někdy nestačí - nutnost přístupu k singletonu kdykoliv • idea: bájný pták Fénix vstane ze svého popela • znovuvytvoření při detekci zrušeného objektu • příklad KDL - 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(pInstance) Singleton; atexit(KillPhoenix); destroyed = false; } void Singleton::KillPhoenix() { pInstance->~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ě registrace destruktoru fénixu explicitní zavolání destruktoru nelze delete!
Singleton – 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 • KDL – 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 fungovatna jakékoliv (dynamické) objekty class SomeSingleton { ... }; class SomeClass { ... }; SomeClass* pGlobalObject(new SomeClass); template <typename T> void SetLongevity(T* pDynObj, int longevity); int main() { SetLongevity(&SomeSingle().Inst(), 5); SetLongevity(pGlobalObject, 6); ... } šablona pronastavování dlouhověkosti prioritní fronta na zabití po ukončení programu se objekty destruují v tomto pořadí
Singleton – 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 • čeho to bude fronta? • 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žákvirtuální destruktor šablona instanciovánaa objekt vytvořenšablonou SetLongevity delete obj;
Singleton – implementace dlouhověkosti class LifetimeTracker { public: LifetimeTracker(unsignedint x): longevity(x) {} virtual ~LifetimeTracker() = 0; friend inlinebool Compare( unsignedint 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 Delete(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, ... Bylo dříve vejce nebo slepice? • TrackerArray se musí chovat jako Singleton • čímžpádem ale trpí všemi jeho problémy
Singleton – kategorizace • Dělení z hlediska vytvoření • dynamické (operátor new, malloc) • destruktor se nezavolá • statické (Meyers singleton) • funkce vracející referenci na statický objekt • Threading model • jednovláknové • bez synchronizace • double-checked locking • problémy se synchronizací – platformě závislé • Životnost (lifetime) • Ostrichovo řešení (Leaking singleton) • resource, memory leaks • killer • obalení ukazatele třídou starající se o destrukci • životnost určená kompilátorem (Meyers singleton) • destrukce v pořadí podle LIFO • detekce mrtvé reference • při detekci zrušeného objektu výjimka • Fénix singleton • po zrušení objektu jeho znovuvytvoření • dlouhověkost (singleton with longevity) • specifikace pořadí destrukcí
Singleton – shrnutí, související vzory • Možné použití • zjednodušení stavu aplikace - zajištění pouze jedné instance nějakého objektu • řízení přístupu do DB, tiskových zdrojů… • omezení plýtvaní zdrojů (lze přidělit existující spojení) • možnost regulovat počty, frekvence… přístupů • přidělení spojení s vhodnými právy • Související NV • Abstract Factory, Builder, Prototype • častá implementace pomocí singletonu • Facade • v případě potřeby pouze jednoho vstup do systému • State • stavové objekty jsou často Singletony