stats

Poczytaj mi Clojure, cz. 17

Współbieżność: Agenty

Grafika

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, niekoordynowanewspół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:

  1. Tworzony jest obiekt typu Agent.
  2. 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ść.
  3. 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.
  4. 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.
  5. W wątku wywoływana jest funkcja wyliczająca nową wartość na bazie bieżącej, a kod z jej ciała jest wykonywany.
  6. 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ą.
  7. 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.

Przykład użycia funkcji agent
1
2
3
4
5
(agent  0                     )  ; => #<Agent@7511f190: 0>
(agent  0         :meta {:a 1})  ; => #<Agent@751b6338: 0>
(agent                 [1 2 3])  ; => #<Agent@752e4307: [1 2 3]>
(agent 10 :validator #(> %1 1))  ; => #<Agent@753cb49e: 10>
(agent  0 :validator #(> %1 1))  ; => java.lang.IllegalStateException
(agent 0 ) ; =&gt; #&lt;Agent@7511f190: 0&gt; (agent 0 :meta {:a 1}) ; =&gt; #&lt;Agent@751b6338: 0&gt; (agent [1 2 3]) ; =&gt; #&lt;Agent@752e4307: [1 2 3]&gt; (agent 10 :validator #(&gt; %1 1)) ; =&gt; #&lt;Agent@753cb49e: 10&gt; (agent 0 :validator #(&gt; %1 1)) ; =&gt; 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, 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).

Przykłady tworzenia powiązań z obiektami typu Agent
1
2
(def nasz-agent (agent 0))         ; zmienna globalna
(let [agencik (agent 0)] agencik)  ; powiązanie leksykalne
(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).
Przykład użycia funkcji deref
1
2
3
(def nasz-agent (agent 0))
(deref nasz-agent)
; => 0
(def nasz-agent (agent 0)) (deref nasz-agent) ; =&gt; 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, 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.

Przykład dereferencji Agenta z użyciem makra czytnika @
1
2
3
(def nasz-agent (agent 0))
@nasz-agent
; => 0
(def nasz-agent (agent 0)) @nasz-agent ; =&gt; 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 (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…).

Przykład użycia funkcji send
1
2
3
4
5
(def nasz-agent (agent 0))

@nasz-agent            ; => 0
(send nasz-agent inc)  ; => #<Agent@74c33910: 0>
@nasz-agent            ; => 1
(def nasz-agent (agent 0)) @nasz-agent ; =&gt; 0 (send nasz-agent inc) ; =&gt; #&lt;Agent@74c33910: 0&gt; @nasz-agent ; =&gt; 1

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…).

Przykład użycia funkcji send-off
1
2
3
4
5
(def nasz-agent (agent 0))

@nasz-agent                ; => 0
(send-off nasz-agent inc)  ; => #<Agent@74c33910: 0>
@nasz-agent                ; => 1
(def nasz-agent (agent 0)) @nasz-agent ; =&gt; 0 (send-off nasz-agent inc) ; =&gt; #&lt;Agent@74c33910: 0&gt; @nasz-agent ; =&gt; 1

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…).
Przykład użycia funkcji send-via
1
2
3
4
5
6
7
8
(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)  ; => #<Agent@3116bb9b: 0>
@nasz-agent                           ; => 1
(import java.util.concurrent.Executors) (def nasza-pula (Executors/newFixedThreadPool 32)) (def nasz-agent (agent 0)) @nasz-agent ; =&gt; 0 (send-via nasza-pula nasz-agent inc) ; =&gt; #&lt;Agent@3116bb9b: 0&gt; @nasz-agent ; =&gt; 1

Zobacz także:

Walidatory

Agenty można opcjonalnie wyposażyć w mechanizmy testujące poprawność ustawianych wartości bieżących. Służą do tego funkcje set-validator!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!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.

Przykład użycia funkcji set-validator!
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
(def nasz-agent (agent 1))
(set-validator! nasz-agent pos?)   ; akceptujemy tylko liczby dodatnie

(send nasz-agent (constantly 2))   ; #<Agent@3db7416a: 1>
@nasz-agent                        ; => 2

