Clojure - Programmazione concorrente

Nella programmazione Clojure la maggior parte dei tipi di dati sono immutabili, quindi quando si tratta di programmazione simultanea, il codice che utilizza questi tipi di dati è abbastanza sicuro quando il codice viene eseguito su più processori. Ma molte volte è necessario condividere i dati e, quando si tratta di dati condivisi tra più processori, diventa necessario garantire che lo stato dei dati sia mantenuto in termini di integrità quando si lavora con più processori. Questo è noto comeconcurrent programming e Clojure fornisce supporto per tale programmazione.

Il sistema di memoria transazionale software (STM), esposto tramite dosync, ref, set, alter, ecc. Supporta la condivisione dello stato di cambiamento tra i thread in modo sincrono e coordinato. Il sistema dell'agente supporta la condivisione dello stato di cambiamento tra i thread in modo asincrono e indipendente. Il sistema degli atomi supporta la condivisione dello stato di cambiamento tra i thread in modo sincrono e indipendente. Mentre il sistema var dinamico, esposto tramite def, binding, ecc. Supporta l'isolamento del cambiamento di stato all'interno dei thread.

Anche altri linguaggi di programmazione seguono il modello per la programmazione concorrente.

  • Hanno un riferimento diretto ai dati che possono essere modificati.

  • Se è richiesto l'accesso condiviso, l'oggetto è bloccato, il valore viene modificato e il processo continua per il successivo accesso a quel valore.

In Clojure non ci sono blocchi, ma riferimenti indiretti a strutture di dati persistenti immutabili.

Ci sono tre tipi di riferimenti in Clojure.

  • Vars - Le modifiche sono isolate nei thread.

  • Refs - Le modifiche vengono sincronizzate e coordinate tra i thread.

  • Agents - Coinvolge modifiche indipendenti asincrone tra i thread.

Le seguenti operazioni sono possibili in Clojure per quanto riguarda la programmazione simultanea.

Transazioni

La concorrenza in Clojure si basa sulle transazioni. I riferimenti possono essere modificati solo all'interno di una transazione. Le seguenti regole vengono applicate nelle transazioni.

  • Tutti i cambiamenti sono atomici e isolati.
  • Ogni modifica a un riferimento avviene in una transazione.
  • Nessuna transazione vede l'effetto prodotto da un'altra transazione.
  • Tutte le transazioni vengono inserite all'interno del blocco dosync.

Abbiamo già visto cosa fa il blocco dosync, guardiamolo di nuovo.

dosync

Esegue l'espressione (in un do implicito) in una transazione che comprende l'espressione e qualsiasi chiamata annidata. Avvia una transazione se nessuna è già in esecuzione su questo thread. Qualsiasi eccezione non rilevata interromperà la transazione e uscirà da dosync.

Di seguito è riportata la sintassi.

Sintassi

(dosync expression)

Parameters - 'espressione' è l'insieme di espressioni che arriverà nel blocco dosync.

Return Value - Nessuno.

Diamo un'occhiata a un esempio in cui proviamo a cambiare il valore di una variabile di riferimento.

Esempio

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

Produzione

Il programma di cui sopra quando viene eseguito dà il seguente errore.

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

Dall'errore puoi vedere chiaramente che non puoi modificare il valore di un tipo di riferimento senza prima avviare una transazione.

Affinché il codice di cui sopra funzioni, dobbiamo inserire il comando alter in un blocco dosync come fatto nel programma seguente.

Esempio

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

Il programma precedente produce il seguente output.

Produzione

[John Mark]

Vediamo un altro esempio di dosync.

Esempio

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

Nell'esempio sopra, abbiamo due valori che vengono modificati in un blocco dosync. Se la transazione ha esito positivo, entrambi i valori cambieranno altrimenti l'intera transazione fallirà.

Il programma precedente produce il seguente output.

Produzione

10 20
-10 40