Clojure - Concurrent Programming



In Clojure programming most data types are immutable, thus when it comes to concurrent programming, the code using these data types are pretty safe when the code runs on multiple processors. But many a times, there is a requirement to share data, and when it comes to shared data across multiple processors, it becomes necessary to ensure that the state of the data is maintained in terms of integrity when working with multiple processors. This is known as concurrent programming and Clojure provides support for such programming.

The software transactional memory system (STM), exposed through dosync, ref, set, alter, etc. supports sharing changing state between threads in a synchronous and coordinated manner. The agent system supports sharing changing state between threads in an asynchronous and independent manner. The atoms system supports sharing changing state between threads in a synchronous and independent manner. Whereas the dynamic var system, exposed through def, binding, etc. supports isolating changing state within threads.

Other programming languages also follow the model for concurrent programming.

  • They have a direct reference to the data which can be changed.

  • If shared access is required, the object is locked, the value is changed, and the process continues for the next access to that value.

In Clojure there are no locks, but Indirect references to immutable persistent data structures.

There are three types of references in Clojure.

  • Vars − Changes are isolated in threads.

  • Refs − Changes are synchronized and coordinated between threads.

  • Agents − Involves asynchronous independent changes between threads.

The following operations are possible in Clojure with regards to concurrent programming.

Transactions

Concurrency in Clojure is based on transactions. References can only be changed within a transaction. Following rules are applied in transactions.

  • All changes are atomic and isolated.
  • Every change to a reference happens in a transaction.
  • No transaction sees the effect made by another transaction.
  • All transactions are placed inside of dosync block.

We already seen what the dosync block does, let’s look at it again.

dosync

Runs the expression (in an implicit do) in a transaction that encompasses expression and any nested calls. Starts a transaction if none is already running on this thread. Any uncaught exception will abort the transaction and flow out of dosync.

Following is the syntax.

Syntax

(dosync expression)

Parameters − ‘expression’ is the set of expressions which will come in the dosync block.

Return Value − None.

Let’s look at an example wherein we try to change the value of a reference variable.

Example

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

Output

The above program when run gives the following error.

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

From the error you can clearly see that you cannot change the value of a reference type without first initiating a transaction.

In order for the above code to work, we have to place the alter command in a dosync block as done in the following program.

Example

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

The above program produces the following output.

Output

[John Mark]

Let’s see another example of dosync.

Example

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

In the above example, we have two values which are being changed in a dosync block. If the transaction is successful, both values will change else the whole transaction will fail.

The above program produces the following output.

Output

10 20
-10 40
Advertisements