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