Clojure - Gleichzeitige Programmierung

Bei der Clojure-Programmierung sind die meisten Datentypen unveränderlich. Wenn Sie also gleichzeitig programmieren, ist der Code, der diese Datentypen verwendet, ziemlich sicher, wenn der Code auf mehreren Prozessoren ausgeführt wird. Oft ist es jedoch erforderlich, Daten gemeinsam zu nutzen, und wenn Daten über mehrere Prozessoren hinweg gemeinsam genutzt werden sollen, muss sichergestellt werden, dass der Status der Daten in Bezug auf die Integrität bei der Arbeit mit mehreren Prozessoren erhalten bleibt. Dies ist bekannt alsconcurrent programming und Clojure bietet Unterstützung für eine solche Programmierung.

Das Software-Transaktionsspeichersystem (STM), das durch Dosieren, Ref, Setzen, Ändern usw. verfügbar gemacht wird, unterstützt das synchrone und koordinierte Teilen des sich ändernden Status zwischen Threads. Das Agentensystem unterstützt das asynchrone und unabhängige Teilen des sich ändernden Status zwischen Threads. Das Atomsystem unterstützt das synchrone und unabhängige Teilen des sich ändernden Zustands zwischen Threads. Während das dynamische VAR-System, das durch Def, Bindung usw. verfügbar gemacht wird, das Isolieren des sich ändernden Zustands innerhalb von Threads unterstützt.

Andere Programmiersprachen folgen ebenfalls dem Modell für die gleichzeitige Programmierung.

  • Sie haben einen direkten Bezug zu den Daten, die geändert werden können.

  • Wenn ein gemeinsamer Zugriff erforderlich ist, wird das Objekt gesperrt, der Wert geändert und der Prozess für den nächsten Zugriff auf diesen Wert fortgesetzt.

In Clojure gibt es keine Sperren, sondern indirekte Verweise auf unveränderliche persistente Datenstrukturen.

In Clojure gibt es drei Arten von Referenzen.

  • Vars - Änderungen sind in Threads isoliert.

  • Refs - Änderungen werden zwischen Threads synchronisiert und koordiniert.

  • Agents - Beinhaltet asynchrone unabhängige Änderungen zwischen Threads.

Die folgenden Operationen sind in Clojure in Bezug auf die gleichzeitige Programmierung möglich.

Transaktionen

Die Parallelität in Clojure basiert auf Transaktionen. Referenzen können nur innerhalb einer Transaktion geändert werden. Die folgenden Regeln werden bei Transaktionen angewendet.

  • Alle Änderungen sind atomar und isoliert.
  • Jede Änderung an einer Referenz erfolgt in einer Transaktion.
  • Keine Transaktion sieht den Effekt einer anderen Transaktion.
  • Alle Transaktionen werden innerhalb des Dosync-Blocks platziert.

Wir haben bereits gesehen, was der Dosync-Block tut. Schauen wir uns das noch einmal an.

dosync

Führt den Ausdruck (implizit) in einer Transaktion aus, die den Ausdruck und alle verschachtelten Aufrufe umfasst. Startet eine Transaktion, wenn in diesem Thread noch keine ausgeführt wird. Jede nicht erfasste Ausnahme bricht die Transaktion ab und fließt aus der Dosync heraus.

Es folgt die Syntax.

Syntax

(dosync expression)

Parameters - 'Ausdruck' ist die Menge der Ausdrücke, die im Dosync-Block enthalten sind.

Return Value - Keine.

Schauen wir uns ein Beispiel an, in dem wir versuchen, den Wert einer Referenzvariablen zu ändern.

Beispiel

(ns clojure.examples.example
   (:gen-class))
(defn Example []
   (def names (ref []))
   (alter names conj "Mark"))
(Example)

Ausgabe

Das obige Programm gibt beim Ausführen den folgenden Fehler aus.

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

Anhand des Fehlers können Sie deutlich erkennen, dass Sie den Wert eines Referenztyps nicht ändern können, ohne zuvor eine Transaktion initiiert zu haben.

Damit der obige Code funktioniert, müssen wir den Befehl alter in einen Dosync-Block einfügen, wie im folgenden Programm beschrieben.

Beispiel

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

Das obige Programm erzeugt die folgende Ausgabe.

Ausgabe

[John Mark]

Sehen wir uns ein weiteres Beispiel für Dosync an.

Beispiel

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

Im obigen Beispiel haben wir zwei Werte, die in einem Dosync-Block geändert werden. Wenn die Transaktion erfolgreich ist, ändern sich beide Werte, andernfalls schlägt die gesamte Transaktion fehl.

Das obige Programm erzeugt die folgende Ausgabe.

Ausgabe

10 20
-10 40