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.
Download
Report
Transcript 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.
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….
....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 – 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
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ěď:
Program používá nejvýše jednu instanci třídy
Mít více instancí může být zbytečné, nežádoucí, nebezpečné nebo nemožné
Často se používají, proto vyžadují snadný přístup z libovolného místa v programu
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
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
Globální přístupový bod
použití na libovolném místě kódu
přístup poskytován samotnou třídou
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
Globální přístupový bod
použití na libovolném místě kódu
přístup poskytován samotnou třídou
Rádi bychom
měli možnost rozšiřovat třídu bez nutnosti zásahu do kódu předka
používali všechny nástroje objektového programování
virtuální metody, abstraktní metody, přetěžování metod
měli nějakou rozšiřitelnou šablonu singletonu
Singleton – naivní implementace → základní myšlenka
Globální proměnná
+ zaručuje globální přístupový bod
- nedokáže zaručit jedinou instanci objektu
- menší přehlednost kódu
Singleton – naivní implementace → základní myšlenka
Globální proměnná
+ zaručuje globální přístupový bod
- nedokáže zaručit jedinou instanci objektu
- menší přehlednost kódu
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 – nemůžeme ovlivnit pořadí
- problém úklidu – opět pořadí
Singleton – naivní implementace → základní myšlenka
Globální proměnná
+ zaručuje globální přístupový bod
- nedokáže zaručit jedinou instanci objektu
- menší přehlednost kódu
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 – nemůžeme ovlivnit pořadí
- problém úklidu – opět pořadí
Lepší řešení
třída se o jedinečnost své instance stará sama
inicializace: konstruktor
úklid: destruktor
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
class Singleton {
navíc objekt může být vytvořen zbytečně
private:
static Singleton
pozor, C# má pravidla pro inicializaci statických
public:
položek
static Singleton
instance;
* instance() {
return &instance;
Na první pohled správné
Ale: nedefinované pořadí inicializace
}
};
Singleton – Lazy instantiation
V C++ nelze použít statickou inicializaci
nelze definovat pořadí konstruktorů statických tříd
class Singleton {
navíc objekt může být vytvořen zbytečně
private:
static Singleton
pozor, C# má pravidla pro inicializaci statických
public:
položek
static Singleton
instance;
* instance() {
return &instance;
Na první pohled správné
Ale: nedefinované pořadí inicializace
}
};
Ř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 == nullptr) {
instance = new Singleton;
}
return instance;
}
Instance se vytvoří při prvním volání
instance() – až když je to potřeba
Singleton – implementace C++
class Singleton {
public:
static Singleton &instance() {
if (pInstance == nullptr) {
pInstance = new Singleton;
}
return *pInstance;
}
Statická metoda pro přístup k jediné instanci
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
void doSomethingUseful() {...}
private:
Singleton() {...}
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
~Singleton() {...}
static Singleton* pInstance;
};
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
Singleton* Singleton::pInstance = nullptr;
Statická inicializace
int main(int argc, char** argv) {
Singleton::instance().doSomethingUseful();
...
}
Jednoduchý příklad použití
Singleton – implementace C#
class Singleton {
public static Singleton Instance() {
return instance;
}
void doSomethingUseful() {...}
Statická metoda pro globální
přístup k jediné instanci
Konstruktor nesmí být public
Inicializuje třídu, volán jen zevnitř třídy
private Singleton() {...}
private static Singleton instance
= new Singleton();
}
Referenci na instanci si spravuje třída sama
Statická inicializace
C# v tomto případě zajistí vždy korektní konstrukci a téměř nikdy
nevytvoří Singleton zbytečně
Singleton – implementace C#
Detailnější pohled – ještě línější implementace
C# inicializuje statické položky třídy před prvním přístupem
public class Singleton {
Vnitřní třída pro línou inicializaci
private Singleton () {}
class SingletonCreator {
private static SingletonCreator () {}
internal static readonly Singleton instance =
new Singleton();
}
public static Singleton Instance() {
return SingletonCreator.instance;
}
}
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 – 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!”
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ů (C#)
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;
}
Řešení - zámky
Singleton – více vláken a procesorů (C#)
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;
}
Řešení - zámky
sem se může dostat
více vláken
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();
public static Singleton Instance() {
lock (lockObj) {
if (instance == null) {
instance = new Singleton();
}
}
uzamknutí zajistí přístup jen
jednomu vláknu
return instance;
}
Jde to lépe?
referenci (či pointer) si můžeme ukládat – nevolat opakovaně Instance()
double-checked locking pattern
Singleton – double-checked locking (C#)
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();
public static Singleton Instance() {
if (instance == null) {
lock (lockObj) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
Singleton – double-checked locking (C#)
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();
public static 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
původní norma (před C++11) pro vícevláknové aplikace mnoho nezaručuje
dovoluje prohazovat instrukce. Instance nemusí být null, ale přesto nemusí být
inicializována
C++11 zavádí memory model pro C++
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, ktero vedlo k nové definici
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 – double-checked locking
Je toto správné řešení pro C++11 ?
Singleton* Singleton::getInstance() {
Singleton* tmp = m_instance;
if (tmp == NULL) {
Lock lock;
tmp = m_instance;
if (tmp == NULL) {
tmp = new Singleton;
m_instance = tmp;
}
}
return tmp;
}
Singleton – double-checked locking
Je toto správné řešení pro C++11 ?
Singleton* Singleton::getInstance() {
Singleton* tmp = m_instance;
}
NE
if (tmp == NULL) {
překladač má dovoleno optimalizovat
Lock lock;
optimalizace se dají ovlivnit
tmp = m_instance;
if (tmp == NULL) {
tmp = new Singleton;
m_instance = tmp;
}
std::atomic<Singleton*> Singleton::m_instance;
}
std::mutex Singleton::m_mutex;
return tmp;
Singleton* Singleton::getInstance() {
Singleton* tmp = m_instance.load(std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_acquire);
if (tmp == nullptr) {
std::lock_guard<std::mutex> lock(m_mutex);
tmp = m_instance.load(std::memory_order_relaxed);
if (tmp == nullptr) {
tmp = new Singleton;
std::atomic_thread_fence(std::memory_order_release);
m_instance.store(tmp, std::memory_order_relaxed);
}
}
return tmp;
}
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
C++ : potomek musí být spřátelený s bázovou třídou nebo být vnořenou třídou
C# : potomek musí být vnořenou třídou
Několik singletonů implementuje společné rozhraní
existuje maximálně jedna instance pro každého z potomků
Singleton – exkluzivní dědičnost potomka
class Singleton {
public:
static Singleton &instance() {
if (pInstance == nullptr) {
if (getEnv(“NAZEV”) == “PRVNI”)
pInstance = new Prvni;
else
pInstance = new Druhy;
}
return pInstance;
}
virtual void doSomethingUseful() = 0;
Vytvoří se požadovaná instance
Společné rozhraní, které musí všechny
odděděné třídy implementovat
private:
// konstruktory, destruktory, atd.
};
class Prvni : public Singleton {
public:
virtual void doSomethingUseful() {...}
private:
// konstruktory, destruktory, atd.
friend class Singleton;
};
class Druhy : public Singleton {
// podobně jako třída Prvni
};
Implementace rozhraní
Aby mohla bázová třída vytvářet instance
Singleton – exkluzivní dědičnost potomka
class Singleton {
public:
static void Register(const char * name, Singleton *);
static Singleton * Instance() {
if (_instance == nullptr) {
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);
}
};
Registruje instanci singletonu
pod zadaným jménem
Nalezne a vrátí požadovanou instanci
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: Jedna „pravá“ instance
přes všechny potomky, ale ostatní
stejně existují
Singleton – instance každého potomka
#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í
CRTP - Curiously Recurring
Template Pattern
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
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 nezavolá
ukládáme statický ukazatel, ne statický objekt
Singleton – destrukce killer
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 = nullptr;
SingletonKiller Singleton::singletonKiller;
Idea
ukazatel zabalit do třídy
starajíci se o destrukci
kompilátor se stará o zrušení
statického objektu singletonKiller,
který ve svém destruktoru instanci
Singleton zabíjí
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
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();
...
}
2. inicializace statického objektu
pouze při prvním průchodu
registrace atexit
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*
extern void __destroySingleton();
memory);
static bool __initialized = false;
static char __buffer[sizeof(Singleton)];
funkce generované kompilátorem
proměnné generované kompilátorem
__buffer obsahuje Singleton
if (!__initialized) {
__constructSingleton(__buffer);
atexit(__DestroySingleton);
__initialized = true;
}
volání funkce
__constructSingleton() zavolá
konstruktor na paměti __buffer
return *reinterpret_cast<Singleton*>(__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ě
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
destrukce Logu by měla následovat až po destrukcích ostatních singletonů
nebo aspoň poznat problém a slušně umřít
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;
}
static bool destroyed;
static Singleton* pInstance;
};
Singleton* Singleton::pInstance = 0;
bool Singleton::destroyed = false;
detekce problému
inicializace přesunuta
do 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
class SomeSingleton { ... };
class SomeClass { ... };
SomeClass* pGlobalObject(new SomeClass);
template <typename T>
void SetLongevity(T* pDynObj, int longevity);
mechanismus by měl fungovat
na jakékoliv (dynamické) objekty
šablona pro
nastavování dlouhověkosti
prioritní fronta na zabití
int main()
{
SetLongevity(&SomeSingle().Inst(), 5);
SetLongevity(pGlobalObject, 6);
...
}
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žák
virtuální destruktor
LifeTimeTracker
virtual ~LifeTimeTracker()
longevity
ConcreteLTT<T>
~ConcreteLTT()
delete obj;
šablona instanciována
a objekt vytvořen
šablonou SetLongevity
T
~T()
Singleton – 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;
};
virtuální držák
umí zabíjet...
... a porovnávat stáří
inline LifetimeTracker::~LifetimeTracker() {}
vlastní fronta na zabití
typedef LifetimeTracker** TrackerArray;
extern TrackerArray pTA;
extern unsigned int elements;
template <typename T> struct Deleter {
static void DeleteIt(T* pObj) {
delete pObj;
}
};
způsob zabití
defaultně delete
lze i free, ...
Co bylo dříve - vejce nebo slepice?
TrackerArray se musí chovat jako Singleton
a kdo tedy vyřeší problému singletonu u TrackerArray ?
Singleton – přehled
Základní implementace
Dědičnost
Threading model
jedna instance všech potomků
nejvýše jedna instance každého předka
jednovláknové
bez synchronizace
double-checked locking
problémy se synchronizací – platformově závislé
Životnost (lifetime)
pštrosí ř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 – přehled, související vzory
Možné použití
zjednodušení stavu aplikace - zajištění pouze jedné instance nějakého objektu
řízení přístupu k externímu prostředí, tiskovým zdrojům, …
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
Facade
častá implementace pomocí singletonu
v případě potřeby pouze jednoho vstup do systému
State
stavové objekty jsou často Singletony