stats

Poczytaj mi Clojure, cz. 19

Współbieżność: Wątki

Grafika

Korzystanie z dodatkowych wątków i sterowanie wykonywaniem bieżącego pozwalają precyzyjnie zarządzać współbieżnym realizowaniem zadań. W Clojure możemy w tym celu użyć dodatkowych typów referencyjnych: Future, Promise i Delay. Istnieją również odpowiednie klasy Javy realizujące podobne cele, a nawet typ Volatile, który pozwala tworzyć szybkie odpowiedniki konwencjonalnych zmiennych.

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.

Future’y

Future to mechanizm asynchronicznegorównoległego realizowania zadań, w którym wyrażenie obliczające nową wartość jest przekazywane do innego, stworzonego specjalnie na tę potrzebę wątku, a po zakończeniu kalkulacji odniesienie do rezultatu jest umieszczane w obiekcie referencyjnym współdzielonym między wszystkimi wątkami.

Future’y obsługiwane są przez referencyjny typ danych o nazwie Future. Użyjemy ich na przykład wtedy, gdy pojawi się potrzeba wykonania pewnego fragmentu programu równolegle, aby bieżący wątek mógł płynnie kontynuować działanie w trakcie realizowania jakichś czasochłonnych obliczeń bądź operacji wejścia/wyjścia.

Użytkowanie Future’ów polega na utworzeniu odpowiednich obiektów z przekazaniem im wyrażeń, których wartości zostaną obliczone w osobnym wątku. Ostatnia z wartości stanie się współdzieloną wartością bieżącą obiektu typu Future. Do ustawienia stanu dojdzie tylko raz, bez możliwości późniejszej aktualizacji referencji. Operacja tworzenia Future’a nie zablokuje bieżącego wątku.

Próba odczytu wartości bieżącej Future’a spowoduje zablokowanie wątku, w którym do niej doszło, do czasu, gdy będzie ona dostępna. Oznacza to, że jeżeli zechcemy poznać wartość współdzieloną obiektu typu Future, ale równolegle realizowana zmiana stanu jeszcze się nie zakończyła (trwają obliczenia), dereferencja będzie wiązała się z oczekiwaniem. Po zakończeniu obliczeń ich wynik zostanie zapamiętany, więc każdy kolejny odczyt spowoduje skorzystanie z niego bez blokowania.

Korzystając z Future’ów, nie możemy wybrać wątku, w którym wartościowane będą wyrażenia. Wiemy tylko, że będzie to wątek różny od bieżącego, w którym wykonany będzie podprogram. Po zakończeniu jego realizacji nie nastąpi powrót do kolejnego wyrażenia programu (prowadziło by to do rozwidlenia wykonywania i zrównoleglenia wszystkich operacji), lecz przetwarzanie zostanie zakończone, a dodatkowy wątek przeznaczony jest do zwolnienia.

Podczas tworzenia obiektu typu Future wszystkie dynamiczne powiązania widoczne w bieżącym wątku zostaną skopiowane do wątku, w którym realizowane będzie wartościowanie wyrażenia przekazanego do Future’a. Normalnie powiązania wartości bieżących zmiennych dynamicznych (reprezentowanych obiektami typu Var) są izolowane między wątkami (współdzielone jest tylko powiązanie główne), ale dzięki temu mechanizmowi wartości są przenoszone.

Zazwyczaj obiekt typu Future 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.

Obsługa błędów

W pewnych sytuacjach wartość bieżąca obiektu typu Future może nie zostać nigdy obliczona i ustawiona:

  • Gdy doszło do zawieszenia pracy podprogramu realizującego obliczenia (np. z powodu zapętlenia bądź innego błędu w algorytmie).

  • Gdy działający w osobnym wątku podprogram zgłosił wyjątek.

  • Gdy z użyciem future-cancel anulowano działanie podprogramu realizującego obliczenia.

W pierwszym z wymienionych przypadków sposobem obsługi sytuacji jest zastosowanie funkcji deref, która w wersji trójargumentowej pozwala określić maksymalny czas oczekiwania na uzyskanie wartości podczas dereferencji. Inny sposób to sprawdzanie, czy wartość bieżąca jest gotowa do odczytu (funkcja realized?), albo czy podprogram realizujący obliczenia się zakończył (funkcja future-done?). Sposobem na interwencję w przypadku błędnie działającego podprogram aktualizującego wartość Future’a może być na przykład przerwanie jego pracy z użyciem funkcji future-cancel.

W pozostałych dwóch przypadkach próby dereferencji będą powodowały zgłaszanie wyjątków. Jeżeli realizację obliczeń anulowano, będzie to java.util.concurrent.CancellationException, natomiast w przypadku awaryjnego przerwania inny wyjątek. Wyjątki te możemy przechwytywać, lub też przed próbą odczytu wartości bieżącej korzystać z funkcji, które sprawdzą, czy da się to zrobić:

  • realized? – aby sprawdzić czy już obliczono wartość i ustawiono stan,

  • future-cancelled? – aby zbadać, czy doszło do anulowania obliczeń.

Tworzenie Future’ów

Tworzenie obiektów Future, future

Do tworzenia obiektów typu Future służy makro future.

Użycie:

  • (future & wyrażenie).

Makro przyjmuje zero lub więcej argumentów – wyrażeń, które będą wartościowane w osobnym wątku ze skopiowanymi z obecnego wątku wartościami bieżącymi zmiennych dynamicznych. Wartość ostatniego z wyrażeń stanie się bieżącą wartością powiązaną z tworzonym obiektem. Do czasu zakończenia obliczeń wartość ta pozostanie nieustalona.

Zwracaną wartością jest obiekt typu Future, który po zakończeniu wartościowania przekazanych wyrażeń będzie zawierał odniesienie do wartości ostatniego z nich.

