Active object – współbieżny wzorzec projektowy, którego celem jest współbieżne wykonywanie metod obiektu. We wzorcu oddziela się proces wywołania metody od jej wykonania, które odbywa się we własnym wątku obiektu[1]. Wywołania metod są kolejkowane i wykonywane sekwencyjnie przez planistę. W międzyczasie, wątek wywołujący metodę może zająć się wykonywaniem innych czynności w oczekiwaniu na pojawienie się wyniku.
W programowaniu współbieżnym pojawia się problem synchronizacji dostępu do zasobów. Wiele operacji nie może być jednocześnie wykonywanych przez więcej niż jeden wątek, gdyż skutkowałoby to zniszczeniem danych lub przekłamaniami wyników. Programista musi stosować różne techniki synchronizacji, jak monitory czy semafory. Zakładają one, że metody obiektu mogą być wykonywane przez różne wątki, a w sekcjach krytycznych następuje kolejkowanie w oczekiwaniu na dostęp. Nie sprawdzają się one, gdy chcemy, aby wątek w oczekiwaniu na wyniki zajął się wykonywaniem innych zadań.
Rozpatrywany we wzorcu aktywny obiekt posiada swój własny wątek sterujący, który zarządza wykonywaniem metod. Gdy jakiś wątek jest zainteresowany wywołaniem metody, wysyła do obiektu żądanie, po czym powraca do własnych zadań. Obiekt odbiera żądania, wykonuje je, po czym przekazuje wyniki z powrotem do wątku wywołującego. Jeśli dwa wątki chcą wywołać tę samą metodę, żądania są kolejkowane i obsługiwane jedno po drugim, dzięki czemu unikamy uszkodzeń danych.
Wzorzec składa się z sześciu elementów[2]:
Servant
– oryginalny obiekt, do którego chcemy zapewnić współbieżny dostęp.Proxy
– dostępny publicznie interfejs z metodami analogicznymi, jak w oryginalnym obiekcie, które generują odpowiednie żądania w imieniu wywołującego je wątku.Scheduler
– planista kontrolowany przez proxy, w którym wykonuje się własny wątek aktywnego obiektu. Planista odbiera żądania, kolejkuje je oraz wykonuje po kolei.ActivationQueue
– kolejka wywołań metod.MethodRequest
– interfejs żądania rozszerzony przez konkretne implementacje dla poszczególnych metod aktywnego obiektu. Przenosi argumenty wywoływanej metody oraz opisuje sposób jej wywołania.Future
– obiekt, do którego zapisywany zostanie wynik wykonania metody, zwracany wątkowi wywołującemu.Servant
jest zwyczajną klasą, do której obiektów chcemy zapewnić synchroniczny dostęp. Nie zawiera ona żadnych operacji współbieżnych oraz nie ma pojęcia o tym, czy będzie wykorzystana w środowisku jedno- czy wielowątkowym. Jej obiekty mogą być wykorzystane jednocześnie jako elementy wzorca active object oraz jako zwykłe bez żadnych strat wydajności. Zarządzanie dostępem do metod jest przeniesione poza klasę Servant
.
Przykładowa implementacja:
class Bufor { private int liczba = 0; public int zwieksz(int ilość) { zwiększ wartość zmiennej liczba o wartość ilość zwróć wartość zmiennej liczba } public int zmniejsz(int ilość) { zmniejsz wartość zmiennej liczba o wartość ilość zwróć wartość zmiennej liczba } }
Proxy
jest publicznie dostępnym interfejsem dostępowym dla aktywnego obiektu. Zawiera on dokładnie takie same metody, jak oryginalna klasa, które przyjmują dokładnie takie same argumenty. Zamiast wyniku zwracany jest obiekt klasy Future
, czyli rodzaj obietnicy dla wywołującego wątku, że w przyszłości znajdzie się tam wynik żądanej operacji. Metody Proxy
tworzą żądanie wywołania metody oraz obiekt Future
, łączą je ze sobą oraz przekazują żądanie do planisty.
Przykładowa implementacja:
class BuforProxy { private Scheduler planista; private Bufor aktywnyObiekt; konstruktor { utwórz obiekt planisty; utwórz właściwy bufor w aktywnyObiekt (Servant); } public Future zwieksz(int ilość) { utwórz obiekt Future: future; utwórz obiekt żądania dla metody "zwiększ": żądanie; przypisz do żądania argument ilość; przypisz do żądania obiekty future oraz aktywnyObiekt; dodaj żądanie do kolejki planisty; zwróć future } public Future zmniejsz(int ilość) { utwórz obiekt Future: future; utwórz obiekt żądania dla metody "zmniejsz": żądanie; przypisz do żądania argument ilość; przypisz do żądania obiekty future oraz aktywnyObiekt; dodaj żądanie do kolejki planisty; zwróć future } }
Istotne jest, że poszczególne metody klasy Proxy
nie są operacjami blokującymi. Gdy tylko żądanie znajdzie się w kolejce, sterowanie natychmiast powraca do wywołującego je wątku, nawet jeśli reprezentowana przez nie operacja nie została jeszcze wykonana. Jeśli wątek chce sprawdzić jej status, musi we własnym zakresie odpytać zwrócony obiekt Future
o dostępność wyniku.
Klasa MethodRequest
dostarcza interfejs żądania używany przez planistę. Jest ona rozszerzana przez konkretne klasy dla poszczególnych metod aktywnego obiektu. W skład interfejsu wchodzą dwie podstawowe metody:
guard()
– sprawdza, czy spełniony jest warunek umożliwiający wykonanie metody. Jest to odpowiednik zmiennych warunkowych w monitorach. Konkretne klasy implementują tutaj sprawdzenie odpowiedniego warunku i zwracają odpowiednią wartość logiczną jako wynik.execute()
– wykonuje odpowiadającą żądaniu metodę w oryginalnym obiekcie, a jej wynik zapisuje w przechowywanym obiekcie Future
.Konkretne klasy implementują we własnym zakresie sposób zapisania w żądaniu argumentów wywoływanej metody, a także sposób dostarczenia obiektów Servant
oraz Future
z Proxy
. Poniższy przykład demonstruje implementację takiej klasy dla metody zwieksz()
:
class ZwiekszMethodRequest {
private Future future;
private Bufor aktywnyObiekt;
private int ilość;
public bool guard() {
jeśli metoda zwieksz()
może być wywołana:
zwróć prawdę
w przeciwnym wypadku:
zwróć fałsz
}
public void execute() {
zaalokuj pamięć na wynik;
wywołaj z argumentem ilość;
skopiuj wynik do zaalokowanej pamięci;
przekaż wskaźnik zaalokowanej pamięci do obiektu future;
zapisz w obiekcie future informację, że żądanie zostało obsłużone;
}
}
Obiekt Scheduler
jest planistą, który zarządza wykonywaniem żądań. Wyposażony jest w kolejkę a na zewnątrz udostępnia operację enqueue
, która kolejkuje przekazane w argumencie żądanie. Planista pracuje we własnym wątku kontroli, pobierając kolejne żądania z kolejki, sprawdzając ich warunek oraz wykonując. Poniższy pseudokod przedstawia główną pętlę wątku planisty:
powtarzaj dopóki obiekt ma istnieć: pobierz z kolejki kolejne żądanie jeśli warunek jest spełniony: wykonaj metodę żądania w przeciwnym wypadku: dodaj z powrotem do kolejki
W przykładzie powyżej zastosowany został prosty planista, który wykonuje metody w kolejności ich nadchodzenia, a w razie niespełnienia warunku przenosi żądanie z powrotem na koniec kolejki. Programista może dostosować powyższy algorytm do własnych potrzeb.
Jest to tradycyjna kolejka z operacjami dequeue
oraz enqueue
. Musi ona być zaimplementowana jako monitor, ponieważ dodawanie żądania do kolejki może być wykonane zarówno przez planistę, jak i w obrębie wątku wywołującego metodę, gdy pragnie on wysłać żądanie.
Obiekty tej klasy są zwracane przez Proxy
jako wynik wywołania metody. Stanowią one gwarancję, że w przyszłości trafi do nich wynik wykonania właściwej metody aktywnego obiektu, dlatego wyposażone są w dwie podstawowe operacje:
Zalety[2]:
Wady[2]:
Zastosowanie wzorca można pokazać na przykładzie dostępu do współdzielonego pliku logów. Jest on używany przez większość komponentów systemu do rejestrowania zmian i wykonywanych czynności, dlatego należy zagwarantować, że w dowolnym momencie do pliku będzie pisać co najwyżej jeden wątek. Choć klasyczne muteksy pozwalają na osiągnięcie tego celu, posiadają one dwie istotne wady[3]. Po pierwsze, jeśli dwa wątki będą próbowały zapisać jednocześnie jakieś informacje do pliku, jeden z nich zostanie wstrzymany w oczekiwaniu na zakończenie pracy przez drugi[3]. Ponadto, jako mechanizm niskopoziomowy są słabo skalowalne[3].
Implementacja z zastosowaniem wzorca Active object oddziela zgłoszenie żądania wykonania metody zapisu od faktycznego zapisu. Wątki aplikacji mają do dyspozycji interfejs Proxy
, który pozwala im wysłać żądanie i natychmiast powrócić do realizacji swoich zadań, gdyż zazwyczaj nie potrzebują one otrzymania potwierdzenia wykonania takiego zapisu. Scheduler
może uwzględniać priorytet wiadomości przy kolejkowaniu żądań tak, by te istotniejsze były obsługiwane w pierwszej kolejności.
Istotną przewagą Active object nad klasycznymi muteksami jest skalowalność. Żądania wykonania zapisu mogą być przesyłane przez sieć, dzięki czemu tak zrealizowany obiekt logów może obsługiwać również systemy rozproszone, wykonywane na kilku fizycznych maszynach[3][2].
Active object jest często wykorzystywany w bibliotekach ORB (np. CORBA czy DCOM)[2][4]. Mimo różnic w nazewnictwie, zasada działania wewnętrznych mechanizmów jest bardzo zbliżona do definicji wzorca.