(send nasz-agent (constantly -1))  ; #<Agent@3db7416a 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
(def nasz-agent (agent 1)) (set-validator! nasz-agent pos?) ; akceptujemy tylko liczby dodatnie (send nasz-agent (constantly 2)) ; #&lt;Agent@3db7416a: 1&gt; @nasz-agent ; =&gt; 2 (send nasz-agent (constantly -1)) ; #&lt;Agent@3db7416a FAILED: 2&gt; @nasz-agent ; =&gt; 2 (send nasz-agent (constantly 3)) ; &gt;&gt; java.lang.RuntimeException: Agent is failed, needs restart ; &gt;&gt; java.lang.IllegalStateException: Invalid reference state @nasz-agent ; =&gt; 2 (restart-agent nasz-agent 4) ; =&gt; 4 @nasz-agent ; =&gt; 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).
Przykład użycia funkcji get-validator
1
2
3
4
5
6
7
8
(def nasz-agent (agent 0))
(def nasz-walidator #(< % 5))

(set-validator! nasz-agent nasz-walidator)
; => nil

(get-validator nasz-agent)
; => #<user$nasz_walidator user$nasz_walidator@533f2bec>
(def nasz-agent (agent 0)) (def nasz-walidator #(&lt; % 5)) (set-validator! nasz-agent nasz-walidator) ; =&gt; nil (get-validator nasz-agent) ; =&gt; #&lt;user$nasz_walidator user$nasz_walidator@533f2bec&gt;

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

Przykład użycia funkcji agent-error
1
2
3
4
5
6
7
(def nasz-agent (agent 10 :validator pos?))

(send nasz-agent (constantly -1))
; => #<Agent@6442aec9 FAILED: 10>

(agent-error nasz-agent)
; >> #<IllegalStateException java.lang.IllegalStateException: Invalid reference state>
(def nasz-agent (agent 10 :validator pos?)) (send nasz-agent (constantly -1)) ; =&gt; #&lt;Agent@6442aec9 FAILED: 10&gt; (agent-error nasz-agent) ; &gt;&gt; #&lt;IllegalStateException java.lang.IllegalStateException: Invalid reference state&gt;

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.

Przykład użycia funkcji restart-agent
1
2
3
4
5
6
7
8
(def nasz-agent (agent 10 :validator pos?))

@nasz-agent                        ; => 10
(send nasz-agent (constantly -1))  ; => #<Agent@4cde17e5 FAILED: 10>
@nasz-agent                        ; => 10

(restart-agent nasz-agent 5)       ; => 5
@nasz-agent                        ; => 5
(def nasz-agent (agent 10 :validator pos?)) @nasz-agent ; =&gt; 10 (send nasz-agent (constantly -1)) ; =&gt; #&lt;Agent@4cde17e5 FAILED: 10&gt; @nasz-agent ; =&gt; 10 (restart-agent nasz-agent 5) ; =&gt; 5 @nasz-agent ; =&gt; 5

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.

Przykład użycia funkcji set-error-handler!
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
(def nasz-agent (agent 10 :validator pos?))
(set-error-handler! nasz-agent
                    #(println (apply str (interpose ",\n" %&))))

(send nasz-agent (constantly -1))
; => #<Agent@33c1da84: 10>
; >> clojure.lang.Agent@33c1da84,
; >> java.lang.IllegalStateException: Invalid reference state

@nasz-agent
; => 10

(restart-agent nasz-agent 4)
; => 4
(def nasz-agent (agent 10 :validator pos?)) (set-error-handler! nasz-agent #(println (apply str (interpose &#34;,\n&#34; %&amp;)))) (send nasz-agent (constantly -1)) ; =&gt; #&lt;Agent@33c1da84: 10&gt; ; &gt;&gt; clojure.lang.Agent@33c1da84, ; &gt;&gt; java.lang.IllegalStateException: Invalid reference state @nasz-agent ; =&gt; 10 (restart-agent nasz-agent 4) ; =&gt; 4

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

Przykład użycia funkcji error-handler
1
2
3
4
5
6
7
8
(def nasz-agent (agent 0))

(error-handler nasz-agent)
; => nil

(set-error-handler! nasz-agent identity)
(error-handler nasz-agent)
; => #<core$identity clojure.core$identity@3a8d2a11>
(def nasz-agent (agent 0)) (error-handler nasz-agent) ; =&gt; nil (set-error-handler! nasz-agent identity) (error-handler nasz-agent) ; =&gt; #&lt;core$identity clojure.core$identity@3a8d2a11&gt;

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.

Przykład użycia funkcji set-error-mode!
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
(def nasz-agent (agent 10 :validator pos?))
(set-error-mode! nasz-agent :continue)

(send nasz-agent (constantly -1))
; => #<Agent@33c1da84: 10>
; >> clojure.lang.Agent@33c1da84,
; >> java.lang.IllegalStateException: Invalid reference state

@nasz-agent
; => 10

(send nasz-agent (constantly 4))
@nasz-agent
; => 4
(def nasz-agent (agent 10 :validator pos?)) (set-error-mode! nasz-agent :continue) (send nasz-agent (constantly -1)) ; =&gt; #&lt;Agent@33c1da84: 10&gt; ; &gt;&gt; clojure.lang.Agent@33c1da84, ; &gt;&gt; java.lang.IllegalStateException: Invalid reference state @nasz-agent ; =&gt; 10 (send nasz-agent (constantly 4)) @nasz-agent ; =&gt; 4

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).
Przykład użycia funkcji error-mode
1
2
3
4
5
6
7
8
(def nasz-agent (agent 0))

(error-mode nasz-agent)
; => :fail

(set-error-mode! nasz-agent :continue)
(error-mode nasz-agent)
; => :continue
(def nasz-agent (agent 0)) (error-mode nasz-agent) ; =&gt; :fail (set-error-mode! nasz-agent :continue) (error-mode nasz-agent) ; =&gt; :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, 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).

Przykład użycia funkcji add-watch
1
2
3
4
5
6
(def nasz-agent (agent 0))
(add-watch nasz-agent :debug #(println (apply str (interpose ", " %&))))

(send nasz-agent inc)  ; >> :debug, clojure.lang.Agent@121b121a, 0, 1
(send nasz-agent inc)  ; >> :debug, clojure.lang.Agent@121b121a, 1, 2
(send nasz-agent inc)  ; >> :debug, clojure.lang.Agent@121b121a, 2, 3
(def nasz-agent (agent 0)) (add-watch nasz-agent :debug #(println (apply str (interpose &#34;, &#34; %&amp;)))) (send nasz-agent inc) ; &gt;&gt; :debug, clojure.lang.Agent@121b121a, 0, 1 (send nasz-agent inc) ; &gt;&gt; :debug, clojure.lang.Agent@121b121a, 1, 2 (send nasz-agent inc) ; &gt;&gt; :debug, clojure.lang.Agent@121b121a, 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).
Przykład użycia funkcji remove-watch
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
(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
(def nasz-agent (agent 0)) (defn reporter [&amp; args] (println (first args))) (add-watch nasz-agent :debug reporter) (add-watch nasz-agent :inna reporter) (send nasz-agent inc) ; &gt;&gt; :debug ; &gt;&gt; :inna (remove-watch nasz-agent :inna) (send nasz-agent inc) ; &gt;&gt; :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:

Przykład globalnego licznika i wieloczłonowej funkcji obsługującej
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
(defonce agent-licznika (agent -1))

(defn licznik
  ([x] (send agent-licznika (constantly (int x))))
  ([]  (send agent-licznika inc)))

(licznik)        ; => #<Agent@736f657b: 0>
(licznik)        ; => #<Agent@736f657b: 0>
(licznik)        ; => #<Agent@736f657b: 2>
@agent-licznika  ; => 2

(licznik 100)    ; => 100
@agent-licznika  ; => 100
(defonce agent-licznika (agent -1)) (defn licznik ([x] (send agent-licznika (constantly (int x)))) ([] (send agent-licznika inc))) (licznik) ; =&gt; #&lt;Agent@736f657b: 0&gt; (licznik) ; =&gt; #&lt;Agent@736f657b: 0&gt; (licznik) ; =&gt; #&lt;Agent@736f657b: 2&gt; @agent-licznika ; =&gt; 2 (licznik 100) ; =&gt; 100 @agent-licznika ; =&gt; 100

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
(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."
  [^java.io.BufferedWriter pisak ^String komunikat]
  (doto pisak (.write komunikat)))

(defn- zamknij-log
  "Zamyka otwarty plik skojarzony z obiektem typu
  BufferedWriter."
  [^java.io.BufferedWriter pisak]
  (.close pisak))

(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)]
    (str (+ dy 1900) "-" dm "-" dd " " h ":" m ":" s)))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; 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)
(use &#39;clojure.java.io) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; obsługa wywołań Javy (zapisywanie do plików) (defn- zapisz-log &#34;Zapisuje podany komunikat do pliku, używając obiektu typu BufferedWriter podanego jako pierwszy argument.&#34; [^java.io.BufferedWriter pisak ^String komunikat] (doto pisak (.write komunikat))) (defn- zamknij-log &#34;Zamyka otwarty plik skojarzony z obiektem typu BufferedWriter.&#34; [^java.io.BufferedWriter pisak] (.close pisak)) (defn- aktualny-czas &#34;Zwraca łańcuch znakowy reprezentujący aktualny czas.&#34; [] (let [data (bean (java.util.Date.)) h (:hours data) m (:minutes data) s (:seconds data) dy (:year data) dm (:month data) dd (:day data)] (str (+ dy 1900) &#34;-&#34; dm &#34;-&#34; dd &#34; &#34; h &#34;:&#34; m &#34;:&#34; s))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; obsługa raportowania (defn otwórz-raport &#34;Otwiera plik o podanej nazwie. Zwraca obiekt typu Agent, którego wartością bieżącą jest skojarzony z otwartym plikiem obiekt typu BufferedWriter.&#34; [nazwa-pliku] (agent (writer nazwa-pliku :append true))) (defn do-raportu &#34;Korzystając z podanego obiektu typu Agent, dodaje do skojarzonego z nim pliku linię określoną łańcuchem znakowym przekazanym jako drugi argument.&#34; [agent-pliku komunikat] (let [czas (aktualny-czas) wpis (str czas &#34; &#34; komunikat)] (send agent-pliku zapisz-log wpis))) (defn zamknij-raport &#34;Zamyka plik wskazywany podanym obiektem typu Agent&#34; [agent-pliku] (send agent-pliku zamknij-log)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; funkcje i wartości pomocnicze (def log (otwórz-raport &#34;/tmp/test-agent&#34;)) (def raportuj (partial do-raportu log)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; logika aplikacji (raportuj &#34;Start\n&#34;) (raportuj &#34;Stop\n&#34;) (zamknij-raport log) (shutdown-agents)
Jesteś w sekcji .
Tematyka:

Taksonomie: