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 asynchronicznego i ró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ędzie
nil`.
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ścinil
, 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 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ż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.
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 +)
; => #<core$future_call$reify__6320@e019b8e: 0>
(future-call #(do (println "test") 3))
; >> test
; => #<core$future_call$reify__6320@2c97a223: :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).
Future
(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
.
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 "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 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
.
@
(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?
.
future-cancel
(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.
future?
(future? 1) ; => false
(future? (future nil)) ; => 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.
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)
; => 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.
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)
; => false
(Thread/sleep 1000)
(realized? x)
; => 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.
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)
; => 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.
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.
promise
(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).
Promise
(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.
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 "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 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
.
@
(def x (promise))
(deliver x 0)
@x
; => 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.
realized?
(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 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
.
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)
; => 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).
Delay
(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.
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 "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 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
.
@
(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ż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.
force
(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.
delay?
(delay? 1) ; => false
(delay? (delay nil)) ; => 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.
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)
; => 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’ó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
.
volatile!
(volatile! 0) ; => #<Volatile@316f0a55: 0>
(volatile! [1 2 3]) ; => #<Volatile@545c5f23: [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’ó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).
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.
deref
(def nasz-volatile (volatile! 0))
(deref nasz-volatile)
; => 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
.
@
(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.
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 ; => 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.
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 ; => 0
(vreset! nasz-volatile "A") ; => "A"
@nasz-volatile ; => "A"
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ń.
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 "Pierwszy startuje..")
(Thread/sleep 4000)
(println "Pierwszy gotowy!")))
(Thread/sleep 1250)
(locking x (println "Drugi startuje..") (println "Drugi gotowy!"))
; >> Pierwszy startuje..
; >> Pierwszy gotowy!
; >> Drugi startuje..
; >> 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.