Agent to typ referencyjny podobny do Atomu. Pozwala wyrażać częste, niekoordynowane zmiany stanów współdzielonych tożsamości w asynchroniczny sposób. Wykorzystywany bywa na przykład do obsługi niezależnych zdarzeń bądź jednokierunkowej komunikacji między komponentami realizującymi dostęp do pewnego zasobu, jak np. równoległe pobieranie danych z sieci, zapis do 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 15.
Agenty
Agent to typ danych języka Clojure, dzięki któremu można wyrażać częste, niekoordynowane i współdzielone zmiany stanów ustalonej tożsamości.
Współdzielenie oznacza, że żądanie dostępu do wartości powiązanej z Agentem realizowane może być w tym samym czasie przez więcej niż jeden uruchomiony wątek, natomiast asynchroniczność, że wątek, który zapoczątkował operację odczytu bądź zmiany tej wartości nie będzie blokował innym wątkom dostępu i powodował oczekiwania na rezultat.
Widzimy więc, że w przeciwieństwie do Atomu aktualizacja odniesienia do wartości w Agencie nie blokuje wykonywania się programu w bieżącym wątku, lecz jest zleceniem, które trafia do odpowiedniego bufora, aby w innym wątku oczekiwać na realizację.
Nie będziemy więc mieli do czynienia z sytuacją automatycznego ponawiania wywołań funkcji obliczającej nową wartość, gdy wiele wątków stara się zmienić stan referencji w podobnym czasie i konkuruje o pierwszeństwo, ponieważ z powodu szeregowania zleceń w kolejce taka sytuacja nigdy się nie zdarzy.
Operacja zmiany stanu Agenta jest nieblokująca dla bieżącego wątku, ale w związku z tym konieczne może być przypisanie do niego dodatkowych funkcji, które będą sprawdzały poprawność nowej wartości bądź obsługiwały usterki.
Agenty wykorzystać możemy tam, gdzie zależy nam na zmianie stanu współdzielonego obiektu, lecz nie zależy nam na tym, aby natychmiast dowiedzieć się, czy zmiana nastąpiła i jaki jest jej efekt, gdyż ważniejsza jest dalsza, niezakłócona praca podprogramu realizowanego w bieżącym wątku.
Zastosowania Agentów to zazwyczaj 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, na przykład 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, więc wewnętrznie przechowuje odniesienia
do umieszczonych w pamięci wartości. Odniesienia te mogą być zmieniane z użyciem
odpowiednich funkcji. Operacja zmiany powiązania Agenta z wartością zachodzi w sposób
atomowy – może w całości udać się lub nie. To znaczy, że nie wystąpi sytuacja
częściowej modyfikacji jakiejś pamięciowej struktury lub modyfikacji, która przesłoni
wartość z powodu zmiany dokonanej w tym samym czasie przez inny 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ć kolejności
w jakiej będą następowały.
Obiektowi typu Agent
można zlecić zmianę stanu (tzn. powiązania z wartością
bieżącą) przez przekazanie mu funkcji, która otrzyma aktualną, a zwróci nową
wartość. W odpowiednim czasie funkcja ta zostanie wywołana i dojdzie do podmiany
(ang. swap) wartości bieżącej. Możemy monitorować Agenta: sprawdzać, czy zmiana (już)
się dokonała i/lub odczytywać jego bieżący stan, badając czy aby nie doszło do
sytuacji wyjątkowej.
Korzystanie z Agentów polega na utworzeniu odpowiedniego obiektu, a następnie
aktualizowaniu jego wartości bieżącej. 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ń. W miarę jak pojawiają się kolejne zlecenia pewnych czynności, wątki są z tego zbioru wyłączane i delegowane do ich obsługi. Kiedy wątek kończy pracę, powraca do puli i oczekuje na kolejne zlecenie.
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, a kod z jej ciała jest wykonywany.
- Jeżeli nie wystąpił błąd, a nowa wartość jest akceptowalna, dochodzi do atomowej
podmiany referencji wewnątrz obiektu typu
Agent
i zostaje on powiązany z nową wartością. - Wątek przestaje realizować zadanie zmiany stanu i będąc oznaczonym jako wolny powraca 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, globalnych 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 i pamięci operacyjnej. 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, 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. W przypadku puli rozszerzalnej ogranicza nas dostępna pamięć. 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 jedna minuta). Dodatkowo Clojure korzysta ze 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, wtedy 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
.
Agenty a STM
Obsługa Agentów w Clojure zintegrowana jest z programową pamięcią transakcyjną (ang. Software Transactional Memory, skr. STM), która omówiona będzie w dalszej części.
Jeżeli zlecenie podmiany wartości bieżącej pojawia się wewnątrz transakcji, operacja taka jest wstrzymywana do czasu jej zatwierdzenia, a anulowana, gdy transakcja została przerwana. Jeżeli z jakichś powodów transakcja musi być ponowiona, aktualizacja Agenta nie zostanie wykonana. Stanie się to dopiero, gdy transakcja dojdzie do skutku. Należy jednak pamiętać, że zmiany wartości bieżących Agentów nie będą realizowane wewnątrz transakcji, a dopiero po jej zakończeniu.
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 –a także
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 przyjmować jeden argument
i nie generować efektów ubocznych. Będzie ona wywoływana za każdym razem, gdy
zażądano zmiany stanu obiektu typu Agent
i przez argument przekazywana jej będzie
proponowana, nowa wartość. Funkcja powinna zwracać false
lub zgłaszać wyjątek, gdy
nowa wartość jest nieakceptowalna. Operację taką nazywamy walidacją, a wyrażającą
ją funkcję walidatorem.
Pierwsze wywołanie walidatora będzie testowało wartość początkową tworzonego Agenta
i natychmiastowo zgłaszało wyjątek (w bieżącym, wywołującym wątku). Kolejne
wywołanie, które spowoduje zwrócenie przez walidator wartości false
lub zgłoszenie
wyjątku, nie przerwie wykonywania podprogramu bieżącego wątku, a jedynie zmieni
wewnętrzną flagę Agenta, która informuje o tym, że doświadczył on usterki. Od tej
chwili każda kolejna próba zmiany stanu – niezależnie od tego, czy wartość byłaby
poprawna, czy też nie – sprawi, że zgłaszany będzie wyjątek. To zachowanie można
zmienić korzystając z odpowiednich opcji, m.in. rejestrując dodatkową funkcję, której
zadaniem jest obsługa sytuacji wyjątkowych.
Wspomnianą funkcję, która będzie służyła do obsługi wyjątków, 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
z wartością :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ż wspomnianą funkcję obsługującą usterki (z użyciem
:error-handler
), domyślnym sposobem reakcji na sytuację wyjątkową będzie
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 go jako wadliwego
i zablokowanie dalszej realizacji zleceń zmiany wartości bieżącej (odpowiednik
:fail
) do momentu wywołania funkcji restart-agent
. Istniejące
już w kolejce zlecenia, będą oczekiwały na walidację i realizację, gdy tylko Agent
będzie gotów do ponownego ich przetwarzania.
Zarówno opcja :validator
, jak i :error-handler
, może mieć przypisaną wartość
nil
. Jest to równoznaczne z niepodawaniem funkcji.
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, 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).
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)
.
Dereferencja Agentów, makro @
Makro czytnika @
(znak małpki) umieszczone przed wyrażeniem sprawia, że jeżeli
zwracany przez nie obiekt jest typem referencyjnym, 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
.
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 (w standardowych ustawieniach).
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 (zgłaszając wyjątek), jednak te, które już znajdują się w kolejce, będą oczekiwać na realizację do momentu, gdy przywrócony zostanie poprawny stan. Warto pamiętać, że realizacja będzie wiązała się z walidacją i odwieszone zlecenie może znów spowodować oznaczenie Agenta jako uszkodzonego.
Można zmienić 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…)
.
Długie 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 bądź 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 (zgłaszając wyjątek), jednak te, które już znajdują się w kolejce, będą oczekiwać na realizację do momentu, gdy przywrócony zostanie poprawny stan. Warto pamiętać, że realizacja będzie wiązała się z walidacją i odwieszone zlecenie może znów spowodować oznaczenie Agenta jako uszkodzonego.
Można zmienić 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…)
.
Aktualizowanie 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 (zgłaszając wyjątek), jednak te, które już znajdują się w kolejce, będą oczekiwać na realizację do momentu, gdy przywrócony zostanie poprawny stan. Warto pamiętać, że realizacja będzie wiązała się z walidacją i odwieszone zlecenie może znów spowodować oznaczenie Agenta jako uszkodzonego.
Można zmienić 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…)
.
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 wartość ta jest niedopuszczalna, funkcja powinna zwrócić 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 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, a te, które już znajdują się w kolejce oczekujących, pozostaną w niej do momentu naprawy stanu.
Wyjątkiem od powyższej zasady jest natychmiastowa obsługa błędów przez
zarejestrowanie specjalnej funkcji: 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
, walidator zostanie odłączony
od podanego obiektu typu Agent
.
Funkcja set-validator!
zwraca wartość nil
, jeżeli udało się ustawić walidatora.
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)
.
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, ale też później, wywołując set-validator!
.
Gdy Agent zostanie oznaczony jako wadliwy przestaje przyjmować nowe zlecenia zmiany stanu, jednak te, które już oczekują w kolejce, pozostaną w niej do momentu oznaczenia Agenta jako działającego poprawnie.
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. Gdy wyjątek jest efektem zwrócenia przez walidator wartości false
, będzie
on obiektem typu IllegalStateException
.
Zachowanie Agenta w kwestii obsługi sytuacji wyjątkowych można zmieniać, rejestrując
własną funkcję, która będzie 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ść włączenia specjalnej opcji Agenta, która sprawia, że
w przypadku wystąpienia usterki bądź 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, bądź też ustawiono
ignorowanie takich sytuacji z użyciem set-error-mode!
.
Użycie:
(agent-error agent)
.
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 wadliwego nie zostanie usunięte.
Nawet jeżeli z obiektem Agenta skojarzono podpięcia w postaci funkcji obserwujących,
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.
Warto pamiętać, że restart Agenta sprawi, iż oczekujące w kolejce zlecenia zaczną
znowu być realizowane, ale wartość bieżąca, na której będą operowały, pierwotnie
zmieni się na ustawioną podczas wywołania restart-agent
.
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
), 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,
również tych, które nie przejdą walidacji.
Obsługa błędów, 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.
Odczyt, 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)
.
Tryb 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.
Odczyt, 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)
.
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, czyli wartość bieżąca. 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)
.
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)
.
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:
Implementacja raportowania zdarzeń
Spróbujmy stworzyć przykładową implementację procesu zapisującego w pliku komunikaty dotyczące zdarzeń. Zakładamy, że funkcja realizująca zadanie będzie mogła być wywoływana równolegle przez wiele wątków.