Clojure - programowanie współbieżne
W programowaniu Clojure większość typów danych jest niezmienna, więc jeśli chodzi o programowanie współbieżne, kod wykorzystujący te typy danych jest dość bezpieczny, gdy kod działa na wielu procesorach. Jednak często istnieje wymóg współdzielenia danych, a jeśli chodzi o udostępnianie danych na wielu procesorach, konieczne staje się zapewnienie, że stan danych jest utrzymywany pod względem integralności podczas pracy z wieloma procesorami. Jest to znane jakoconcurrent programming Clojure zapewnia wsparcie dla takiego programowania.
System pamięci transakcyjnej oprogramowania (STM), udostępniany poprzez dosync, ref, set, alter, itp. Obsługuje współdzielenie zmieniającego się stanu między wątkami w sposób synchroniczny i skoordynowany. System agenta obsługuje współdzielenie zmieniającego się stanu między wątkami w sposób asynchroniczny i niezależny. System atomów obsługuje współdzielenie zmieniającego się stanu między wątkami w sposób synchroniczny i niezależny. Podczas gdy dynamiczny system var, udostępniany przez def, bind, itp., Wspiera izolowanie zmieniającego się stanu w wątkach.
Inne języki programowania również podążają za modelem programowania współbieżnego.
Mają bezpośrednie odniesienie do danych, które można zmienić.
Jeśli wymagany jest dostęp współdzielony, obiekt jest blokowany, wartość jest zmieniana, a proces jest kontynuowany do następnego dostępu do tej wartości.
W Clojure nie ma blokad, ale pośrednie odniesienia do niezmiennych trwałych struktur danych.
W Clojure istnieją trzy typy odniesień.
Vars - Zmiany są izolowane w wątkach.
Refs - Zmiany są synchronizowane i koordynowane między wątkami.
Agents - Obejmuje asynchroniczne, niezależne zmiany między wątkami.
Następujące operacje są możliwe w Clojure w odniesieniu do programowania współbieżnego.
Transakcje
Współbieżność w Clojure opiera się na transakcjach. Referencje można zmieniać tylko w ramach transakcji. W transakcjach obowiązują następujące zasady.
- Wszystkie zmiany są atomowe i izolowane.
- Każda zmiana odniesienia ma miejsce w transakcji.
- Żadna transakcja nie widzi skutku innej transakcji.
- Wszystkie transakcje są umieszczane wewnątrz bloku dosync.
Widzieliśmy już, co robi blok dosync, spójrzmy na to ponownie.
dosync
Uruchamia wyrażenie (w niejawnym do) w transakcji, która obejmuje wyrażenie i wszelkie zagnieżdżone wywołania. Rozpoczyna transakcję, jeśli żadna nie jest już uruchomiona w tym wątku. Każdy nieprzechwycony wyjątek spowoduje przerwanie transakcji i wyjście z dosync.
Poniżej znajduje się składnia.
Składnia
(dosync expression)
Parameters - „wyrażenie” to zestaw wyrażeń, które pojawią się w bloku dosync.
Return Value - Żaden.
Spójrzmy na przykład, w którym próbujemy zmienić wartość zmiennej referencyjnej.
Przykład
(ns clojure.examples.example
(:gen-class))
(defn Example []
(def names (ref []))
(alter names conj "Mark"))
(Example)
Wynik
Powyższy program po uruchomieniu daje następujący błąd.
Caused by: java.lang.IllegalStateException: No transaction running
at clojure.lang.LockingTransaction.getEx(LockingTransaction.java:208)
at clojure.lang.Ref.alter(Ref.java:173)
at clojure.core$alter.doInvoke(core.clj:1866)
at clojure.lang.RestFn.invoke(RestFn.java:443)
at clojure.examples.example$Example.invoke(main.clj:5)
at clojure.examples.example$eval8.invoke(main.clj:7)
at clojure.lang.Compiler.eval(Compiler.java:5424)
... 12 more
Z błędu widać wyraźnie, że nie można zmienić wartości typu referencyjnego bez uprzedniego zainicjowania transakcji.
Aby powyższy kod działał, musimy umieścić polecenie alter w bloku dosync, tak jak to zrobiono w poniższym programie.
Przykład
(ns clojure.examples.example
(:gen-class))
(defn Example []
(def names (ref []))
(defn change [newname]
(dosync
(alter names conj newname)))
(change "John")
(change "Mark")
(println @names))
(Example)
Powyższy program generuje następujące dane wyjściowe.
Wynik
[John Mark]
Zobaczmy kolejny przykład dosync.
Przykład
(ns clojure.examples.example
(:gen-class))
(defn Example []
(def var1 (ref 10))
(def var2 (ref 20))
(println @var1 @var2)
(defn change-value [var1 var2 newvalue]
(dosync
(alter var1 - newvalue)
(alter var2 + newvalue)))
(change-value var1 var2 20)
(println @var1 @var2))
(Example)
W powyższym przykładzie mamy dwie wartości, które są zmieniane w bloku dosync. Jeśli transakcja się powiedzie, obie wartości zmienią się, w przeciwnym razie cała transakcja zakończy się niepowodzeniem.
Powyższy program generuje następujące dane wyjściowe.
Wynik
10 20
-10 40