Lekce 3 - Object pool (fond objektů)
V minulé lekci, Servant (Služebník), jsme si ukázali návrhový vzor Služebník. Už víme, že ho použijeme v situaci, kdy chceme skupině tříd přidat určitou funkčnost, aniž bychom ji do nich přímo zabudovali.
Návrhový vzor Object pool nebo Object fond (v anglické literatuře se objevuje pojem „pool“, v článku budu používat pojem „fond“) nám uchovává objekty na pozdější použití. To se nám hodí v situacích, kde je vytváření instance příliš složité nebo trvá příliš dlouho.
Motivace
Pracujeme-li na mnoha místech se třídou, která je složitá na vytváření, její opakovaná konstrukce nám zpomaluje celý program. Návrhovým vzor object fond se sám stará o vytváření a předávání objektů zbytku programu s tím, že již vytvořené objekty nemaže, ale uchovává na další použití.
Příklad
Přejdu hned k praktické části. Snad každá webová aplikace využívá databázi pro uchovávání dat. S tím souvisí připojení k samotné databázi, kdy si aplikace musí vytvořit spojení. Takové spojení může v extrémních případech trvat i několik sekund. Pokud takových připojení vytváříme v aplikaci více (například kvůli více vláknům, nebo dokonce pro každý dotaz), je odezva aplikace vysoká.
Z této situace nás zachrání fond. V našem případě bude udržovat připojení k databázi. Pokud se bude nějaká část aplikace chtít dotázat na něco databáze, požádá fond o připojení. Jakmile dotaz provede, vrátí připojení zpět fondu, který ho „zrecykluje“ a může poté předat další části programu, která o připojení požádá. Hlavní přínos tedy spočívá v eliminaci opětovného vytváření připojení, které na celé operaci trvá nejdéle.
Implementace
Nejprve se podíváme, jak by takový fond z minulého příkladu vypadal, a poté si vysvětlíme jeho funkčnost:
class FondPripojeni { private string JmenoServeru = "MujServer"; private string Databaze = "Northwind"; private List<SqlConnection> Connections = new List<SqlConnection>(); public SqlConnection ZiskejPripojeni() { if (Connections.Count == 0) return new SqlConnection($"Server={$JmenoServer}; Database={$Database}; Trusted_Connection = true"); else { SqlConnection Vratit = Connections[0]; Connections.RemoveAt(0); return Vratit; } } public void VratitPripojeni(SqlConnection Pripojeni) { Connections.Add(Pripojeni); } }
Fond má v základní implementaci pouze dvě metody. Jedna vrací připojení, pomocí druhé metody připojení vracíme zpět fondu. Teprve když vrátíme připojení zpět fondu, může jej fond předat jiné části programu. Abychom si situaci demonstrovali, použiji následující program. Vytvoří se 20 vláken, které se uspí na náhodnou dobu. Po probuzení požádají fond o připojení. Aby bylo lépe vidět, že se vytváří méně připojení, než je vláken, budu u každého připojení vypisovat informaci o tom, že bylo právě vytvořeno:
class Program { static Random Ran = new Random(); static FondPripojeni Fond = new FondPripojeni(); static void Funkce() { int DelkaUspani = Ran.Next(0,3000); Thread.Sleep(DelkaUspani); Console.WriteLine("Žádost o objekt"); SqlConnection Pripojeni=Fond.ZiskejPripojeni(); // SIMULACE PRÁCE S OBJEKTEM Thread.Sleep(Ran.Next(0,200)); Fond.VratitPripojeni(Pripojeni); } static void Main(string[] args) { for (int a = 0; a < 20; a++) { Thread vlakno = new Thread(Funkce); vlakno.Start(); } Console.ReadKey(); } }
Výstup programu:
Žádost o objekt Připojení bylo vytvořeno Žádost o objekt Žádost o objekt Žádost o objekt Žádost o objekt Žádost o objekt Připojení bylo vytvořeno Žádost o objekt Žádost o objekt Žádost o objekt Žádost o objekt Žádost o objekt Žádost o objekt Žádost o objekt Žádost o objekt Žádost o objekt Žádost o objekt Žádost o objekt Žádost o objekt Připojení bylo vytvořeno Žádost o objekt Žádost o objekt
Jak je vidět, ačkoliv chtělo 20 vláken připojení, ve skutečnosti se vytvořily připojení pouze 3. Kdyby si každé vlákno vytvořilo vlastní připojení, zvýší se potřebný čas, který bude potřeba pro výpočet, dále bude více zahlcena linka i samotná databáze, protože bude muset udržovat několik spojení současně.
Možná konfigurace
Některé objekty nám mohou zabírat mnoho místa. Použijeme-li je ve
fondu, může nám rychle dojít paměť. V takových situacích
zpravidla nechceme mít neomezený počet existujících instancí. Můžeme
dát fondu parametr, který nám řekne maximální počet instancí,
které má uchovávat. Pokud by chtělo vlákno další instanci, může metoda
ZiskejPripojeni()
například vyhodit výjimku, nebo vrátit
null. Druhý přístup spočívá v tom, že si nadefinujeme dvě
metody: ZiskejPripojeniIhned()
a
ZiskejPripojeniCekam()
. V prvním případě by se (nejsou-li
žádné volné objekty) vytvořila nová instance. Při navrácení objektů
zpět by se objekt, který by byl již nad hranici, neuložil zpět, ale rovnou
smazal. Při volání funkce ZiskejPripojeniCekam()
by se vlákno
zablokovalo do doby, než bude volná instance ve fondu. Který způsob
použít závisí na konkrétní situaci. V našem případě optimalizujeme
především mezi časem a pamětí.
Hlavní problém, který s Fondem souvisí, je, že musíme vracet objekty zpět. Na první pohled se jedná o relativně jednoduchý princip, ale ne všichni programátoři ho dodržují. Prvním problém, který může nastat, je, že programátor bude objekt z fondu používat i po jeho navrácení zpět. Tato situace lze vyřešit obalením návratového objektu – viz. článek o proxy. Proxy zajistí, aby objekt nešel použít po tom, co se navrátí zpět do fondu. Druhým případem je situace, kdy programátor objekt vůbec vracet nebude. S takovou situací se vypořádáme jen velmi těžko a zde záleží na konkrétním jazyce. Opět by se uplatnil návrhový vzor proxy, ale například v C++ by se vracela hodnota přímo (ne ukazatelem). Tím by bylo zajištěno, že se objekt vrátí zpět do Fondu po opuštění kontextu (například v destruktoru proxy). V C# bychom mohli například definovat proxy odvozené od IDisposable, který sice pořád vyžaduje nějakou akci od programátora, ale stále je to více než nic.
Vícevláknové aplikace
Ačkoliv byla první ukázka demonstrována pomocí více vláken, v reálné aplikaci se o vhodnou implementaci nejedná. Fond se vůbec nestará o synchronizaci, jak by tomu mělo být. Níže je ukázka fondu tak, jak by měl být implementován. Vícevláknové mechanismy jsou mimo obsah tohoto článku, proto pro plné pochopení doporučuji přečíst seriál Paralelní programování a vícevláknové aplikace v C#.NET. Protože budu používat více tříd, a nechci zahltit celý článek kódem, kompletní řešení bude v přiloženém archivu pod článkem. Spojíme zde synchronizaci, maximální počet instancí i navrácení zpět do Fondu bez přímého volání metody. Zde uvedu jen UML diagram, jaký je výsledek:
Všimněme si, jak se nám kód rozrostl. To je daň za to, kdy chceme
pracovat s více vlákny současně. Také bych chtěl upozornit na metodu
ZiskejPripojeniPockam()
. Vlákno pravidelně uspáváme do doby,
než se uvolní objekt, který potřebujeme. V rámci jednoduchosti je metoda
definována tímto způsobem, ale lze dále optimalizovat (třeba pomocí
semaforů), to je ovšem mimo zaměření tohoto článku.
Závěr
Fond použijeme v programech, kde chceme zamezit opětovnému vytváření instance, nebo chceme mít kontrolu nad tím, kolik objektů existuje. Můžeme počet existujících objektů kontrolovat, a to i když při psaní kódu nevíme, kolik objektů bude aplikace reálně potřebovat.
V další lekci, Immutable objects (neměnné objekty), si probereme neměnné objekty, tzv. immutable objects. Výhody a využití tohoto návrhového vzoru si představíme.
Stáhnout
Stažením následujícího souboru souhlasíš s licenčními podmínkamiStaženo 762x (4.54 kB)