Poczytaj mi Clojure, cz. 12D: Współbieżność – Wątki

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 choć niebezpieczne odpowiedniki 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 12.

Future’y

Future to mechanizm asynchronicznegorównoległego realizowania zadań, w którym wyrażenie w celu obliczenia jego wartości jest przekazywane do innego, stworzonego specjalnie na tę potrzebę wątku, a po uzyskaniu wartości odniesienie do niej 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ł 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śli 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, lecz przetwarzanie zostanie zakończone, a dodatkowy wątek przeznaczony do zwolnienia.

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?). Reakcją na błędnie działający podprogram 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. Możemy je przechwytywać, albo przed próbą odczytu wartości bieżącej korzystać z funkcji, które sprawdzą, czy da się to zrobić: realized? – aby sprawdzić czy ustawiono stan, a future-cancelled? – aby zbadać, czy doszło do anulowania obliczeń.

Tworzenie Future’ów

Tworzenie obiektów typu 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, a wartość ostatniego z nich 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ędzie nil.

Przykłady użycia makra future
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(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 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ą w osobnym wątku.

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 makro future może generować efekty uboczne, jeśli 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 mapy o kluczu :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śli 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 typu 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
2
3
4
5
6
(future-call +)
; => #<[email protected]: 0>

(future-call #(do (println "test") 3))
; >> test
; => #<[email protected]: :pending>

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
2
(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śli 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
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
;; wersja bez kontroli czasu oczekiwania:

(do
  (def x
    (future
      (println "obliczenia…")
      (Thread/sleep 2000)
      0))

  (println "wynik:" (deref x))
  (println "ponowny odczyt:" (deref x)))

; >> obliczenia…
; >> wynik: 0
; >> ponowny odczyt: 0

;; wersja z kontrolą czasu oczekiwania:

(do
  (def x
    (future
      (println "obliczenia…")
      (Thread/sleep 2000)
      0))

  (println "wynik:" (deref x 100 "przekroczony czas"))
  (Thread/sleep 2000)  ; dajemy obliczeniom trochę czasu
  (println "wynik:" (deref x 100 "przekroczony czas")))

; >> obliczenia…
; >> wynik: przekroczony czas
; >> wynik: 0

Dereferencja Future’ów, makro @

Makro czytnika @ (znak małpki) umieszczone przed wyrażeniem sprawia, że jeśli 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
2
3
(def x (future 0))
@x
; => 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
2
3
4
(def x (future (Thread/sleep 5000)))

(future-cancel x)  ; => true
(future-cancel x)  ; => 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
2
(future? 1)             ; => false
(future? (future nil))  ; => true

Sprawdzanie zakończenia pracy, 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
2
3
4
5
6
7
8
9
10
11
(def x
  (future
    (Thread/sleep 1000)
    0))

(future-done? x)
; => false

(Thread/sleep 1000)
(future-done? x)
; => 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
2
3
4
5
6
7
8
9
10
11
(def x
  (future
    (Thread/sleep 1000)
    0))

(realized? x)
; => false

(Thread/sleep 1000)
(realized? x)
; => true

Sprawdzanie anulowania pracy, 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
2
3
4
5
6
7
8
(def x (future (Thread/sleep 5000)))

(future-cancelled? x)
; => false

(future-cancel x)
(future-cancelled? x)
; => 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.

W przeciwieństwie do Future’ów wyrażenie, którego wartość stanie się wartością bieżącą obiektu, musi być już wcześniej obliczone w wybranym przez programistę wątku. Korzystanie z Promise’ów nie tworzy automatycznie nowych wątków, a ich obiekty inicjowane są przekazywanymi jako argumenty odpowiedniej funkcji stałymi wartościami, 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, zaś 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, natomiast zapis jest operacją nieblokującą.

Wątek oczekujący na wartość może dokonać blokującej dereferencji, natomiast wątek realizujący obliczenia tej wartości musi użyć funkcji dokonującej powiązania z nią obiektu typu Promise. Operacja ta może być przeprowadzona tylko raz.

Każda kolejna (poza pierwszą) dereferencja Promise’a spowoduje zwrócenie wcześniej ustawionej wartości współdzielonej.

Tworzenie

Tworzenie obiektów typu 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
2
(promise)
; => clojure.core$promise$reify__6779 0x7f5338bf {: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
2
(def x (promise))      ; zmienna globalna
(let [y (promise)] y)  ; powiązanie leksykalne

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 zwraca wartość, do której odniesienie jest przechowywane w tym obiekcie, jeżeli została już ona dostarczona. Jeśli 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
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
;; wersja bez kontroli czasu oczekiwania:

(do
  (def x (promise))

  (future
    (println "obliczenia…")
    (Thread/sleep 2000)
    (deliver x 0))

  (println "wynik:" (deref x))
  (println "ponowny odczyt:" (deref x)))

; >> obliczenia…
; >> wynik: 0
; >> ponowny odczyt: 0

;; wersja z kontrolą czasu oczekiwania:

(do
  (def x (promise))

  (println "oczekiwanie…")
  (println "wynik:" (deref x 100 "przekroczony czas"))

  (deliver x 0)  ; dostarczenie wartości

  (println "oczekiwanie…")
  (println "wynik:" (deref x 100 "przekroczony czas")))

; >> oczekiwanie…
; >> wynik: przekroczony czas
; >> oczekiwanie…
; >> wynik: 0

Dereferencja Promise’ów, makro @

Makro czytnika @ (znak małpki) umieszczone przed wyrażeniem sprawia, że jeśli 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
2
3
4
5
(def x (promise))

(deliver x 0)
@x
; => 0

Testowanie Promise’ów

Sprawdzanie dostarczenia wartości, 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
2
3
4
5
6
7
8
(def x (promise))

(realized? x)
; => false

(deliver x 0)
(realized? x)
; => true

Delay’e

Referencyjny typ Delay służy do obsługi operacji, które powinny być odłożone w czasie w celu realizacji w wybranym wątku. 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 nastąpi dopiero, gdy pojawi się pierwsza próba dereferencji (odczytu wartości bieżącej) Delay’a.

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

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 przeliczały ponownie wyrażenia. Oznacza to, że do aktualizacji stanu Delay’a dojdzie tylko raz.

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 typu 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
2
3
4
5
6
7
8
9
10
11
12
13
14
(delay)
; => clojure.lang.Delay 0x4bb84a3 {:status :pending, :val nil}

(delay 1)
; => clojure.lang.Delay 0x588c6ea1 {:status :pending, :val nil}

(delay 1 2)
; => clojure.lang.Delay 0x2b95b0f5 {:status :pending, :val nil}

(delay (reduce +' (range 1 2000002)))
; => clojure.lang.Delay 0x7e90b907 {:status :pending, :val nil}

(delay (println "x") 3)
; => 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
2
(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, to 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
2
3
4
5
6
7
8
9
10
11
12
13
(do
  (def x
    (delay
      (println "obliczenia…")
      (Thread/sleep 2000)
      0))

  (println "wynik:" (deref x))
  (println "ponowny odczyt:" (deref x)))

; >> obliczenia…
; >> wynik: 0
; >> ponowny odczyt: 0

Dereferencja Delay’ów, makro @

Makro czytnika @ (znak małpki) umieszczone przed wyrażeniem sprawia, że jeśli 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
2
3
(def x (delay 0))
@x
; => 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śli 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śli nie mamy do czynienia z Delay’em.

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

(force 4)
; => 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
2
(delay? 1)            ; => false
(delay? (delay nil))  ; => true

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 Delay’a, zakończył się pomyślnie i ustawiono wartość bieżącą referencji.

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
2
3
4
5
6
7
8
9
10
11
12
(def x
  (delay
    (Thread/sleep 1000)
    0))

(realized? x)
; => false

(do
  (deref x)
  (realized? x))
; => 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’i 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 zmienią kolejności ich realizowania. 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 aktualizacje Volatile’i 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’i 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 typu 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
2
(volatile!       0)  ; => #<[email protected]: 0>
(volatile! [1 2 3])  ; => #<[email protected]: [1 2 3]>

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’i

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
2
(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ść Volatileu 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
2
3
(def nasz-volatile (volatile! 0))
(deref nasz-volatile)
; => 0

Dereferencja Volatileów, makro @

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

Użycie:

  • @volatile.
Przykład dereferencji Volatile’a z użyciem makra czytnika @
1
2
3
(def nasz-volatile (volatile! 0))
@nasz-volatile
; => 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
2
3
4
5
(def nasz-volatile (volatile! 0))

@nasz-volatile              ; => 0
(vswap! nasz-volatile inc)  ; => 1
@nasz-volatile              ; => 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
2
3
4
(def nasz-volatile (volatile! 0))
@nasz-volatile               ; => 0
(vreset! nasz-volatile "A")  ; => "A"
@nasz-volatile               ; => "A"

Ten materiał jest częścią większej całości, którą można zobaczyć odwiedzając poniższy odnośnik:

Komentarze