Active Object
Active object to wzorzec projektowy stosowany w programowaniu współbieżnym, którego zadaniem jest umożliwienie równoległego wykonywania metod obiektów. W tym wzorcu proces wywołania metody jest oddzielony od jej wykonywania, które odbywa się w dedykowanym wątku obiektu. Wywołania metod są kolejno umieszczane w kolejce i realizowane sekwencyjnie przez planistę. W międzyczasie, wątek, który wywołuje metodę, może wykonywać inne zadania, czekając na wyniki.
Problem
W kontekście programowania współbieżnego występuje problem z synchronizacją dostępu do zasobów. Niektóre operacje nie mogą być wykonywane jednocześnie przez więcej niż jeden wątek, ponieważ prowadziłoby to do uszkodzenia danych lub błędnych wyników. Programiści muszą stosować różne techniki synchronizacji, takie jak monitory czy semafory, które zakładają, że metody obiektu mogą być wywoływane przez różne wątki, a sekcje krytyczne oczekują na dostęp. Techniki te nie sprawdzają się jednak, gdy chcemy, aby wątek mógł wykonywać inne zadania podczas oczekiwania na wyniki.
W przypadku wzorca aktywnego obiektu, obiekt ma własny wątek sterujący, który zarządza wykonywaniem metod. Kiedy wątek chce wywołać metodę, wysyła do obiektu żądanie, a następnie wraca do swoich zadań. Obiekt odbiera żądania, realizuje je, a po zakończeniu przekazuje wyniki z powrotem do wątku, który je wywołał. Gdy dwa wątki próbują wywołać tę samą metodę, żądania są kolejkowane i obsługiwane jedno po drugim, co zapobiega uszkodzeniu danych.
Budowa
Wzorzec składa się z sześciu kluczowych elementów:
- Servant – oryginalny obiekt, do którego zapewniamy współbieżny dostęp.
- Proxy – publiczny interfejs z metodami analogicznymi do tych w oryginalnym obiekcie, które generują odpowiednie żądania w imieniu wątku wywołującego.
- Scheduler – planista kontrolowany przez proxy, który wykonuje własny wątek aktywnego obiektu. Odbiera żądania, kolejkuje je i realizuje po kolei.
- ActivationQueue – kolejka wywołań metod.
- MethodRequest – interfejs żądania, który jest rozszerzany przez konkretne implementacje dla poszczególnych metod aktywnego obiektu, przenosząc argumenty oraz definiując sposób wywołania metody.
- Future – obiekt, do którego zapisywany jest wynik wykonania metody, zwracany wątkowi, który ja wywołał.
Servant
Servant to standardowa klasa, której obiekty mają zapewnić synchroniczny dostęp. Nie zawiera operacji współbieżnych ani nie ma wiedzy o tym, czy będzie używana w środowisku jedno- czy wielowątkowym. Jej obiekty mogą pełnić rolę zarówno elementów wzorca Active Object, jak i zwykłych obiektów, bez strat wydajności. Zarządzanie dostępem do metod zostało 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
Proxy to publiczny interfejs dostępowy dla aktywnego obiektu. Zawiera metody identyczne jak w oryginalnej klasie, które przyjmują te same argumenty. Zamiast wyniku zwracany jest obiekt klasy Future, stanowiący obietnicę dla wywołującego wątku, że w przyszłości wynik żądanej operacji będzie dostępny. Metody Proxy tworzą żądanie wywołania metody oraz obiekt Future, łączą je i 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
}
}
Ważne jest, aby metody klasy Proxy nie były operacjami blokującymi. Gdy żądanie znajdzie się w kolejce, sterowanie natychmiast wraca do wątku, który je wywołał, nawet jeśli reprezentowana operacja nie została jeszcze zrealizowana. Jeśli wątek chce sprawdzić status, musi samodzielnie odpytać obiekt Future o dostępność wyniku.
MethodRequest
Klasa MethodRequest dostarcza interfejs żądania, który jest używany przez planistę. Jest ona rozszerzana przez konkretne klasy dla poszczególnych metod aktywnego obiektu. Interfejs zawiera dwie kluczowe metody:
- guard() – sprawdza, czy warunek umożliwiający wykonanie metody jest spełniony, odpowiednik zmiennych warunkowych w monitorach. Konkretne klasy implementują tutaj sprawdzenie warunku i zwracają odpowiednią wartość logiczną.
- execute() – wykonuje metodę odpowiadającą żądaniu w oryginalnym obiekcie i zapisuje wynik w obiekcie Future.
Konkretne klasy implementują sposób zapisania w żądaniu argumentów wywoływanej metody oraz sposób dostarczenia obiektów Servant oraz Future z Proxy. Oto przykład implementacji 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 aktywnyObiekt.zwieksz 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;
}
}
Scheduler
Obiekt Scheduler jest planistą, który zarządza realizacją żądań. Posiada kolejkę Q i na zewnątrz udostępnia operację enqueue, która kolejkuje przekazane w argumencie żądanie. Planista działa w swoim własnym wątku kontrolnym, pobierając kolejne żądania z kolejki, sprawdzając ich warunki oraz je wykonując. Poniższy pseudokod przedstawia główną pętlę wątku planisty:
powtarzaj dopóki obiekt ma istnieć:
pobierz z kolejki Q kolejne żądanie r
jeśli warunek r.guard() jest spełniony:
wykonaj metodę żądania r.execute()
w przeciwnym wypadku:
dodaj z powrotem r do kolejki Q
W powyższym przykładzie zastosowano prosty planista, który wykonuje metody w kolejności ich przybycia, a w przypadku niespełnienia warunku przenosi żądanie z powrotem na koniec kolejki. Programista może dostosować algorytm do własnych potrzeb.
ActivationQueue
ActivationQueue to standardowa kolejka z operacjami dequeue oraz enqueue. Powinna być zaimplementowana jako monitor, ponieważ dodawanie żądania do kolejki może być realizowane zarówno przez planistę, jak i w obrębie wątku wywołującego metodę, gdy chce wysłać żądanie.
Future
Obiekty klasy Future są zwracane przez Proxy jako wynik wywołania metody. Stanowią one gwarancję, że w przyszłości otrzymają wynik wykonania odpowiedniej metody aktywnego obiektu, dlatego mają dwie podstawowe operacje:
- czy dostępny – sprawdza, czy wynik wykonania metody aktywnego obiektu jest już dostępny,
- pobierz wynik – zwraca wynik wykonania właściwej metody.
Konsekwencje stosowania
Zalety:
- Oddzielenie mechanizmów współbieżnych od właściwej klasy, co czyni ją prostszą w konstrukcji.
- Przezroczyste wykorzystanie dostępnych mechanizmów współbieżności – planiści mogą być łatwo wymienialni, co pozwala na dostosowywanie ich do konkretnych zastosowań, np. wykorzystania możliwości wielu procesorów.
- Kolejność wykonania metod może różnić się od kolejności ich wywołania.
- Wątki nie muszą czekać na obsługę wywołania metody.
Wady:
- Niższa wydajność – wzorzec korzysta z wewnętrznych mechanizmów synchronizacyjnych i wymaga większej liczby operacji przy każdym wywołaniu metody, co czyni go bardziej obciążającym obliczeniowo.
- Trudności w debugowaniu – z powodu niedeterministycznego działania planistów, debugowanie programów wykorzystujących Active Object jest bardziej skomplikowane. Wiele narzędzi debugujących ma ograniczone wsparcie dla aplikacji wielowątkowych.
Przykład zastosowania
Wzorzec Active Object można zobrazować na przykładzie dostępu do współdzielonego pliku logów. Jest on wykorzystywany przez większość komponentów systemu do rejestrowania zmian i działań, dlatego ważne jest, aby w danym momencie do pliku mogła pisać tylko jedna instancja. Choć klasyczne muteksy pozwalają na osiągnięcie tego celu, mają dwie kluczowe wady. Po pierwsze, gdy dwa wątki próbują jednocześnie zapisać informacje do pliku, jeden z nich zostaje wstrzymany do zakończenia pracy drugiego. Po drugie, jako mechanizm niskopoziomowy, są słabo skalowalne.
Implementacja przy użyciu wzorca Active Object oddziela zgłoszenie żądania wykonania metody zapisu od samego zapisu. Wątki aplikacji korzystają z interfejsu Proxy, który umożliwia im wysłanie żądania i natychmiastowe powrócenie do realizacji swoich zadań, gdyż nie zawsze potrzebują potwierdzenia wykonanego zapisu. Scheduler może uwzględniać priorytety wiadomości przy kolejkowaniu żądań, aby te ważniejsze były obsługiwane w pierwszej kolejności.
Najważniejszą zaletą Active Object w porównaniu do klasycznych muteksów jest skalowalność. Żądania wykonania zapisu mogą być przesyłane przez sieć, co pozwala na obsługę systemów rozproszonych działających na kilku fizycznych maszynach.
Implementacje
Active Object jest często wykorzystywany w bibliotekach ORB (np. CORBA czy DCOM). Chociaż nazewnictwo się różni, zasada działania wewnętrznych mechanizmów jest zbliżona do definicji wzorca.
Zobacz też
Przypisy
Bibliografia
R. Greg Lavender, Douglas C. Schmidt: Active Object: An Object Behavioral Design Pattern For Concurrent Programming. [dostęp 2009-01-10]. [zarchiwizowane z tego adresu (24 września 2012)]. (ang.).
R. Greg Lavender, Douglas C. Schmidt: Active Object: An Object Behavioral Design Pattern For Concurrent Programming. W: John M. Vlissides, James O. Coplien, Norman L. Kerth: Pattern Languages of Program Design 2. Addison-Wesley, 1996. ISBN 0-201-89527-7. Brak numerów stron w książce.