Wzór puli obiektów
Wzorzec puli obiektów jest wzorcem projektowym tworzenia oprogramowania , który wykorzystuje zestaw zainicjowanych obiektów gotowych do użycia – „ pulę ” – zamiast przydzielać je i niszczyć na żądanie. Klient puli zażąda obiektu z puli i wykona operacje na zwróconym obiekcie. Kiedy klient skończy, zwraca obiekt do puli zamiast go niszczyć ; można to zrobić ręcznie lub automatycznie.
Pule obiektów są używane głównie do zwiększania wydajności: w pewnych okolicznościach pule obiektów znacząco poprawiają wydajność. Pule obiektów komplikują czas życia obiektów , ponieważ obiekty uzyskiwane z puli i zwracane do puli nie są faktycznie tworzone ani niszczone w tym czasie, a zatem wymagają starannej implementacji.
Opis
Gdy konieczna jest praca z wieloma obiektami, których utworzenie jest szczególnie kosztowne, a każdy obiekt jest potrzebny tylko przez krótki okres czasu, może to niekorzystnie wpłynąć na wydajność całej aplikacji. Wzorzec projektowy puli obiektów może być uznany za pożądany w takich przypadkach.
Wzorzec projektowy puli obiektów tworzy zestaw obiektów, które mogą być ponownie wykorzystane. Gdy potrzebny jest nowy obiekt, jest on żądany z puli. Jeśli dostępny jest wcześniej przygotowany obiekt, jest on natychmiast zwracany, co pozwala uniknąć kosztów tworzenia instancji. Jeśli w puli nie ma żadnych obiektów, tworzony jest i zwracany nowy element. Kiedy obiekt został użyty i nie jest już potrzebny, jest zwracany do puli, co pozwala na ponowne użycie go w przyszłości bez powtarzania kosztownego obliczeniowo procesu tworzenia instancji. Należy pamiętać, że po użyciu i zwróceniu obiektu istniejące referencje stracą ważność.
W niektórych pulach obiektów zasoby są ograniczone, dlatego określona jest maksymalna liczba obiektów. Jeśli ta liczba zostanie osiągnięta i zażądany zostanie nowy element, może zostać zgłoszony wyjątek lub wątek zostanie zablokowany do czasu zwolnienia obiektu z powrotem do puli.
Wzorzec projektowy puli obiektów jest używany w kilku miejscach w standardowych klasach .NET Framework. Jednym z przykładów jest dostawca danych .NET Framework dla programu SQL Server. Ponieważ tworzenie połączeń z bazą danych programu SQL Server może być powolne, utrzymywana jest pula połączeń. Zamknięcie połączenia nie oznacza w rzeczywistości rezygnacji z łącza do programu SQL Server. Zamiast tego połączenie jest utrzymywane w puli, z której można je odzyskać podczas żądania nowego połączenia. Znacząco zwiększa to szybkość wykonywania połączeń.
Korzyści
Łączenie obiektów może zapewnić znaczny wzrost wydajności w sytuacjach, gdy koszt inicjalizacji instancji klasy jest wysoki, a szybkość tworzenia instancji i niszczenia klasy jest wysoka — w takim przypadku obiekty mogą być często ponownie wykorzystywane, a każde ponowne użycie pozwala zaoszczędzić znaczną ilość czas. Pule obiektów wymagają zasobów – pamięci i ewentualnie innych zasobów, takich jak gniazda sieciowe, dlatego preferowane jest, aby liczba instancji używanych w danym momencie była niska, ale nie jest to wymagane.
Obiekt w puli jest uzyskiwany w przewidywalnym czasie, podczas gdy tworzenie nowych obiektów (zwłaszcza w sieci) może zająć zmienny czas. Korzyści te dotyczą głównie obiektów kosztownych w czasie, takich jak połączenia z bazami danych, połączenia gniazd, wątki i duże obiekty graficzne, takie jak czcionki lub mapy bitowe.
W innych sytuacjach proste łączenie obiektów (które nie przechowuje żadnych zasobów zewnętrznych, ale zajmuje tylko pamięć) może nie być wydajne i może obniżyć wydajność. W przypadku prostego łączenia pamięci, z alokacją płyt jest bardziej odpowiednia, ponieważ jedynym celem jest zminimalizowanie kosztów alokacji i zwalniania pamięci poprzez zmniejszenie fragmentacji.
Realizacja
Pule obiektów można implementować w sposób zautomatyzowany w językach takich jak C++ za pomocą inteligentnych wskaźników . W konstruktorze inteligentnego wskaźnika można zażądać obiektu z puli, aw destruktorze inteligentnego wskaźnika obiekt można zwolnić z powrotem do puli. W językach z wyrzucaniem elementów bezużytecznych, w których nie ma destruktorów (które z pewnością zostaną wywołane jako część rozwijania stosu), pule obiektów muszą być implementowane ręcznie, poprzez jawne żądanie obiektu z fabryki i zwrócenie obiektu przez wywołanie metody usuwania (jak we wzorcu usuwania ). Używać finalizatora , aby to zrobić, nie jest dobrym pomysłem, ponieważ zwykle nie ma gwarancji, kiedy (lub czy) finalizator zostanie uruchomiony. Zamiast tego należy użyć polecenia „spróbuj… w końcu”, aby upewnić się, że pobieranie i zwalnianie obiektu jest neutralne pod względem wyjątków.
Ręczne pule obiektów są proste do wdrożenia, ale trudniejsze w użyciu, ponieważ wymagają ręcznego zarządzania pamięcią obiektów puli.
Postępowanie z pustymi basenami
Pule obiektów wykorzystują jedną z trzech strategii do obsługi żądania, gdy w puli nie ma obiektów zapasowych.
- Niepowodzenie w dostarczeniu obiektu (i zwrócenie błędu do klienta).
- Przydziel nowy obiekt, zwiększając w ten sposób rozmiar puli. Baseny, które to robią, zwykle pozwalają ustawić znak wysokiej wody (maksymalna liczba obiektów, jakie kiedykolwiek zostały użyte).
- W środowisku wielowątkowym pula może blokować klienta, dopóki inny wątek nie zwróci obiektu do puli.
Pułapki
Należy uważać, aby stan obiektów zwróconych do puli został zresetowany z powrotem do rozsądnego stanu do następnego użycia obiektu, w przeciwnym razie obiekt może znajdować się w stanie nieoczekiwanym przez klienta, co może spowodować jego awarię. Pula jest odpowiedzialna za resetowanie obiektów, a nie klientów. Pule obiektów pełne obiektów o niebezpiecznie nieaktualnym stanie są czasami nazywane szambami obiektów i uważane za antywzorzec .
Nieaktualny stan nie zawsze może stanowić problem; staje się niebezpieczny, gdy powoduje nieoczekiwane zachowanie obiektu. Na przykład obiekt reprezentujący szczegóły uwierzytelnienia może się nie powieść, jeśli flaga „uwierzytelnienie pomyślnie” nie zostanie zresetowana przed ponownym użyciem, ponieważ wskazuje, że użytkownik jest uwierzytelniony (prawdopodobnie jako ktoś inny), gdy tak nie jest. Jednak niepowodzenie zresetowania wartości używanej tylko do debugowania, takiej jak tożsamość ostatnio używanego serwera uwierzytelniania, może nie powodować problemów.
Nieodpowiednie zresetowanie obiektów może spowodować wycieki informacji. Przedmioty zawierające poufne dane (np. numery kart kredytowych użytkownika) muszą zostać oczyszczone przed przekazaniem ich nowym klientom, w przeciwnym razie mogą zostać ujawnione osobom nieupoważnionym.
Jeśli pula jest używana przez wiele wątków, może potrzebować środków zapobiegających równoległym próbom ponownego użycia tego samego obiektu równolegle. Nie jest to konieczne, jeśli obiekty w puli są niezmienne lub w inny sposób bezpieczne wątkowo.
Krytyka
Niektóre publikacje nie zalecają używania puli obiektów w niektórych językach, takich jak Java , zwłaszcza w przypadku obiektów, które używają tylko pamięci i nie przechowują żadnych zasobów zewnętrznych (takich jak połączenia z bazą danych). Przeciwnicy zwykle twierdzą, że alokacja obiektów jest stosunkowo szybka we współczesnych językach z modułami zbierającymi elementy bezużyteczne ; podczas gdy operator new
potrzebuje tylko dziesięciu instrukcji, klasyczny new
- delete
pair znaleziona w projektach puli wymaga ich setek, ponieważ wykonuje bardziej złożoną pracę. Ponadto większość wyrzucaczy elementów bezużytecznych skanuje „żywe” odniesienia do obiektów, a nie pamięć używaną przez te obiekty do ich zawartości. Oznacza to, że dowolną liczbę „martwych” obiektów bez odniesień można odrzucić niewielkim kosztem. Natomiast przechowywanie dużej liczby „żywych”, ale nieużywanych obiektów wydłuża czas usuwania elementów bezużytecznych.
Przykłady
Iść
Poniższy kod Go inicjuje pulę zasobów o określonym rozmiarze (inicjowanie współbieżne), aby uniknąć problemów z wyścigiem zasobów przez kanały, aw przypadku pustej puli ustawia limit czasu przetwarzania, aby uniemożliwić klientom zbyt długie oczekiwanie.
// pula pakietów import puli pakietów ( "errors" "log" "math/rand" "sync" "time" ) const getResMaxTime = 3 * time . Druga zmienna ( ErrPoolNotExist = błędy . Nowy ( „ pula nie istnieje” ) ErrGetResTimeout = błędy . Nowy (
"get resource time out" ) ) // Typ zasobu Resource struct { resId int } // NewResource Symulacja powolnego tworzenia inicjalizacji zasobów // (np. połączenie TCP, pozyskiwanie klucza symetrycznego SSL, autoryzacja są czasochłonne) func NewResource ( id int ) * Zasób { czas . Sen ( czas 500 * . Milisekunda ) powrót
& Resource { resId : id } } // Do Zasoby symulacji są czasochłonne, a zużycie losowe wynosi 0~400 ms func ( r * Resource ) Do ( workId int ) { time . Uśpienie ( czas . Czas trwania ( rand . Intn ( 5 )) * 100 * czas . Milisekunda )
dziennik . Printf ( "używając zasobu #%d zakończono pracę %d zakończ\n" , r . resId , workId ) } // Pula oparta na implementacji kanału Go, aby uniknąć problemu z wyścigiem zasobów typu Pool chan * Resource // Nowa pula zasobów określonego rozmiaru // Zasoby są tworzone współbieżnie, aby zaoszczędzić czas inicjalizacji zasobów func New ( size int ) Pool { p :=
0
make ( pula , rozmiar ) wg := new ( sync . WaitGroup ) wg . Dodaj ( rozmiar ) dla i := ; i < rozmiar ; i ++ { go func ( resId int ) { p <- NewResource ( resId ) wg . Gotowe ()
}( ja ) } wg . Czekaj () return p } //GetResource na podstawie kanału, unika się stanu wyścigu zasobów i ustawia się limit czasu pozyskiwania zasobów dla pustej puli func ( p Pool ) GetResource () ( r * Resource , err error ) { select { case r := <- p : zwróć r , zero
przypadek <- czas . After ( getResMaxTime ): return nil , ErrGetResTimeout } } //GiveBackResource zwraca zasoby do puli zasobów func ( p Pool ) GiveBackResource ( r * Resource ) error { if p == nil { return ErrPoolNotExist } p <- r return
nil } // pakiet main package main import ( "github.com/tkstorm/go-design/creational/object-pool/pool" "log" "sync" ) func main () { // Inicjalizacja puli pięciu zasobów, // które można dostosować do 1 lub 10, aby zobaczyć różnicę size := 5 p := pool . New ( size ) // Wywołuje zasób w celu wykonania zadania id doWork :=
func ( workId int , wg * sync . WaitGroup ) { odroczyć wg . Gotowe () // Pobierz zasób z puli zasobów res , err := p . GetResource () jeśli err != nil { log . Println ( err ) return } // Zasoby do zwrócenia odroczenie p .
0 GiveBackResource ( res ) // Użyj zasobów do obsługi pracy res . Do ( workId ) } // Symulacja 100 współbieżnych procesów w celu pobrania zasobów z puli zasobów num := 100 wg := new ( Sync . WaitGroup ) wg . Dodaj ( num ) dla i := ; i < liczba ; ja ++ {
idź doWork ( i , wg ) } wg . Czekaj () }
C#
W bibliotece .NET Base Class Library istnieje kilka obiektów, które implementują ten wzorzec. System.Threading.ThreadPool
jest skonfigurowany tak, aby mieć wstępnie zdefiniowaną liczbę wątków do przydzielenia. Po zwróceniu wątków są one dostępne do innego obliczenia. W ten sposób można używać wątków bez ponoszenia kosztów tworzenia i usuwania wątków.
Poniżej przedstawiono podstawowy kod wzorca projektowego puli obiektów zaimplementowanego przy użyciu języka C#. Dla zwięzłości właściwości klas są deklarowane przy użyciu automatycznie zaimplementowanej składni właściwości języka C# 3.0. Można je zastąpić pełnymi definicjami właściwości dla wcześniejszych wersji języka. Pula jest pokazana jako klasa statyczna, ponieważ nie jest wymagane wiele pul. Jednak równie dopuszczalne jest używanie klas instancji dla pul obiektów.
przestrzeń nazw DesignPattern.Objectpool ; // Klasa PooledObject jest typem, którego instancja jest kosztowna lub powolna, // lub ma ograniczoną dostępność, dlatego należy ją przechowywać w puli obiektów. klasa publiczna PooledObject { private DateTime _createdAt = DateTime . teraz ; public DateTime CreatedAt { get { return _createdAt ; } } publiczny ciąg TempData
{ dostać ; zestaw ; } } // Klasa Pool kontroluje dostęp do obiektów znajdujących się w puli. Przechowuje listę dostępnych obiektów i // kolekcję obiektów, które zostały pozyskane z puli i są w użyciu. Pula zapewnia, że zwolnione obiekty // wracają do odpowiedniego stanu, gotowego do ponownego użycia. public static class Pool { private static List < PooledObject > _available = new List <
0
0
Obiekt w puli >(); private static Lista < PooledObject > _inUse = nowa lista < PooledObject >(); public static PooledObject GetObject () { lock ( _dostępny ) { if ( _dostępny . Count != ) { PooledObject po = _dostępny [ ]; _w użyciu
0
. Dodaj ( po ); _dostępny . Usuń w ( ); zwrot po ; } else { Obiekt w puli po = nowy Obiekt w puli (); _w użyciu . Dodaj ( po ); zwrot po ; } } } public static void ReleaseObject ( PooledObject po ) { CleanUp (
po ); lock ( _dostępny ) { _ dostępny . Dodaj ( po ); _w użyciu . Usuń ( po ); } } private static void CleanUp ( PopuledObject po ) { po . TempData = null ; } }
W powyższym kodzie obiekt PooledObject ma właściwości na czas, w którym został utworzony, oraz inny, który może być modyfikowany przez klienta, tj. resetowany, gdy obiekt PooledObject jest ponownie umieszczany w puli. Pokazany jest proces czyszczenia, po zwolnieniu obiektu, upewniając się, że jest on w prawidłowym stanie, zanim będzie można ponownie zażądać go z puli.
Jawa
Java obsługuje łączenie wątków za pośrednictwem java.util.concurrent.ExecutorService
i innych powiązanych klas. Usługa executora ma pewną liczbę „podstawowych” wątków, które nigdy nie są odrzucane. Jeśli wszystkie wątki są zajęte, usługa przydziela dozwoloną liczbę dodatkowych wątków, które są później odrzucane, jeśli nie są używane przez określony czas wygaśnięcia. Jeśli więcej wątków nie jest dozwolonych, zadania można umieścić w kolejce. Wreszcie, jeśli ta kolejka może być zbyt długa, można ją skonfigurować tak, aby zawieszała żądający wątek.
klasa publiczna PooledObject { public String temp1 ; publiczny Ciąg temp2 ; publiczny ciąg temp3 ; publiczny String getTemp1 () { return temp1 ; } public void setTemp1 ( String temp1 ) { to . temp1 = temp1 ; } publiczny ciąg getTemp2
() { zwróć temp2 ; } public void setTemp2 ( String temp2 ) { to . temp2 = temp2 ; } public String getTemp3 () { return temp3 ; } public void setTemp3 ( String temp3 ) { to . temp3 = temp3 ; }
}
public class PooledObjectPool { private static long expTime = 6000 ; //6 sekund public static HashMap < PooledObject , Long > available = new HashMap < PooledObject , Long > (); public static HashMap < PooledObject , Long > inUse =
nowy HashMap < PooledObject , Long > (); publiczne zsynchronizowane statyczne PooledObject getObject () { long now = System . bieżący CzasMillis (); if ( ! available . isEmpty ()) { for ( Map . Entry < PooledObject , Long > entry :
dostępny . entrySet ()) { if ( now - entry . getValue () > expTime ) { // obiekt wygasł popElement ( dostępny ); } else { PooledObject po = popElement ( dostępny , wpis . getKey ()); push ( w użyciu , po , teraz
); powrót po ; } } } // albo żaden PooledObject nie jest dostępny, albo każdy wygasł, więc zwróć nowy return createPooledObject ( now ); } private synchronized static PooledObject createPooledObject ( od dawna ) { PooledObject po = new PooledObject ( ); push ( w użyciu , po , teraz );
zwrot po ; } prywatny zsynchronizowany static void push ( HashMap < PooledObject , Long > map , PooledObject po , long now ) { map . umieścić ( po , teraz ); } public static void releaseObject ( PooledObject po ) {
sprzątanie ( po ); dostępny . put ( po , System . currentTimeMillis ()); w użyciu . usunąć ( po ); } private static PooledObject popElement ( HashMap < PooledObject , Long > map ) { Map . Wpis < obiekt w puli , długi >
wpis = mapa . zestaw wpisów (). iterator (). następny (); Klucz PooledObject = wpis . pobierz klucz (); //Długa wartość=entry.getValue(); mapa . usuń ( wpis . getKey ()); klawisz powrotu ; } private static PooledObject popElement ( HashMap < PooledObject , Long >
mapa , klucz PooledObject ) { mapa . usuń ( klucz ); klawisz powrotu ; } public static void cleanUp ( PopuledObject po ) { po . setTemp1 ( null ); po . setTemp2 ( null ); po . setTemp3 ( null ); } }
Zobacz też
Notatki
- Kircher, Michael; Prashant Jain (2002-07-04). „Wzorzec łączenia” (PDF) . EuroPLoP 2002 . Niemcy . Źródło 2007-06-09 .
- Goldsztein, Sasza; Zurbalew, Dima; Flatow, Ido (2012). Profesjonalna wydajność platformy .NET: optymalizacja aplikacji C# . Apress. ISBN 978-1-4302-4458-5 .