Active object

Active objectwspół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.

Problem

[edytuj | edytuj kod]

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.

Budowa

[edytuj | edytuj kod]
Diagram UML wzorca Active object

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.
  • Schedulerplanista kontrolowany przez proxy, w którym wykonuje się własny wątek aktywnego obiektu. Planista odbiera żądania, kolejkuje je oraz wykonuje po kolei.
  • ActivationQueuekolejka wywołań metod.
  • MethodRequestinterfejs żą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

[edytuj | edytuj kod]

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.

MethodRequest

[edytuj | edytuj kod]

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;     
   }
}

Scheduler

[edytuj | edytuj kod]

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.

ActivationQueue

[edytuj | edytuj kod]

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.

Future

[edytuj | edytuj kod]

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:

  • 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

[edytuj | edytuj kod]

Zalety[2]:

  • Odseparowanie mechanizmów współbieżnych od właściwej klasy, która dzięki temu jest dużo prostsza w budowie.
  • Przezroczyste wykorzystanie dostępnych mechanizmów współbieżności – planiści mogą być łatwo wymienialni, dlatego można dostosowywać ich do konkretnych zastosowań, np. wykorzystania możliwości wielu procesorów.
  • Kolejność wykonania metod może być inna, niż kolejność ich wywołania.
  • Wątki nie muszą czekać, aż wywołanie metody zostanie faktycznie obsłużone.

Wady[2]:

  • Niższa wydajność – wzorzec nie tylko korzysta z wewnętrznych mechanizmów synchronizacyjnych, ale wymaga też wykonania dużo większej liczby operacji przy każdorazowym wywołaniu metody. W porównaniu z innymi sposobami zapewnienia synchronizacji jest on dużo bardziej wymagający obliczeniowo.
  • Kłopotliwe debugowanie – z powodu niedeterministycznego działania planistów, debugowanie programów korzystających z active object jest trudniejsze. W dodatku wiele debugerów słabo wspiera wielowątkowe aplikacje.

Przykład zastosowania

[edytuj | edytuj kod]

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].

Implementacje

[edytuj | edytuj kod]

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.

Zobacz też

[edytuj | edytuj kod]

Przypisy

[edytuj | edytuj kod]
  1. D. Schmidt, Stal Stal, Hans Rohnert, Frank Buschmann: Pattern-Oriented Software Architecture, Volume 2: Patterns for Concurrent and Networked Objects. John Wiley & Sons, 2000. ISBN 0-471-60695-2.
  2. a b c d e R. Greg Lavender, Douglas C. Schmidt: Active Object. [dostęp 2009-01-10]. [zarchiwizowane z tego adresu (24 września 2012)]. (ang.).
  3. a b c d Herb Sutter: Know When to Use an Active Object Instead of a Mutex. Dr.Dobb’s Go Parallel, 2010-09-16. [dostęp 2010-09-17]. (ang.).
  4. Marvin V. Zelkowitz: Distributed Information Resources. Academic Press, 1999, s. 99. ISBN 0-12-012148-4.

Bibliografia

[edytuj | edytuj kod]