Agent to typ referencyjny podobny do Atomu. Dzięki niemu można wyrażać częste, niekoordynowane zmiany stanów współdzielonych tożsamości w asynchroniczny sposób. Wykorzystywane są na przykład do obsługi niezależnych zdarzeń czy jednokierunkowej komunikacji między komponentami realizującymi dostęp do jednego zasobu (równoległe pobieranie danych z sieci, zapis do jednego pliku przez wiele wątków itp.).
Współbieżność
Wykonywanie współbieżne (ang. concurrent) to cecha systemów i programów komputerowych, w których te same obliczenia dokonywane są jednocześnie (w tym samym czasie) przez więcej niż jeden komponent, przy opcjonalnej komunikacji między komponentami.
Więcej o współbieżności w Clojure można przeczytać w odcinku 12.
Agenty
Agent to mechanizm, dzięki któremu można wyrażać częste, niekoordynowane i współdzielone zmiany stanów ustalonej tożsamości. W przeciwieństwie do atomów podmiany wartości dokonywane są asynchronicznie, to znaczy operacja aktualizacji odniesienia nie blokuje wykonywania się programu, lecz staje się zleceniem, które trafia do odpowiedniego bufora, gdzie oczekuje na realizację w osobnym wątku.
Agenty wykorzystywane są tam, gdzie zależy nam na zmianie stanu współdzielonego obiektu, lecz nie zależy nam na tym kiedy do tej zmiany dojdzie, bo ważniejsza jest dalsza, niezakłócona praca bieżącego wątku. Zastosowania Agentów to zwykle obsługa jednokierunkowych strumieni komunikatów (bezpieczny zapis do plików czy równoległe pobieranie danych z kilku źródeł), a ogólnie mówiąc wymiana danych w procesach zarządzania zdarzeniami (np. w systemach gromadzenia raportów czy sterowania niezależnymi zadaniami). Specyficznym zastosowaniem może być też kontrola współbieżności w wywołaniach tych metod Javy, które same nie są bezpieczne pod tym względem.
Agent
jest typem referencyjnym i przechowuje odniesienia, które mogą
być zmieniane z użyciem odpowiednich funkcji. Operacje zmian powiązań obiektów
tego typu z wartościami zachodzą w sposób atomowy – mogą w całości udać się lub
nie. To znaczy, że nie wystąpi sytuacja częściowej modyfikacji lub modyfikacji
dokonywanej w tym samym czasie przez więcej niż jeden wątek. Jeżeli zmiana
z jakichś przyczyn się nie powiedzie, będzie można zdiagnozować, że tak się
stało, lecz ewentualna reakcja na takie zdarzenie (np. ponowienie operacji) leży
w gestii programisty. Mimo że same zmiany są atomowe, nie możemy przewidzieć
w jakiej kolejności wystąpią.
Obiektowi typu Agent
można zlecić zmianę powiązania z wartością bieżącą
(zmianę stanu) przez przekazanie mu funkcji, która tego dokona.
W odpowiednim czasie funkcja ta zostanie wywołana i dojdzie do podmiany
wartości, z którą powiązany jest Agent. Nową wartością będzie ta zwrócona przez
przekazaną funkcję. Możemy następnie monitorować Agenta: sprawdzać, czy zmiana
się powiodła i/lub odczytywać jego bieżący stan.
Korzystanie z Agentów polega na utworzeniu odpowiedniego obiektu,
a następnie aktualizowaniu jego powiązań z wartościami. Zazwyczaj obiekt
typu Agent
będzie dodatkowo w sposób stały utożsamiany z symbolem określonym
zmienną globalną lub
powiązaniem leksykalnym. W ten sposób można będzie
identyfikować go z użyciem nazwy widocznej w odpowiednich obszarach programu.
Pule wątków
Aby asynchronicznie realizować zlecenia zmian stanów (powiązań referencyjnych z obliczanymi wartościami), Agenty korzystają z zaimplementowanego w maszynie wirtualnej Javy mechanizmu puli wątków (ang. thread pool).
Przypomnijmy, że wątek (ang. thread) to pewien fragment programu działający w obrębie jednego procesu, który może być wykonywany równolegle z innymi realizowanymi fragmentami tego procesu (innymi wątkami) i który współdzieli z nimi przestrzeń danych.
Pula wątków to, dokładniej rzecz ujmując, utworzona zgodnie z koncepcją tzw. wzorca puli obiektów (ang. object pool pattern) kolekcja obiektów reprezentujących wątki (służących do zarządzania nimi). Wątki w puli są inicjowane i gotowe do wykorzystania tuż po jej powstaniu, co eliminuje konieczność tworzenia ich za każdym razem, gdy zajdzie potrzeba użycia. Idea puli bazuje więc na tym, że istnieje określony zbiór wątków gotowych do realizacji zadań, które są z tego zbioru wyłączane, gdy pojawiają się zlecenia obsługi pewnych czynności, a kiedy kończą pracę, powracają do niego.
Każda pula jest wyposażona w kolejkę puli wątków (ang. thread pool queue), do której trafiają zadania przeznaczone do rozdzielenia między wolnymi wątkami. Kolejka jest potrzebna, ponieważ mogą zdarzać się sytuacje chwilowego wyczerpania puli (zajętości wszystkich wątków).
Z implementacyjnego punktu widzenia pula jest obiektem wyposażonym w jeden
z wariantów obecnego w Javie interfejsu Executor
(z tej przyczyny
reprezentujący ją obiekt nazywany też bywa wykonawcą), natomiast wątki
zadaniowe (ang. worker threads) reprezentowane są przez obiekty wyposażone
w interfejs Runnable
.
Spróbujmy przedstawić, jak krok po kroku wygląda korzystanie z pul w przypadku systemu Agentów obecnego w Clojure:
- Tworzony jest obiekt typu
Agent
. - Wywoływana jest funkcja zmiany stanu tego obiektu, która jako argument przyjmuje funkcję odpowiedzialną za przeprowadzenie obliczeń na bieżącej wartości powiązanej z Agentem, a zwraca nową wartość.
- Przekazany obiekt funkcyjny nie jest wywoływany od razu, lecz trafia do kolejki puli wątków w celu realizacji we właściwym czasie.
- Gdy w puli znajduje się wolny wątek, a zlecenie zmiany stanu jest na początku kolejki, funkcja przekazywana jest do realizacji w tym wątku.
- W wątku wywoływana jest funkcja wyliczająca nową wartość na bazie bieżącej.
- Jeżeli nie wystąpił błąd, a nowa wartość jest akceptowalna, dochodzi do
atomowej podmiany referencji w obiekcie typu
Agent
i zostaje on powiązany z tą wartością. - Wątek przestaje realizować zadanie zmiany stanu i zostaje oznaczony jako wolny (wraca do puli).
Rodzaje pul
Ze względu na liczbę wątków pula może być z góry ustalona (ang. fixed thread pool) lub rozszerzalna (ang. expandable thread pool). Ta ostatnia jest w przypadku Agentów dodatkowo buforowana (ang. cached thread pool). Pula o stałej liczbie wątków ma ich 2 plus tyle, ile dostępnych jest maszynie wirtualnej jednostek obliczeniowych. Z kolei do puli rozszerzalnej mogą być dynamicznie dodawane nowe wątki, jeżeli wystąpi taka konieczność.
Podstawowa obsługa Agentów polega na korzystaniu z dwóch już utworzonych pul. Pierwsza jest obiektem reprezentującym pulę o ustalonej liczbie wątków, a druga instancją puli rozszerzalnej. To, z której gotowej puli skorzystamy, zlecając aktualizację wartości bieżącej Agenta, zależy od użytej w tym celu funkcji.
Pula o ustalonej liczbie wątków dobrze nadaje się do przeprowadzania
krótkotrwałych obliczeń, korzystających z CPU. Zlecanie operacji zmiany
stanu Agenta, przez wysłanie obiektu funkcyjnego przeznaczonego do wykonania
przez jeden z wolnych wątków puli, odbywa się z użyciem funkcji send
.
Pula o rozszerzalnej liczbie wątków przydaje się wtedy, gdy chcemy obsługiwać
wiele czasochłonnych operacji, które mogą blokować wątki na dłużej.
Przykładem może być tu interakcja z podsystemem wejścia/wyjścia czy mechanizmami
komunikacji międzyprocesowej (terminal, pliki, gniazda sieciowe, pliki,
systemowe kolejki FIFO itp.). Przy stałej puli czynności tego typu szybko
doprowadziłyby do jej wyczerpania z powodu zajęcia wszystkich wolnych wątków
zadaniami oczekującymi na dane wejściowe. Programista może zlecić operację
zmiany stanu Agenta za pośrednictwem tej puli, używając funkcji
send-off
.
Wątki z puli rozszerzalnej cechuje określony z góry czas bezczynności (ang. timeout), po którym są one niszczone (domyślnie 1 minuta). Dodatkowo Clojure korzysta z wspomnianego, buforowanego wariantu tej puli. Oznacza to, że jeżeli jakiś wątek zakończy poprawnie pracę przed określonym czasem, a w kolejce pojawi się nowe zadanie do zrealizowania, to zamiast stwarzać nowy wątek, wykorzystane zostaną zasoby bieżącego, który nie został jeszcze zniszczony. W efekcie pule tego rodzaju nadają się również do realizowania zadań związanych z dużą liczbą krótkich obliczeń czy obsługą napływających komunikatów, ponieważ moc obliczeniowa nie jest marnotrawiona na operacja wielokrotnego stwarzania i niszczenia wątkowych obiektów.
Poza dwoma wbudowanymi pulami o odpowiednich dla większości problemów
charakterystykach, możemy również skorzystać z puli wątków, którą wcześniej
samodzielnie utworzyliśmy z użyciem odpowiednich wywołań metod Javy. Do
obsługi takich przypadków służy funkcja send-via
.
Zwalnianie pul
Wątki używane do obsługi Agentów w Clojure nie są tzw. demonami, zwanymi
też wątkami demonowymi (ang. daemon threads). Oznacza to między innymi, że
aktywne korzystanie z nich może wiązać się z tym, iż maszyna wirtualna Javy
nie przestanie działać po przerwaniu lub zakończeniu pracy przez aplikację.
W przypadku puli rozszerzalnej do zakończenia wprawdzie dojdzie, ale dopiero po
upłynięciu maksymalnego czasu bezczynności ostatnio aktywnego wątku. Możemy temu
zaradzić, niszcząc pule wątków używane do obsługi Agentów przez wywołanie funkcji
shutdown-agents
.
Tworzenie
Tworzenie obiektów typu Agent
, agent
Do tworzenia obiektów typu Agent
służy funkcja agent
. Przyjmuje ona jeden
obowiązkowy argument – wartość początkową (z którą powiązany zostanie Agent)
i argumenty nieobowiązkowe opcji, wyrażane w sposób nazwany, czyli w postaci par
klucz–wartość.
Funkcja zwraca obiekt typu Agent
.
Użycie:
(agent wartość-początkowa & opcje)
.
Możliwe opcje to:
:meta mapa-metadanowa
,:validator funkcja
,:error-handler funkcja
,:error-mode klucz-trybu
.
Mapa metadanowa po słowie kluczowym :meta
powinna być mapą
wyrażoną z użyciem powiązanego z nią symbolu lub osadzoną w kodzie jako
tzw. mapowe S-wyrażenie. Podane asocjacje staną się
metadanymi tworzonego obiektu referencyjnego.
Funkcja podawana po słowie kluczowym :validator
powinna być jednoargumentowym
obiektem funkcyjnym, który nie generuje efektów ubocznych, albo wartością nil
.
Funkcja ta będzie wywoływana za każdym razem, gdy zażądano zmiany stanu obiektu
typu Agent
i przekazywana jej będzie proponowana, nowa wartość. Powinna ona
zwracać false
lub zgłaszać wyjątek, gdy wartość ta jest nieakceptowalna.
Operację taką nazywamy walidacją, a wyrażającą ją funkcję
walidatorem. Pierwsze wywołanie walidatora będzie testowało
wartość początkową Agenta i natychmiastowo komunikowało błąd. Kolejne wywołania,
z racji asynchronicznej natury Agentów, będą zwracały obiekt typu Agent
niezależnie od tego czy nowa wartość była poprawnie zweryfikowana. Zamiast tego
zostanie uruchomiony kod specjalnej funkcji, której zadaniem jest obsługa
sytuacji wyjątkowych – oczywiście pod warunkiem, że została ona wcześniej
zarejestrowana.
Wspomnianą funkcję, która będzie służyła do obsługi sytuacji wyjątkowych, można
skojarzyć z Agentem przez podanie jej obiektu po słowie kluczowym
:error-handler
. Będzie ona wywołana wtedy, gdy nowa wartość bieżąca nie spełni
kryteriów walidacji lub wystąpi innego rodzaju błąd i stan Agenta
nie zostanie zaktualizowany. Funkcja obsługująca błędy powinna przyjmować dwa
argumenty: jako pierwszy przekazywany jej będzie obiekt obsługiwanego Agenta,
a jako drugi obiekt wyjątku, który został zgłoszony przy nieudanej próbie zmiany
stanu. Podanie funkcji obsługującej sytuacje wyjątkowe sprawia, że automatycznie
ustawiana jest opcja :error-mode
, a jej wartością jest :continue
.
Jeżeli podamy słowo kluczowe :error-mode
, musimy umieścić po nim kolejne słowo
kluczowe, aby wybrać jeden z dwóch sposobów reakcji Agenta na usterki:
:continue
– kontynuacja pracy,:fail
– wstrzymanie przyjmowania zleceń zmiany stanu.
Jeżeli podano również funkcję obsługującą usterki (z użyciem :error-handler
),
domyślnym sposobem reakcji na sytuację wyjątkową jest kontynuowanie pracy bez
oznaczania Agenta jako wadliwego (odpowiednik :continue
). Jeżeli jednak nie
zarejestrowano takiej funkcji, reakcją na błąd podczas aktualizowania stanu
Agenta będzie oznaczenie Agenta jako wadliwego i zablokowanie dalszej realizacji
zleceń zmiany wartości bieżącej (odpowiednik :fail
) do momentu wywołania
funkcji restart-agent
. Złożone w tym czasie zlecenia będą
oczekiwały na ten moment w odpowiedniej kolejce.
agent
|
|
(agent 0 ) ; => #<[email protected]: 0>
(agent 0 :meta {:a 1}) ; => #<[email protected]: 0>
(agent [1 2 3]) ; => #<[email protected]: [1 2 3]>
(agent 10 :validator #(> %1 1)) ; => #<[email protected]: 10>
(agent 0 :validator #(> %1 1)) ; => java.lang.IllegalStateException
Wyrażenie z linii nr 1 tworzy obiekt referencyjny typu Agent
, który odnosi się
do wartości 0.
Wyrażenie drugie (z linii nr 2) działa tak samo, lecz dodatkowo ustawia metadaną.
Kolejne wyrażenie przykładu (z linii nr 3) ustawia wartość początkową na wektor stworzony z użyciem wektorowego S-wyrażenia.
Dwa ostatnie wyrażenia dotyczą sytuacji, w których przekazywana jest funkcja walidująca. Ostatnie wywołanie powoduje zgłoszenie wyjątku, ponieważ podana wartość początkowa nie jest poprawna.
Identyfikowanie Agentów
Agenty są obiektami, które z którymi, podobnie jak z innymi pamięciowymi
wartościami, można powiązać symboliczne identyfikatory i w ten sposób
określać ich tożsamości. Możemy więc stwarzać odnoszące się do obiektów typu
Agent
globalne zmienne, jak również korzystać z innych
rodzajów powiązań (np. leksykalnych).
Agent
|
|
(def nasz-agent (agent 0)) ; zmienna globalna
(let [agencik (agent 0)] agencik) ; powiązanie leksykalne
Pobieranie wartości
Odczytywanie wartości Agentów, deref
Żeby odczytać wartość Agenta możemy użyć funkcji deref
. Przyjmuje ona jeden
argument, którym powinien być obiekt typu Agent
, a zwraca wartość, do której
odniesienie jest przechowywane w tym obiekcie.
Użycie:
(deref agent)
.
deref
|
|
(def nasz-agent (agent 0))
(deref nasz-agent)
; => 0
Dereferencja Agentów, makro @
Makro czytnika @
(znak małpki) umieszczone przed wyrażeniem sprawia, że jeżeli
zwracany przez nie obiekt jest typem referencyjnym, to wywołana zostanie na nim
funkcja odpowiedzialna za odczyt wskazywanej wartości. W przypadku Agentów
będzie to omówiona wyżej funkcja deref
.
Użycie:
@agent
.
@
|
|
(def nasz-agent (agent 0))
@nasz-agent
; => 0
Zmiany wartości
Aktualizowanie wartości, send
Zmiana stanu obiektu typu Agent
, czyli aktualizacja odniesienia do wartości,
możliwa jest z zastosowaniem funkcji send
.
Pierwszy przyjmowany argument powinien być symbolem identyfikującym obiekt typu
Agent
lub inne wyrażenie, które przeliczone zwróci ten obiekt. Drugim
argumentem powinna być funkcja, która jako pierwszy argument przyjmie wartość
bieżącą (do której odnosi się Agent) i na jej podstawie obliczy nową wartość.
Zostanie ona użyta do zastąpienia poprzedniego referencyjnego odniesienia
w Agencie. Opcjonalnie możemy podać dodatkowe argumenty, które zostaną
przekazane jako kolejne argumenty wywoływanej funkcji.
Funkcja zwracająca nową wartość bieżącą Agenta powinna być nieblokująca, ponieważ jej wartość będzie obliczana w wątku należącym do puli o ustalonej liczbie wątków.
Gdyby któryś z kroków pośrednich (między odczytem a aktualizacją referencji) nie powiódł się, będziemy mieli do czynienia z usterką Agenta, czyli sytuacją wyjątkową. Może ona wystąpić z powodu błędu lub niespełnienia wymogów skojarzonej z Agentem funkcji walidującej. W takim przypadku Agent przestaje realizować kolejne zlecenia zmiany stanu, chociaż są one kolejkowane (jeżeli są poprawne). Można zmienić ten domyślny sposób reakcji na usterki, rejestrując funkcję obsługi błędów, lub ustawiając odpowiednią opcję trybu reakcji na usterki.
Wartością zwracaną przez funkcję send
jest obiekt typu Agent
, do którego
wysłano zlecenie zmiany stanu.
Użycie:
(send agent funkcja & argument…)
.
send
|
|
(def nasz-agent (agent 0))
@nasz-agent ; => 0
(send nasz-agent inc) ; => #<[email protected]: 0>
@nasz-agent ; => 1
Długotrwałe aktualizowanie wartości, send-off
Zmiana stanu obiektu typu Agent
, czyli aktualizacja odniesienia do wartości,
gdy funkcja wyliczająca nową wartość zajmuje dłuższy czas, możliwa jest
z zastosowaniem funkcji send-off
.
Pierwszy przyjmowany argument powinien być symbolem identyfikującym obiekt typu
Agent
lub inne wyrażenie, które przeliczone zwróci ten obiekt. Drugim
argumentem powinna być funkcja, która jako pierwszy argument przyjmie wartość
bieżącą (do której odnosi się Agent) i na jej podstawie obliczy nową wartość.
Zostanie ona użyta do zastąpienia poprzedniego referencyjnego odniesienia
w Agencie. Opcjonalnie możemy podać dodatkowe argumenty, które zostaną
przekazane jako kolejne argumenty wywoływanej funkcji.
Funkcja zwracająca nową wartość bieżącą Agenta może być blokująca, np. związana z przetwarzaniem danych wejścia/wyjścia czy obsługą komunikacji międzyprocesowej, ponieważ jej wartość będzie obliczana w wątku należącym do puli o rozszerzalnej liczbie wątków.
Gdyby któryś z kroków pośrednich (między odczytem a aktualizacją referencji) nie powiódł się, będziemy mieli do czynienia z usterką Agenta, czyli sytuacją wyjątkową. Może ona wystąpić z powodu błędu lub niespełnienia wymogów skojarzonej z Agentem funkcji walidującej. W takim przypadku Agent przestaje realizować kolejne zlecenia zmiany stanu, chociaż są one kolejkowane (jeżeli są poprawne). Można zmienić ten domyślny sposób reakcji na usterki, rejestrując funkcję obsługi błędów, lub ustawiając odpowiednią opcję trybu reakcji na usterki.
Wartością zwracaną przez funkcję send-off
jest obiekt typu Agent
, do którego
wysłano zlecenie zmiany stanu.
Użycie:
(send-off agent funkcja & argument…)
.
send-off
|
|
(def nasz-agent (agent 0))
@nasz-agent ; => 0
(send-off nasz-agent inc) ; => #<[email protected]: 0>
@nasz-agent ; => 1
Aktualizowanie wartości z pulą własną, send-via
Dzięki funkcji send-via
możemy zmienić stan obiektu typu Agent
, posługując
się własnym obiektem wykonawcy, czyli samodzielnie utworzoną pulą wątków, do
której trafi zlecenie wyliczenia nowej wartości bieżącej Agenta.
Pierwszy przyjmowany argument powinien być obiektem implementującym interfejs
Executor
, a kolejny symbolem określającym obiekt typu Agent
lub inne
wyrażenie, które przeliczone zwróci ten obiekt. Jako trzeci argument należy
podać funkcję, która jako pierwszy argument przyjmie wartość bieżącą (do której
odnosi się Agent) i na jej podstawie obliczy nową wartość. Zostanie ona użyta do
zastąpienia poprzedniego referencyjnego odniesienia w Agencie. Opcjonalnie
możemy podać dodatkowe argumenty, które zostaną przekazane jako kolejne
argumenty wywoływanej funkcji.
Gdyby któryś z kroków pośrednich (między odczytem a aktualizacją referencji) nie powiódł się, będziemy mieli do czynienia z usterką Agenta, czyli sytuacją wyjątkową. Może ona wystąpić z powodu błędu lub niespełnienia wymogów skojarzonej z Agentem funkcji walidującej. W takim przypadku Agent przestaje realizować kolejne zlecenia zmiany stanu, chociaż są one kolejkowane (jeżeli są poprawne). Można zmienić ten domyślny sposób reakcji na usterki, rejestrując funkcję obsługi błędów, lub ustawiając odpowiednią opcję trybu reakcji na usterki.
Wartością zwracaną przez funkcję send-via
jest obiekt typu Agent
, do którego
wysłano zlecenie zmiany stanu.
Użycie:
(send-via wykonawca agent funkcja & argument…)
.
send-via
|
|
(import java.util.concurrent.Executors)
(def nasza-pula (Executors/newFixedThreadPool 32))
(def nasz-agent (agent 0))
@nasz-agent ; => 0
(send-via nasza-pula nasz-agent inc) ; => #<[email protected]: 0>
@nasz-agent ; => 1
Zobacz także:
* „Class Executors”, dokumentacja Javy SE
Walidatory
Agenty można opcjonalnie wyposażyć w mechanizmy testujące poprawność ustawianych
wartości bieżących. Służą do tego funkcje set-validator!
i get-validator
.
Walidator (ang. validator) to w tym przypadku czysta (wolna od efektów
ubocznych), jednoargumentowa funkcja, która przyjmuje wartość poddawaną
sprawdzaniu. Jeżeli jest ona niedopuszczalna, funkcja powinna zwrócić wartość
false
lub wygenerować wyjątek.
Zarządzanie walidatorami, set-validator!
Funkcja set-validator!
umożliwia ustawianie lub usuwanie
walidatora dla podanego jako pierwszy argument obiektu typu
Agent
.
Jako drugi argument należy podać funkcję, która powinna przyjmować jeden
argument i nie generować efektów ubocznych. Będzie ona wywoływana za każdym
razem, gdy dojdzie do zmiany stanu referencji i jako argument przekazywana
będzie jej wartość, z którą zażądano powiązania. Funkcja ta powinna zwracać
wartość false
lub zgłaszać wyjątek, gdy przyjmowana wartość jest
nieakceptowalna.
Pierwsze sprawdzenie stanu jest dokonywane już w momencie ustawiania walidatora
i w wątku, w którym doszło do wywołania funkcji. Bieżąca wartość wskazywana
referencją musi więc już wtedy być odpowiednia. Kolejne wywołania walidatora
odbywają się asynchronicznie, w wątkach odpowiedzialnych za realizację zleceń
podmiany wartości bieżącej. Jeżeli sprawdzanie poprawności nie powiedzie się,
Agent zostanie oznaczony jako wadliwy i nowe zlecenia nie będą realizowane,
lecz kolejkowane do momentu obsługi sytuacji wyjątkowej w programie. Wyjątkiem
od tej zasady jest obsługa ich natychmiast, przez zarejestrowanie specjalnej
funkcji obsługującej reakcje na błędy – albo podczas tworzenia
obiektu typu Agent
, albo korzystając z funkcji
set-error-handler!
i set-error-mode!
.
Jeżeli zamiast obiektu funkcyjnego podamy wartość nil
, to walidator zostanie
odłączony od podanego obiektu typu Agent
.
Funkcja set-validator!
zwraca wartość nil
, jeżeli udało się ustawić
walidator.
set-validator!
|
|
(def nasz-agent (agent 1))
(set-validator! nasz-agent pos?) ; akceptujemy tylko liczby dodatnie
(send nasz-agent (constantly 2)) ; #<[email protected]: 1>
@nasz-agent ; => 2
(send nasz-agent (constantly -1)) ; #<[email protected] FAILED: 2>
@nasz-agent ; => 2
(send nasz-agent (constantly 3))
; >> java.lang.RuntimeException: Agent is failed, needs restart
; >> java.lang.IllegalStateException: Invalid reference state
@nasz-agent ; => 2
(restart-agent nasz-agent 4) ; => 4
@nasz-agent ; => 4
Pobieranie walidatora, get-validator
Mając obiekt typu Agent
, możemy pobrać funkcyjny obiekt jego walidatora,
posługując się funkcją get-validator
. Przyjmuje ona jeden argument, który
powinien być obiektem typu Agent
, a zwraca obiekt funkcji lub nil
, gdy
walidatora nie ustawiono.
Funkcja zwraca wartość nil
.
Użycie:
(get-validator agent)
.
get-validator
|
|
(def nasz-agent (agent 0))
(def nasz-walidator #(< % 5))
(set-validator! nasz-agent nasz-walidator)
; => nil
(get-validator nasz-agent)
; => #<user$nasz_walidator [email protected]>
Obsługa usterek
Agent, do którego trafiło zlecenie podmiany wartości bieżącej, może napotkać
błąd podczas realizowania tego zadania. W takim wypadku zostanie on oznaczony
jako wadliwy (ang. failed). Błąd może wynikać z usterki podczas działania
podprogramu funkcji obliczającej nową wartość, albo być spowodowany tym, że nowa
wartość nie spełnia założonych kryteriów poprawności. Funkcję
decydującą o tym, jakie wartości są poprawne, można skojarzyć z Agentem
podczas tworzenia jego obiektu lub później, wywołując
set-validator!
.
Gdy Agent zostanie oznaczony jako wadliwy, nadal przyjmuje zlecenia zmian
stanów, jednak nie są one realizowane, ale oczekują w kolejce do momentu, aż
wyjątkowa sytuacja zostanie obsłużona w programie, a Agent oznaczony jako
poprawny. Wywołania funkcji odpowiedzialnych za składanie zleceń podmiany
wartości bieżącej (np. z użyciemsend
, send-off
czy
send-via
), gdy Agent oznaczony jest jako wadliwy, będą powodowały
zgłoszenie wyjątku: tego samego, który został wygenerowany podczas próby
obliczenia i zmiany wartości bieżącej.
Zachowanie Agenta w kwestii obsługi sytuacji wyjątkowych można zmieniać,
rejestrując własną funkcję – będzie ona wywoływana za każdym razem, gdy dojdzie
do usterki. Podpięcia takiej funkcji można dokonać, ustawiając odpowiednią opcję
reakcji na błąd, podczas tworzenia obiektu typu Agent
lub później,
korzystając z funkcji set-error-handler!
.
Istnieje również możliwość ustawienia specjalnej opcji Agenta, która sprawia, że
w przypadku wystąpienia usterki lub próby powiązania z niepoprawną wartością nie
będzie on oznaczany jako wadliwy. Można ją ustawić podczas tworzenia obiektu
typu Agent
lub korzystając z funkcji set-error-mode!
.
Sprawdzanie błędnego stanu, agent-error
Dzięki funkcji agent-error
możemy sprawdzić, czy wyrażony pierwszym argumentem
Agent jest oznaczony jako wadliwy, tzn. czy wymaga obsłużenia sytuacji
wyjątkowej.
Funkcja zwraca obiekt wyjątku, który doprowadził do oznaczenia Agenta jako
wadliwego lub wartość nil
, jeżeli Agent nie został w taki sposób oznaczony –
nie wystąpiła usterka, walidator nie zgłosił niepoprawności, albo
ustawiono ignorowanie takich sytuacji z użyciem
set-error-mode!
.
Użycie:
(agent-error agent)
.
agent-error
|
|
(def nasz-agent (agent 10 :validator pos?))
(send nasz-agent (constantly -1))
; => #<[email protected] FAILED: 10>
(agent-error nasz-agent)
; >> #<IllegalStateException java.lang.IllegalStateException: Invalid reference state>
Ponowne uruchamianie, restart-agent
Funkcja restart-agent
pozwala dokonać ponownego uruchomienia Agenta, który
został oznaczony jako wadliwy i powiązać go z nową wartością bieżącą
(wyrażoną formą stałą, a nie obiektem funkcyjnym). Przyjmuje ona dwa
obowiązkowe argumenty: obiekt typu Agent
i nową wartość.
Nowa wartość musi pozytywnie przejść test walidacji. W przeciwnym razie zgłoszony zostanie wyjątek, a oznaczenie Agenta jako wadliwy nie zostanie usunięte.
Nawet jeżeli z obiektem Agenta skojarzono podpięcia w postaci funkcji
obserwujących, to nie będą one wywoływane. Poza tym próba wywołania funkcji
na obiekcie, który nie został oznaczony jako wadliwy spowoduje zgłoszenie
wyjątku java.lang.RuntimeException
, komunikującego, że nie ma potrzeby
ponownego uruchamiania.
Jako kolejne, niewymagane argumenty nazwane możemy podać opcje wyrażone parami złożonymi ze słów kluczowych i przypisanych im wartości.
Wartością zwracaną przez funkcję restart-agent
jest wartość nowo ustawionego
powiązania.
Użycie:
(restart-agent agent wartość & opcje)
.
Gdzie możliwe opcje
to:
:clear-actions przełącznik
.
Jeżeli podamy opcję :clear-actions
, a przełącznik
ustawimy na wartość
reprezentującą logiczną prawdę (nie false
i nie nil
), to każde oczekujące
w kolejce zlecenie wykonania zadania na obiekcie Agenta, który uległ awarii,
zostanie usunięte. Domyślnym działaniem jest zrealizowanie wszystkich zleceń
zmiany stanu.
restart-agent
|
|
(def nasz-agent (agent 10 :validator pos?))
@nasz-agent ; => 10
(send nasz-agent (constantly -1)) ; => #<[email protected] FAILED: 10>
@nasz-agent ; => 10
(restart-agent nasz-agent 5) ; => 5
@nasz-agent ; => 5
Ustawianie funkcji obsługującej błędy, set-error-handler!
Funkcja set-error-handler!
rejestruje funkcję, która będzie służyła do obsługi
sytuacji wyjątkowych. Będzie ona wywołana wtedy, gdy nowa wartość bieżąca nie
spełni kryteriów walidacji lub wystąpi innego rodzaju błąd i stan Agenta nie
zostanie zaktualizowany.
Przyjmuje ona jeden argument, którym powinna być wspomniana funkcja, a zwraca
wartość nil
.
Użycie:
(set-error-handler! agent funkcja)
.
Funkcja obsługująca błędy powinna przyjmować dwa argumenty: jako pierwszy przekazywany jej będzie obiekt Agenta, a jako drugi obiekt wyjątku, który został zgłoszony przy nieudanej próbie zmiany stanu.
set-error-handler!
|
|
(def nasz-agent (agent 10 :validator pos?))
(set-error-handler! nasz-agent #(println (apply str (interpose ",\n" %&))))
(send nasz-agent (constantly -1))
; => #<[email protected]: 10>
; >> [email protected],
; >> java.lang.IllegalStateException: Invalid reference state
@nasz-agent
; => 10
(restart-agent nasz-agent 4)
; => 4
Ustawianie trybu reakcji na błędy, set-error-mode!
Funkcja set-error-mode!
ustawia opcję Agenta decydującą o sposobie reagowania
na błędy podczas próby zmiany wartości bieżącej (niedopuszczenie przez
walidator lub błąd w realizowaniu obliczeń przez funkcję
obliczającą nową wartość).
Użycie:
(set-error-mode! agent tryb)
.
Gdzie tryb
to jedno ze słów kluczowych:
:continue
– kontynuacja pracy,:fail
– wstrzymanie przyjmowania zleceń zmiany stanu.
Podanie słowa kluczowego :fail
sprawi, że w przypadku wystąpienia sytuacji
wyjątkowej podczas aktualizowania wartości bieżącej Agenta (np. z użyciem
send
, send-off
czy send-via
) zostanie on
oznaczony jako wadliwy, a realizowanie zleceń zmiany stanów będzie wstrzymane do
momentu wywołania restart-agent
. Jest to domyślny sposób
reakcji na usterki.
Podanie słowa kluczowego :continue
zmienia sposób reagowania na sytuacje
wyjątkowe. W przypadku ich wystąpienia Agent nie będzie oznaczony jako wadliwy,
a zlecenia zmiany stanów będą realizowane.
set-error-mode!
|
|
(def nasz-agent (agent 10 :validator pos?))
(set-error-mode! nasz-agent :continue)
(send nasz-agent (constantly -1))
; => #<[email protected]: 10>
; >> [email protected],
; >> java.lang.IllegalStateException: Invalid reference state
@nasz-agent
; => 10
(send nasz-agent (constantly 4))
@nasz-agent
; => 4
Pobieranie funkcji obsługującej błędy, error-handler
Funkcja error-handler
umożliwia odczytanie obiektu funkcyjnego skojarzonego
z agentem w celu obsługi sytuacji wyjątkowych. Przyjmuje ona jeden obowiązkowy
argument, którym powinien być obiekt typu Agent
, a zwraca funkcję lub wartość
nil
, jeżeli funkcji obsługującej błędy dla danego Agenta nie ustawiono.
Użycie:
(error-handler agent)
.
error-handler
|
|
(def nasz-agent (agent 0))
(error-handler nasz-agent)
; => nil
(set-error-handler! nasz-agent identity)
(error-handler nasz-agent)
; => #<core$identity [email protected]>
Pobieranie trybu reakcji na błędy, error-mode
Funkcja error-mode
umożliwia odczytanie wartości opcji Agenta określającej
tryb reakcji na sytuacje wyjątkowe. Przyjmuje ona jeden obowiązkowy argument,
którym powinien być obiekt typu Agent
, a zwraca słowo kluczowe, które określa
sposób reakcji na błędy.
Użycie:
(error-mode agent)
.
error-mode
|
|
(def nasz-agent (agent 0))
(error-mode nasz-agent)
; => :fail
(set-error-mode! nasz-agent :continue)
(error-mode nasz-agent)
; => :continue
Podpięcia
Obiekty typu Agent
można wyposażać w podpięcia (ang. hooks) w postaci
tzw. funkcji obserwujących (ang. watch functions). Wywoływane są one, gdy
zmienia się stan referencji. Służą do wykonywania dodatkowych, niezależnych
operacji w reakcji na zmiany stanów.
Dodawanie obserwatorów, add-watch
Do obiektu typu Agent
możemy dodawać funkcję obserwującą z użyciem
add-watch
. Funkcja ta przyjmuje trzy argumenty. Pierwszym powinien być Agent,
do którego chcemy podłączyć funkcję, drugim unikatowy klucz identyfikujący
podpięcie, a ostatnim wspomniana funkcja. Unikatowość powinna być zachowana
w obrębie pojedynczego, nadzorowanego obiektu.
Funkcja add-watch
zwraca obiekt typu Agent
, do którego podpięto funkcję
obserwującą.
Funkcja obserwująca powinna przyjmować 4 argumenty: klucz, obiekt typu Agent
,
a także poprzednią wartość oraz nową wartość wskazywaną przez referencję.
Zostanie ona wywołana w sposób asynchroniczny za każdym razem, gdy dojdzie do
zmiany stanu Agenta, przy czym może się to zdarzyć również w czasie jej
wywoływania. W związku z tym należy polegać na przekazanych jako dwa ostatnie
argumenty wartościach, a nie dokonywać dereferencji obiektu typu Agent
.
Możemy zarejestrować wiele funkcji obserwujących, a w przypadku korzystania z tego samego funkcyjnego obiektu jesteśmy w stanie rozróżniać wywołania, korzystając z przekazywanego jako argument klucza.
Wywołanie add-watch
z takim samym kluczem, jak już podany, zastępuje
poprzednią funkcję obserwującą.
Użycie:
(add-watch agent klucz funkcja)
.
add-watch
|
|
(def nasz-agent (agent 0))
(add-watch nasz-agent :debug #(println (apply str (interpose ", " %&))))
(send nasz-agent inc) ; >> :debug, [email protected], 0, 1
(send nasz-agent inc) ; >> :debug, [email protected], 1, 2
(send nasz-agent inc) ; >> :debug, [email protected], 2, 3
Usuwanie obserwatorów, remove-watch
Funkcja remove-watch
pozwala usunąć funkcję obserwującą przypisaną do obiektu
typu Agent
. Przyjmuje ona dwa obowiązkowe argumenty: obiekt referencyjny
i unikatowy klucz identyfikujący podpięcie.
Wartością zwracaną jest obiekt typu Agent
, od którego odłączono funkcję
obserwującą.
Użycie:
(remove-watch atom klucz)
.
remove-watch
|
|
(def nasz-agent (agent 0))
(defn reporter [& args] (println (first args)))
(add-watch nasz-agent :debug reporter)
(add-watch nasz-agent :inna reporter)
(send nasz-agent inc)
; >> :debug
; >> :inna
(remove-watch nasz-agent :inna)
(send nasz-agent inc)
; >> :debug
Przykłady zastosowań
Globalne tożsamości
Korzystając ze zmiennych globalnych, jesteśmy w stanie
tworzyć stałe tożsamości dla wyrażających zmienne stany (przez
odniesienia do różnych wartości) obiektów typu Agent
. W ten sposób możemy
w całym programie odwoływać się do konkretnego Agenta z użyciem nadanej mu
symbolicznej nazwy o globalnym zasięgu.
Spróbujmy zaimplementować przykładowy licznik w postaci funkcji, która za każdym wywołaniem będzie zwiększała jego wartość o jeden, a jeżeli podamy argument, dokonane zostanie przypisanie konkretnej wartości początkowej:
|
|
(defonce agent-licznika (agent -1))
(defn licznik
([x] (send agent-licznika (constantly (int x))))
([] (send agent-licznika inc)))
(licznik) ; => #<[email protected]: 0>
(licznik) ; => #<[email protected]: 0>
(licznik) ; => #<[email protected]: 2>
@agent-licznika ; => 2
(licznik 100) ; => 100
@agent-licznika ; => 100
Implementacja raportowania zdarzeń
Spróbujmy stworzyć implementację procesu zapisującego komunikaty dotyczące zdarzeń w pliku. Zakładamy, że funkcja realizująca zadanie będzie mogła być wywoływana równolegle przez wiele wątków.
|
|
(use 'clojure.java.io)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; obsługa wywołań Javy (zapisywanie do plików)
(defn- zapisz-log
"Zapisuje podany komunikat do pliku, używając obiektu typu
BufferedWriter podanego jako pierwszy argument."
[pisak komunikat]
(doto pisak (.write komunikat)))
(defn- zamknij-log
"Zamyka otwarty plik skojarzony z obiektem typu
BufferedWriter."
[pisak]
(doto pisak (.close)))
(defn- aktualny-czas
"Zwraca łańcuch znakowy reprezentujący aktualny czas."
[]
(let [data (bean (java.util.Date.))
h (:hours data)
m (:minutes data)
s (:seconds data)
dy (:year data)
dm (:month data)
dd (:day data)
r (str (+ dy 1900) "-" dm "-" dd " " h ":" m ":" s)]
r))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; obsługa raportowania
(defn otwórz-raport
"Otwiera plik o podanej nazwie. Zwraca obiekt typu
Agent, którego wartością bieżącą jest skojarzony
z otwartym plikiem obiekt typu BufferedWriter."
[nazwa-pliku]
(agent (writer nazwa-pliku :append true)))
(defn do-raportu
"Korzystając z podanego obiektu typu Agent, dodaje do
skojarzonego z nim pliku linię określoną łańcuchem
znakowym przekazanym jako drugi argument."
[agent-pliku komunikat]
(let [czas (aktualny-czas)
wpis (str czas " " komunikat)]
(send agent-pliku zapisz-log wpis)))
(defn zamknij-raport
"Zamyka plik wskazywany podanym obiektem typu Agent"
[agent-pliku]
(send agent-pliku zamknij-log))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; funkcje i wartości pomocnicze
(def log (otwórz-raport "/tmp/test-agent"))
(def raportuj (partial do-raportu log))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; logika aplikacji
(raportuj "Start\n")
(raportuj "Stop\n")
(zamknij-raport log)
(shutdown-agents)