Jeżeli nie podano wyrażenia, wartością bieżącą, z którą powiązany zostanie obiekt,będzienil`.

Przykłady użycia makra future
 1(future)
 2; => clojure.core$future_call$reify__6736 0x71d198cb
 3; => {:status :ready, :val nil}
 4
 5(future 1)
 6; => clojure.core$future_call$reify__6736 0x5fdf7173
 7; => {:status :ready, :val 1}
 8
 9(future 1 2)
10; => clojure.core$future_call$reify__6736 0x2f6afefb
11; => {:status :pending, :val nil}
12
13(future (reduce +' (range 1 2000002)))
14; => clojure.core$future_call$reify__6736 0x6ab360d
15; => {:status :pending, :val nil}
16
17(future (println "x") 3)
18; >> x
19; => clojure.core$future_call$reify__6736 0x462fd484
20; => {:status :ready, :val 3}
(future) ; => clojure.core$future_call$reify__6736 0x71d198cb ; => {:status :ready, :val nil} (future 1) ; => clojure.core$future_call$reify__6736 0x5fdf7173 ; => {:status :ready, :val 1} (future 1 2) ; => clojure.core$future_call$reify__6736 0x2f6afefb ; => {:status :pending, :val nil} (future (reduce +' (range 1 2000002))) ; => clojure.core$future_call$reify__6736 0x6ab360d ; => {:status :pending, :val nil} (future (println "x") 3) ; >> x ; => clojure.core$future_call$reify__6736 0x462fd484 ; => {:status :ready, :val 3}
  • Wyrażenie z linii nr 1 tworzy obiekt referencyjny typu Future, który odnosi się do wartości nil, ponieważ nie podano wyrażenia.

  • Wyrażenie z linii nr 5 otrzymuje wyrażenie będące wartością stałą, która stanie się wartością bieżącą obiektu po powiązaniu go z nią w osobnym wątku.

  • Kolejne wyrażenie (z linii nr 9) działa podobnie, jak powyższe, ale ustawioną wartością będzie 2, ponieważ jest to ostatnie przekazane wyrażenie.

  • W przykładzie z linii nr 13 mamy przekazanie wyrażenia, które sumuje liczby z pewnego przedziału.

  • Ostatni przykład pokazuje, że makro future może generować efekty uboczne, jeżeli podane wyrażenia je powodują.

Zauważmy, że w niektórych przypadkach widzimy diagnostyczny napis :pending, a w innych :ready (będące wartościami elementu mapykluczu :status). W ten sposób reprezentowany jest status obiektu referencyjnego. Pierwsze słowo kluczowe oznacza, że obliczenia są jeszcze wykonywane, a drugie, że praca równoległego wątku zakończyła się i powiązano Future’a z wartością stanowiącą rezultat obliczeń. W symbolicznie wyrażonej mapie widzimy też klucz :val, do którego przypisana jest wartość bieżąca obiektu typu Future, jeżeli już ją obliczono.

Uwaga: Wywołania future sprawiają, że używana jest pula wątków, która nie będzie automatycznie zniszczona, gdy program zakończy pracę. Powoduje to, że proces maszyny wirtualnej nie zakończy się natychmiastowo, lecz po jednej minucie. Można to przyspieszyć, korzystając z funkcji shutdown-agents.

Tworzenie obiektów Future, future-call

Do tworzenia obiektów typu Future może służyć również funkcja future-call, z której korzysta makro future.

Użycie:

  • (future-call funkcja).

Funkcja przyjmuje jeden obowiązkowy argument, którym powinien być obiekt funkcyjny. Podprogram zawartej w nim funkcji zostanie wywołany w osobnym wątku, a zwracana wartość stanie się wartością bieżącą tworzonego obiektu typu Future. Do czasu zakończenia obliczeń wartość ta pozostanie nieustalona.

Funkcja zwraca obiekt typu Future, który po zakończeniu równoległego wywołania funkcji będzie zawierał odniesienie do zwróconej przez nią wartości.

Przykłady użycia funkcji future-call
1(future-call +)
2; => #<core$future_call$reify__6320@e019b8e: 0>
3
4(future-call #(do (println "test") 3))
5; >> test
6; => #<core$future_call$reify__6320@2c97a223: :pending>
(future-call +) ; =&gt; #&lt;core$future_call$reify__6320@e019b8e: 0&gt; (future-call #(do (println &#34;test&#34;) 3)) ; &gt;&gt; test ; =&gt; #&lt;core$future_call$reify__6320@2c97a223: :pending&gt;

Uwaga: Wywołania future sprawiają, że używana jest pula wątków, która nie będzie automatycznie zniszczona, gdy program zakończy pracę. Powoduje to, że proces maszyny wirtualnej nie zakończy się natychmiastowo, lecz po jednej minucie. Można to przyspieszyć, korzystając z funkcji shutdown-agents.

Identyfikowanie Future’ów

Future’y 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 Future globalne zmienne, jak również korzystać z innych rodzajów powiązań (np. leksykalnych).

Przykłady tworzenia powiązań z obiektami typu Future
1(def x (future 0))      ; zmienna globalna
2(let [y (future 0)] y)  ; powiązanie leksykalne
(def x (future 0)) ; zmienna globalna (let [y (future 0)] y) ; powiązanie leksykalne

Pobieranie wartości

Odczytywanie wartości Future’ów, deref

Żeby odczytać wartość wskazywaną przez Future’a, możemy użyć funkcji deref.

Użycie:

  • (deref future),
  • (deref future czas-oczekiwania-ms wartość-po-czasie).

W wariancie jednoargumentowym przyjmowanym argumentem powinien być obiekt typu Future, a zwraca wartość, do której odniesienie jest przechowywane w tym obiekcie, jeżeli została już ona obliczona. Jeżeli obliczenia prowadzące do jej uzyskania jeszcze się nie zakończyły, wątek zostanie zablokowany do chwili pojawienia się rezultatu.

Wariant trójargumentowy deref służy do odczytywania wartości bieżącej z kontrolą maksymalnego czasu oczekiwania na jej uzyskanie. Drugim argumentem powinna być wyrażona numerycznie całkowita liczba milisekund określająca czas, po którym oczekiwanie zostanie przerwane i nastąpi wyjście z funkcji. Trzecim obowiązkowym argumentem powinna być wartość, która zostanie zwrócona zamiast wartości bieżącej obiektu typu Future, jeżeli dojdzie do upłynięcia czasu oczekiwania. Gdy uda się uzyskać wartość bieżącą przed upływem podanego czasu, zostanie ona zwrócona.

Uwaga: Jeżeli podczas obliczeń przekazanych do future wyrażeń lub przekazanej do future-call funkcji dojdzie do zgłoszenia wyjątku, każda próba odczytu wartości bieżącej będzie generowała ten wyjątek.

Z wyjątkiem będziemy też mieć do czynienia, gdy wartość nie zostanie ustawiona, ponieważ anulowano obliczenia z użyciem funkcji future-cancel.

Przykład użycia funkcji deref
 1;; wersja bez kontroli czasu oczekiwania:
 2
 3(do
 4  (def x
 5    (future
 6      (println "obliczenia…")
 7      (Thread/sleep 2000)
 8      0))
 9
10  (println "wynik:" (deref x))
11  (println "ponowny odczyt:" (deref x)))
12
13; >> obliczenia…
14; >> wynik: 0
15; >> ponowny odczyt: 0
16
17;; wersja z kontrolą czasu oczekiwania:
18
19(do
20  (def x
21    (future
22      (println "obliczenia…")
23      (Thread/sleep 2000)
24      0))
25
26  (println "wynik:" (deref x 100 "przekroczony czas"))
27  (Thread/sleep 2000)  ; dajemy obliczeniom trochę czasu
28  (println "wynik:" (deref x 100 "przekroczony czas")))
29
30; >> obliczenia…
31; >> wynik: przekroczony czas
32; >> wynik: 0
;; wersja bez kontroli czasu oczekiwania: (do (def x (future (println &#34;obliczenia…&#34;) (Thread/sleep 2000) 0)) (println &#34;wynik:&#34; (deref x)) (println &#34;ponowny odczyt:&#34; (deref x))) ; &gt;&gt; obliczenia… ; &gt;&gt; wynik: 0 ; &gt;&gt; ponowny odczyt: 0 ;; wersja z kontrolą czasu oczekiwania: (do (def x (future (println &#34;obliczenia…&#34;) (Thread/sleep 2000) 0)) (println &#34;wynik:&#34; (deref x 100 &#34;przekroczony czas&#34;)) (Thread/sleep 2000) ; dajemy obliczeniom trochę czasu (println &#34;wynik:&#34; (deref x 100 &#34;przekroczony czas&#34;))) ; &gt;&gt; obliczenia… ; &gt;&gt; wynik: przekroczony czas ; &gt;&gt; wynik: 0

Dereferencja Future’ów, makro @

Makro czytnika @ (znak małpki) umieszczone przed wyrażeniem sprawia, że gdy zwracany przez nie obiekt jest typem referencyjnym, wywołana zostanie na nim funkcja odpowiedzialna za odczyt wskazywanej wartości. W przypadku Future’ów będzie to omówiona wyżej funkcja deref w jej jednoargumentowym wariancie.

Użycie:

  • @future.
Przykład dereferencji Future’a z użyciem makra czytnika @
1(def x (future 0))
2@x
3; => 0
(def x (future 0)) @x ; =&gt; 0

Uwaga: Jeżeli podczas obliczeń przekazanych do future wyrażeń lub przekazanej do future-call funkcji dojdzie do zgłoszenia wyjątku, każda próba odczytu wartości bieżącej będzie generowała ten wyjątek.

Kontrolowanie wykonywania

Anulowanie pracy, future-cancel

Funkcja future-cancel pozwala wymusić przerwanie pracy podprogramu realizującego w osobnym wątku obliczenia, których rezultat miał stać się wartością bieżącą obiektu typu Future.

Użycie:

  • (future-cancel future).

Funkcja przyjmuje jeden argument, którym powinien być obiekt typu Future, a zwraca wartość true, jeżeli dokonano anulowania przetwarzania, a false, gdy obliczenia zostały już zakończone lub anulowane i nie można przerwać pracy realizującego je podprogramu, ponieważ nie jest on wykonywany.

Funkcja future-cancel bywa przydatna tam, gdzie z powodu zapętlenia lub innych okoliczności podane przy tworzeniu Future’a wyrażenie nie może być wartościowane w rozsądnym czasie.

Aby sprawdzić, czy obliczenia zostały anulowane, można skorzystać z funkcji future-cancelled?.

Przykłady użycia funkcji future-cancel
1(def x (future (Thread/sleep 5000)))
2
3(future-cancel x)  ; => true
4(future-cancel x)  ; => false
(def x (future (Thread/sleep 5000))) (future-cancel x) ; =&gt; true (future-cancel x) ; =&gt; false

Testowanie Future’ów

Testowanie typu, future?

Funkcja future? służy do sprawdzania, czy podana wartość jest obiektem typu Future.

Użycie:

  • (future? wartość).

Pierwszym, obowiązkowym argumentem funkcji powinna być wartość, której typ zostanie sprawdzony.

Funkcja zwraca wartość true, jeżeli podana wartość jest obiektem typu Future, a false w przeciwnym razie.

Przykłady użycia funkcji future?
1(future? 1)             ; => false
2(future? (future nil))  ; => true
(future? 1) ; =&gt; false (future? (future nil)) ; =&gt; true

Sprawdzanie zakończenia, future-done?

Funkcja future-done? służy do sprawdzania, czy realizowany w osobnym wątku podprogram przeprowadzający obliczenia, które służą do uzyskania wartości bieżącej Future’a, zakończył pracę.

Użycie:

  • (future-done? future).

Pierwszym, obowiązkowym argumentem funkcji powinien być obiekt typu Future.

Funkcja zwraca wartość true, jeżeli podprogram obliczający wartość zakończył się, a false w przeciwnym razie.

Przykład użycia funkcji future-done?
 1(def x
 2  (future
 3    (Thread/sleep 1000)
 4    0))
 5
 6(future-done? x)
 7; => false
 8
 9(Thread/sleep 1000)
10(future-done? x)
11; => true
(def x (future (Thread/sleep 1000) 0)) (future-done? x) ; =&gt; false (Thread/sleep 1000) (future-done? x) ; =&gt; true

Uwaga: Wartość true zostanie zwrócona niezależnie od tego, czy realizacja obliczeń zakończyła się ustawieniem wartości bieżącej, czy też została przerwana.

Sprawdzanie uzyskania wyniku, realized?

Funkcja realized? służy do sprawdzania, czy realizowany w osobnym wątku podprogram przeprowadzający obliczenia, które służą do uzyskania wartości bieżącej Future’a, zakończył się pomyślnie i ustawiono wartość bieżącą referencji.

Użycie:

  • (realized? future).

Pierwszym, obowiązkowym argumentem funkcji powinien być obiekt typu Future.

Funkcja zwraca wartość true, jeżeli ustawiono wartość bieżącą, a false w przeciwnym razie.

Przykład użycia funkcji realized?
 1(def x
 2  (future
 3    (Thread/sleep 1000)
 4    0))
 5
 6(realized? x)
 7; => false
 8
 9(Thread/sleep 1000)
10(realized? x)
11; => true
(def x (future (Thread/sleep 1000) 0)) (realized? x) ; =&gt; false (Thread/sleep 1000) (realized? x) ; =&gt; true

Sprawdzanie anulowania, cancelled?

Funkcja future-cancelled? służy do sprawdzania, czy realizowany w osobnym wątku podprogram przeprowadzający obliczenia, które służą do uzyskania wartości bieżącej Future’a, został przerwany zanim doszło do ustawienia wartości bieżącej referencji.

Użycie:

  • (future-cancelled? future).

Pierwszym, obowiązkowym argumentem funkcji powinien być obiekt typu Future.

Funkcja zwraca wartość true, jeżeli przerwano wykonywanie podprogramu, a false w przeciwnym razie.

Przykład użycia funkcji cancelled?
1(def x (future (Thread/sleep 5000)))
2
3(future-cancelled? x)
4; => false
5
6(future-cancel x)
7(future-cancelled? x)
8; => true
(def x (future (Thread/sleep 5000))) (future-cancelled? x) ; =&gt; false (future-cancel x) (future-cancelled? x) ; =&gt; true

Promise’y

Typ Promise służy do obsługi operacji, które powinny być realizowane równolegle w ten sposób, że jeden lub więcej wątków oczekują na dostarczenie wartości, która obliczana jest w innym wątku. Można powiedzieć, że jest to mechanizm komunikacji międzywątkowej, która polega na ustawianiu wartości współdzielonej Promise’a.

Promise wyraża obietnicę dostarczenia w przyszłości jakiejś wartości. Miejscem przechowywania odwołania do tej wartości będzie właśnie obiekt typu Promise. Dowolny z wątków programu może taką wartość bieżącą dostarczyć (ang. deliver), a wtedy stanie się ona dostępna.

W przeciwieństwie do Future’ów wyrażenie, którego wartość stanie się wartością bieżącą, musi być już wcześniej obliczone. Korzystanie z Promise’ów nie tworzy automatycznie nowych wątków dokonujących kalkulacji. Ich instancje inicjowane są stałymi wartościami, przekazywanymi jako argumenty odpowiedniej funkcji, a nie wyrażeniami, które dopiero będą wartościowane.

Promise to typ referencyjny, dzięki któremu daje się wyrazić niekoordynowaną, pojedynczą zmianę współdzielonego stanu, do której już doszło lub dopiero dojdzie w dowolnym wątku. Realizowanie obliczeń prowadzących do uzyskania wartości odbędzie się w sposób asynchroniczny.

Próba odczytu wartości bieżącej obiektu typu Promise spowoduje zablokowanie bieżącego wątku do momentu jej uzyskania (dostarczenia jej przez inny wątek), natomiast zapis jest operacją nieblokującą.

Wątek oczekujący na wartość może dokonać blokującej dereferencji, natomiast wątek ustawiający ją musi dokonać obliczeń (jeżeli to konieczne), a następnie dostarczyć rezultaty do obiektu typu Promise. Operacja ta może być przeprowadzona tylko raz. Każda późniejsza dereferencja Promise’a spowoduje zwrócenie wcześniej ustawionej wartości współdzielonej.

Tworzenie

Tworzenie obiektów Promise, promise

Do tworzenia obiektów typu Promise służy funkcja promise.

Użycie:

  • (promise).

Funkcja nie przyjmuje żadnych argumentów, a zwraca obiekt typu Promise, którego wartość współdzieloną można ustawiać z wykorzystaniem funkcji deliver, a odczytywać z użyciem deref lub odpowiedniego makra czytnika.

Przykład użycia funkcji promise
1(promise)
2; => clojure.core$promise$reify__6779 0x7f5338bf
3; => {:status :pending, :val nil}
(promise) ; =&gt; clojure.core$promise$reify__6779 0x7f5338bf ; =&gt; {:status :pending, :val nil}

Powyższe wyrażenie tworzy obiekt referencyjny typu Promise, który oczekuje na ustawienie wartości współdzielonej.

Identyfikowanie Promise’ów

Promise’y 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 Promise globalne zmienne, jak również korzystać z innych rodzajów powiązań (np. leksykalnych).

Przykłady tworzenia powiązań z obiektami typu Promise
1(def x (promise))      ; zmienna globalna
2(let [y (promise)] y)  ; powiązanie leksykalne
(def x (promise)) ; zmienna globalna (let [y (promise)] y) ; powiązanie leksykalne

Dostarczanie wartości

Dostarczanie wartości Promise’ów, deliver

Korzystając z funkcji deliver możemy dostarczyć wartość do obiektu typu Promise. Sprawia ona, że wszystkie oczekujące operacje dereferencji zostaną zwolnione, a dostarczona wartość zwrócona.

Wielokrotne wywołania deliver nie będą miały żadnego wpływu na Promise’a.

Użycie:

  • (deliver promise wartość).

Pobieranie wartości

Odczytywanie wartości Promise’ów, deref

Żeby odczytać wartość wskazywaną przez Promise’a, możemy użyć funkcji deref.

Użycie:

  • (deref promise),
  • (deref promise czas-oczekiwania-ms wartość-po-czasie).

W wariancie jednoargumentowym przyjmowanym argumentem powinien być obiekt typu Promise, a zwrócona będzie wartość, do której odniesienie jest przechowywane w tym obiekcie, jeżeli została już ona dostarczona. Jeżeli nie ustawiono jeszcze wartości współdzielonej Promise’a, wątek zostanie zablokowany do chwili jej pojawienia się.

Wariant trójargumentowy deref służy do odczytywania wartości bieżącej z kontrolą maksymalnego czasu oczekiwania na jej uzyskanie. Drugim argumentem powinna być wyrażona numerycznie całkowita liczba milisekund określająca czas, po którym oczekiwanie zostanie przerwane i nastąpi wyjście z funkcji. Trzecim obowiązkowym argumentem powinna być wartość, która zostanie zwrócona zamiast wartości bieżącej obiektu typu Promise, jeżeli dojdzie do upłynięcia czasu oczekiwania. Gdy uda się uzyskać wartość bieżącą przed upływem podanego czasu, zostanie ona zwrócona.

Przykład użycia funkcji deref
 1;; wersja bez kontroli czasu oczekiwania:
 2
 3(do
 4  (def x (promise))
 5
 6  (future
 7    (println "obliczenia…")
 8    (Thread/sleep 2000)
 9    (deliver x 0))
10
11  (println "wynik:" (deref x))
12  (println "ponowny odczyt:" (deref x)))
13
14; >> obliczenia…
15; >> wynik: 0
16; >> ponowny odczyt: 0
17
18;; wersja z kontrolą czasu oczekiwania:
19
20(do
21  (def x (promise))
22
23  (println "oczekiwanie…")
24  (println "wynik:" (deref x 100 "przekroczony czas"))
25
26  (deliver x 0)  ; dostarczenie wartości
27
28  (println "oczekiwanie…")
29  (println "wynik:" (deref x 100 "przekroczony czas")))
30
31; >> oczekiwanie…
32; >> wynik: przekroczony czas
33; >> oczekiwanie…
34; >> wynik: 0
;; wersja bez kontroli czasu oczekiwania: (do (def x (promise)) (future (println &#34;obliczenia…&#34;) (Thread/sleep 2000) (deliver x 0)) (println &#34;wynik:&#34; (deref x)) (println &#34;ponowny odczyt:&#34; (deref x))) ; &gt;&gt; obliczenia… ; &gt;&gt; wynik: 0 ; &gt;&gt; ponowny odczyt: 0 ;; wersja z kontrolą czasu oczekiwania: (do (def x (promise)) (println &#34;oczekiwanie…&#34;) (println &#34;wynik:&#34; (deref x 100 &#34;przekroczony czas&#34;)) (deliver x 0) ; dostarczenie wartości (println &#34;oczekiwanie…&#34;) (println &#34;wynik:&#34; (deref x 100 &#34;przekroczony czas&#34;))) ; &gt;&gt; oczekiwanie… ; &gt;&gt; wynik: przekroczony czas ; &gt;&gt; oczekiwanie… ; &gt;&gt; wynik: 0

Dereferencja Promise’ów, makro @

Makro czytnika @ (znak małpki) umieszczone przed wyrażeniem sprawia, że gdy zwracany przez nie obiekt jest typem referencyjnym, wywołana zostanie na nim funkcja odpowiedzialna za odczyt wskazywanej wartości. W przypadku Promise’ów będzie to omówiona wyżej funkcja deref w jej jednoargumentowym wariancie.

Użycie:

  • @promise.
Przykład dereferencji Promise’a z użyciem makra czytnika @
1(def x (promise))
2
3(deliver x 0)
4@x
5; => 0
(def x (promise)) (deliver x 0) @x ; =&gt; 0

Testowanie Promise’ów

Sprawdzanie uzyskania wyniku, realized?

Funkcja realized? służy do sprawdzania, czy ustawiono współdzieloną wartość bieżącą Promise’a.

Użycie:

  • (realized? promise).

Pierwszym, obowiązkowym argumentem funkcji powinien być obiekt typu Promise.

Funkcja zwraca wartość true, jeżeli ustawiono wartość bieżącą, a false w przeciwnym razie.

Przykłady użycia funkcji realized?
1(def x (promise))
2
3(realized? x)
4; => false
5
6(deliver x 0)
7(realized? x)
8; => true
(def x (promise)) (realized? x) ; =&gt; false (deliver x 0) (realized? x) ; =&gt; true

Delay’e

Referencyjny typ Delay służy do obsługi operacji, które powinny być odłożone w czasie w celu późniejszej realizacji w wybranym wątku, dopiero wtedy, gdy rezultaty kalkulacji będą potrzebne. Możemy w ten sposób wyrażać niekoordynowane, pojedyncze zmiany współdzielonych stanów. Aktualizacja wartości współdzielonych odbywa się w sposób synchroniczny.

Podobnie jak w przypadku Future’ów, tworząc obiekty typu Delay, podajemy im wyrażenia, które nie będą natychmiast wartościowane. Różnica polega na tym, że w przypadku Delay’ów nie będzie tworzony automatycznie nowy wątek, lecz wyrażenie zostanie zapamiętane. Obliczanie jego wartości nie nastąpi od razu, ale dopiero wtedy, gdy w dowolnym wątku wykonywania pojawi się pierwsza próba dereferencji (odczytu wartości bieżącej) Delay’a. To on zostanie użyty do przeprowadzenia obliczeń, aby ją poznać.

Odczyty wartości bieżących obiektów typu Delay mogą być blokujące – pierwsze odwołania do nich sprawiają, że w bieżącym wątku (tym, w którym dokonywana jest dereferencja) rozpoczyna się wykonywanie odłożonych wcześniej kalkulacji. Z kolei zapis (aktualizacja stanu Delay’ów) jest operacją nieblokującą – podane wyrażenia są po prostu zapamiętywane w celu późniejszej ewaluacji.

Delay’ów użyjemy tam, gdzie chcemy odłożyć na później obliczenie wartości wyrażenia. Gdy już do tego dojdzie, obiekt referencyjny zostanie powiązany z uzyskaną w wyniku obliczeń stałą wartością, a kolejne funkcje dokonujące dereferencji będą ją zwracały, a nie wyliczały ją ponownie. Oznacza to, że do aktualizacji stanu Delay’a dojdzie tylko raz –w wątku, który po raz pierwszy dokonuje odczytu wartości bieżącej.

Zazwyczaj obiekt typu Delay 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.

Tworzenie Delay’ów

Tworzenie obiektów Delay, delay

Do tworzenia obiektów typu Delay służy makro delay.

Użycie:

  • (delay & wyrażenie).

Makro delay przyjmuje zero lub więcej argumentów – wyrażeń, których wartościowanie zostanie odłożone w czasie do momentu, gdy nastąpi próba odczytania współdzielonej wartości bieżącej.

Makro zwraca obiekt typu Delay, który po zakończeniu wartościowania przekazanych wyrażeń będzie zawierał odniesienie do wartości ostatniego z nich.

Jeżeli nie podano wyrażenia, wartością bieżącą, z którą powiązany zostanie obiekt będzie nil.

Przykłady użycia makra delay
 1(delay)
 2; => clojure.lang.Delay 0x4bb84a3 {:status :pending, :val nil}
 3
 4(delay 1)
 5; => clojure.lang.Delay 0x588c6ea1 {:status :pending, :val nil}
 6
 7(delay 1 2)
 8; => clojure.lang.Delay 0x2b95b0f5 {:status :pending, :val nil}
 9
10(delay (reduce +' (range 1 2000002)))
11; => clojure.lang.Delay 0x7e90b907 {:status :pending, :val nil}
12
13(delay (println "x") 3)
14; => clojure.lang.Delay 0x6e7ce842 {:status :pending, :val nil}
(delay) ; =&gt; clojure.lang.Delay 0x4bb84a3 {:status :pending, :val nil} (delay 1) ; =&gt; clojure.lang.Delay 0x588c6ea1 {:status :pending, :val nil} (delay 1 2) ; =&gt; clojure.lang.Delay 0x2b95b0f5 {:status :pending, :val nil} (delay (reduce +&#39; (range 1 2000002))) ; =&gt; clojure.lang.Delay 0x7e90b907 {:status :pending, :val nil} (delay (println &#34;x&#34;) 3) ; =&gt; clojure.lang.Delay 0x6e7ce842 {:status :pending, :val nil}

Wyrażenie z linii nr 1 tworzy obiekt referencyjny typu Delay, który odnosi się do wartości nil, ponieważ nie podano wyrażenia.

Wyrażenie z linii nr 4 otrzymuje wyrażenie będące wartością stałą, która stanie się wartością bieżącą obiektu po powiązaniu go z nią podczas pierwszej dereferencji.

Kolejne wyrażenie (z linii nr 7) działa podobnie, jak powyższe, ale ustawioną wartością będzie 2, ponieważ jest to ostatnie przekazane wyrażenie.

W przykładzie z linii nr 10 mamy przekazanie wyrażenia, które sumuje liczby z pewnego przedziału.

Ostatni przykład pokazuje, że przekazane wyrażenia mogą generować efekty uboczne.

Identyfikowanie Delay’ów

Delay’y 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 Delay globalne zmienne, jak również korzystać z innych rodzajów powiązań (np. leksykalnych).

Przykłady tworzenia powiązań z obiektami typu Delay
1(def x (delay 0))      ; zmienna globalna
2(let [y (delay 0)] y)  ; powiązanie leksykalne
(def x (delay 0)) ; zmienna globalna (let [y (delay 0)] y) ; powiązanie leksykalne

Pobieranie wartości

Odczytywanie wartości Delay’ów, deref

Żeby odczytać wartość wskazywaną przez Delay’a, możemy użyć funkcji deref.

Użycie:

  • (deref delay).

Funkcja przyjmuje jeden argument, którym powinien być obiekt typu Delay, a zwraca wartość, do której odniesienie jest przechowywane w tym obiekcie. Jeżeli wartość nie została jeszcze obliczona, wtedy w bieżącym wątku wartościowane jest wyrażenie przekazane podczas tworzenia Delay’a.

Funkcja deref w przypadku Delay’ów wywołuje force na przekazanym obiekcie.

Uwaga: Jeżeli podczas obliczeń przekazanych do delay wyrażeń dojdzie do zgłoszenia wyjątku, każda próba odczytu wartości bieżącej będzie generowała ten wyjątek.

Przykład użycia funkcji deref
 1(do
 2  (def x
 3    (delay
 4      (println "obliczenia…")
 5      (Thread/sleep 2000)
 6      0))
 7
 8  (println "wynik:" (deref x))
 9  (println "ponowny odczyt:" (deref x)))
10
11; >> obliczenia…
12; >> wynik: 0
13; >> ponowny odczyt: 0
(do (def x (delay (println &#34;obliczenia…&#34;) (Thread/sleep 2000) 0)) (println &#34;wynik:&#34; (deref x)) (println &#34;ponowny odczyt:&#34; (deref x))) ; &gt;&gt; obliczenia… ; &gt;&gt; wynik: 0 ; &gt;&gt; ponowny odczyt: 0

Dereferencja Delay’ów, makro @

Makro czytnika @ (znak małpki) umieszczone przed wyrażeniem sprawia, że gdy zwracany przez nie obiekt jest typem referencyjnym, wywołana zostanie na nim funkcja odpowiedzialna za odczyt wskazywanej wartości. W przypadku Delay’ów będzie to omówiona wyżej funkcja deref.

Użycie:

  • @delay.
Przykład dereferencji Delay’a z użyciem makra czytnika @
1(def x (delay 0))
2@x
3; => 0
(def x (delay 0)) @x ; =&gt; 0

Kontrolowanie wykonywania

Wymuszanie wartości, force

Funkcja force służy do odczytu wartości bieżącej obiektów typu Delay lub odczytu dowolnej innej wartości, którą podano (jeżeli obiekt nie jest Delay’em).

Użycie:

  • (force wartość).

Funkcja przyjmuje dowolną wartość jako pierwszy argument i jeżeli jest to obiekt typu Delay, próbuje odczytać jego współdzieloną wartość bieżącą. Gdy nie została ona jeszcze obliczona, wywołany będzie podprogram wartościujący zapamiętane wcześniej wyrażenie, a obiekt typu Delay zostanie powiązany z uzyskanym rezultatem.

Wartością zwracaną jest wartość współdzielona przekazanego obiektu typu Delay lub podana jako argument wartość, jeżeli nie mamy do czynienia z Delay’em.

Przykłady użycia funkcji force
1(force (delay (+ 2 2)))
2; => 4
3
4(force 4)
5; => 4
(force (delay (+ 2 2))) ; =&gt; 4 (force 4) ; =&gt; 4

Testowanie Delay’ów

Testowanie typu, delay?

Funkcja delay? służy do sprawdzania, czy podana wartość jest obiektem typu Delay.

Użycie:

  • (delay? wartość).

Pierwszym, obowiązkowym argumentem funkcji powinna być wartość, której typ zostanie sprawdzony.

Funkcja zwraca wartość true, jeżeli podana wartość jest obiektem typu Delay, a false w przeciwnym razie.

Przykłady użycia funkcji delay?
1(delay? 1)            ; => false
2(delay? (delay nil))  ; => true
(delay? 1) ; =&gt; false (delay? (delay nil)) ; =&gt; true

Sprawdzanie uzyskania wyniku, realized?

Funkcja realized? służy do sprawdzania, czy podprogram przeprowadzający obliczenia, które służą do uzyskania wartości bieżącej Delay’a, zakończył się pomyślnie i ustawiono wartość bieżącą referencji. Można jej użyć na w tym samym lub innym wątku, niż ten, w którym dokonywana była, jest, lub będzie dereferencja, żeby bez blokowania sprawdzić, czy rezultat już został obliczony.

Użycie:

  • (realized? delay).

Pierwszym, obowiązkowym argumentem funkcji powinien być obiekt typu Delay.

Funkcja zwraca wartość true, jeżeli ustawiono wartość bieżącą, a false w przeciwnym razie.

Przykład użycia funkcji realized?
 1(def x
 2  (delay
 3    (Thread/sleep 1000)
 4    0))
 5
 6(realized? x)
 7; => false
 8
 9(do
10  (deref x)
11  (realized? x))
12; => true
(def x (delay (Thread/sleep 1000) 0)) (realized? x) ; =&gt; false (do (deref x) (realized? x)) ; =&gt; true

Volatile’e

Volatile to mechanizm podobny do Atomów, jednak pozbawiony obsługi funkcji służących do badania poprawności wartości bieżących (walidatorów) i nie wyposażony w możliwość rejestrowania tzw. funkcji obserwujących. Jest to sposób utrzymywania lokalnego stanu zbliżony do zmiennych znanych z innych języków programowania, a wykorzystywany głównie w tzw. transduktorach.

Z uwagi na brak gwarancji, że operacje będą atomowe, nie powinno się używać Volatile’ów do zarządzania współdzielonymi stanami, ale do szybkiej wymiany informacji w obrębie pojedynczego, izolowanego wątku z opcjonalnymi odczytami wartości współdzielonych w innych wątkach.

Volatile jest typem referencyjnym i przechowuje odniesienia do wartości, które mogą być zmieniane z użyciem odpowiednich funkcji. Operacje zmian powiązań, jak również ich odczytów, korzystają z obiektów volatile Javy, które cechuje to, że ani procesor, ani kompilator JIT, nie wpłyną na kolejność ich realizowania (zrezygnują z optymalizacji, które polegałyby na takiej czynności). W obsłudze Volatile’ów wykorzystywane są m.in. pamięci podręczne i inne mechanizmy przyspieszające dostęp do zawartości RAM-u. Z tej właśnie przyczyny ich aktualizacje nie są atomowe.

Technicznie rzecz ujmując, obiekty typu Volatile zachowują się podobnie do zmiennych lokalnych, z tą różnicą, że izolowanie w wątku nie jest wymuszane. Jeżeli programista nie będzie ostrożny, może nadpisać współdzieloną wartość bieżącą i zaburzyć pracę innego wątku, który operuje na danym Volatile’u.

Gdy obiekt typu Volatile zmienia wartość bieżącą, dochodzi do podmiany (ang. swap) wartości, na którą wskazuje. Nowa wartość będzie zwykle bazowała na poprzedniej (będzie efektem zastosowania na niej jakiejś funkcji), chociaż można też powiązać obiekt referencyjny z podaną wartością stałą.

Korzystanie z Volatile’a polega na utworzeniu odpowiedniego obiektu, a następnie aktualizowaniu jego powiązań z wartościami. Zazwyczaj obiekt typu Volatile 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żliwym stanie się posługiwanie się nim z użyciem nazwy widocznej w odpowiednich obszarach programu.

Tworzenie

Tworzenie obiektów Volatile, volatile!

Do tworzenia obiektów typu Volatile służy funkcja volatile!.

Użycie:

  • (volatile! wartość-początkowa).

Funkcja przyjmuje jeden obowiązkowy argument – wartość początkową, która będzie wskazywana przez referencję – a zwraca obiekt typu Volatile.

Przykłady użycia funkcji volatile!
1(volatile!       0)  ; => #<Volatile@316f0a55: 0>
2(volatile! [1 2 3])  ; => #<Volatile@545c5f23: [1 2 3]>
(volatile! 0) ; =&gt; #&lt;Volatile@316f0a55: 0&gt; (volatile! [1 2 3]) ; =&gt; #&lt;Volatile@545c5f23: [1 2 3]&gt;

Wyrażenie z linii nr 1 tworzy obiekt referencyjny typu Volatile, który odnosi się do wartości 0.

Ostatnie wyrażenie przykładu ustawia wartość początkową na wektor stworzony z użyciem wektorowego S-wyrażenia.

Identyfikowanie Volatile’ów

Volatile’e 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 Volatile globalne zmienne, jak również korzystać z innych rodzajów powiązań (np. leksykalnych).

Przykłady tworzenia powiązań z obiektami typu Volatile
1(def nasz-volatile (volatile! 0))  ; zmienna globalna
2(let [v (volatile! 0)] v)          ; powiązanie leksykalne
(def nasz-volatile (volatile! 0)) ; zmienna globalna (let [v (volatile! 0)] v) ; powiązanie leksykalne

Pobieranie wartości

Odczytywanie wartości Volatileów, deref

Żeby odczytać wartość Volatile’a możemy użyć funkcji deref.

Użycie:

  • (deref volatile).

Funkcja przyjmuje jeden argument, którym powinien być obiekt typu Volatile, a zwraca wartość, do której odniesienie jest przechowywane w tym obiekcie.

Przykład użycia funkcji deref
1(def nasz-volatile (volatile! 0))
2(deref nasz-volatile)
3; => 0
(def nasz-volatile (volatile! 0)) (deref nasz-volatile) ; =&gt; 0

Dereferencja Volatileów, makro @

Makro czytnika @ (znak małpki) umieszczone przed wyrażeniem sprawia, że gdy zwracany przez nie obiekt jest typem referencyjnym, wtedy wywołana zostanie na nim funkcja odpowiedzialna za odczyt wskazywanej wartości. W przypadku Volatile’ów będzie to omówiona wyżej funkcja deref.

Użycie:

  • @volatile.
Przykład dereferencji Volatile’a z użyciem makra czytnika @
1(def nasz-volatile (volatile! 0))
2@nasz-volatile
3; => 0
(def nasz-volatile (volatile! 0)) @nasz-volatile ; =&gt; 0

Zmiany wartości

Aktualizowanie wartości, vswap!

Zmiana stanu obiektu typu Volatile, czyli aktualizacja odniesienia, aby wskazywało inną wartość, możliwa jest z zastosowaniem funkcji vswap!.

Użycie:

  • (vswap! volatile funkcja & argument…).

Pierwszy przyjmowany argument powinien być symbolem identyfikującym obiekt typu Volatile 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ę Volatile i na jej podstawie obliczy i zwróci nową wartość. Zostanie ona użyta do zastąpienia poprzedniego referencyjnego odniesienia w Volatile’u. Opcjonalnie możemy podać dodatkowe argumenty, które zostaną przekazane jako kolejne argumenty wywoływanej funkcji.

Wartością zwracaną przez funkcję vswap! jest nowa wartość bieżąca referencji.

Przykład użycia funkcji swap!
1(def nasz-volatile (volatile! 0))
2
3@nasz-volatile              ; => 0
4(vswap! nasz-volatile inc)  ; => 1
5@nasz-volatile              ; => 1
(def nasz-volatile (volatile! 0)) @nasz-volatile ; =&gt; 0 (vswap! nasz-volatile inc) ; =&gt; 1 @nasz-volatile ; =&gt; 1

Uwaga: Podmiana wartości nie będzie atomowa! Może zdarzyć się, że dwa wątki operujące na tym samym obiekcie typu Volatile zaktualizują jego wartość bieżącą, lecz później uruchomiony użyje wartości, która została odczytana zanim drugi wątek ją zmienił.

Ustawianie wartości, vreset!

Funkcja vreset! pozwala podmienić obiekt, do którego odnosi się Volatile, ale bez obliczania nowej wartości na bazie aktualnie powiązanej z Volatile’em.

Użycie:

  • (vreset! volatile wartość).

Funkcja przyjmuje dwa argumenty: obiekt typu Volatile i nową wartość.

Wartością zwracaną jest nowa wartość bieżąca referencji.

Przykład użycia funkcji vreset!
1(def nasz-volatile (volatile! 0))
2@nasz-volatile               ; => 0
3(vreset! nasz-volatile "A")  ; => "A"
4@nasz-volatile               ; => "A"
(def nasz-volatile (volatile! 0)) @nasz-volatile ; =&gt; 0 (vreset! nasz-volatile &#34;A&#34;) ; =&gt; &#34;A&#34; @nasz-volatile ; =&gt; &#34;A&#34;

Inne konstrukcje

W celu efektywniejszego realizowania współbieżnych obliczeń możemy skorzystać z dodatkowych funkcji i makr, niezwiązanych z konkretnymi typami referencyjnymi, lecz zakorzenionymi w obsłudze wielowątkowości systemu gospodarza.

Blokowanie, locking

Makro locking umożliwia zablokowanie wartościowania podanych wyrażeń do momentu uzyskania wyłącznego dostępu do blokady skojarzonej z obiektem podanym jako jego pierwszy argument. Jest to odpowiednik słowa kluczowego synchronized z Javy.

Użycie:

  • (locking obiekt & wyrażenie…).

Poza obiektem, z którym skojarzona zostanie blokada, makro przyjmuje wyrażenia, których ewaluacja zostanie wstrzymana do momentu uzyskania wyłączności blokady.

Wartością zwracaną jest ostatnia wartość przeliczanych wyrażeń.

Przykład użycia makra locking
 1(def x)
 2
 3(future (locking x
 4          (println "Pierwszy startuje..")
 5          (Thread/sleep 4000)
 6          (println "Pierwszy gotowy!")))
 7
 8(Thread/sleep 1250)
 9
10(locking x (println "Drugi startuje..") (println "Drugi gotowy!"))
11
12; >> Pierwszy startuje..
13; >> Pierwszy gotowy!
14; >> Drugi startuje..
15; >> Drugi gotowy!
(def x) (future (locking x (println &#34;Pierwszy startuje..&#34;) (Thread/sleep 4000) (println &#34;Pierwszy gotowy!&#34;))) (Thread/sleep 1250) (locking x (println &#34;Drugi startuje..&#34;) (println &#34;Drugi gotowy!&#34;)) ; &gt;&gt; Pierwszy startuje.. ; &gt;&gt; Pierwszy gotowy! ; &gt;&gt; Drugi startuje.. ; &gt;&gt; Drugi gotowy!

W powyższym przykładzie równoległy wątek działał przez około 4 sekundy, a w tym czasie (po ponad sekundzie) w bieżącym wątku uruchomiliśmy drugi zestaw wyrażeń, który oczekiwał na zakończenie poprzedniego.

Jesteś w sekcji Poczytaj mi Clojure
Tematyka:

Taksonomie: