Clojure - Programación concurrente
En la programación de Clojure, la mayoría de los tipos de datos son inmutables, por lo tanto, cuando se trata de programación concurrente, el código que utiliza estos tipos de datos es bastante seguro cuando el código se ejecuta en varios procesadores. Pero muchas veces, existe el requisito de compartir datos, y cuando se trata de datos compartidos entre múltiples procesadores, es necesario asegurarse de que el estado de los datos se mantenga en términos de integridad cuando se trabaja con múltiples procesadores. Esto se conoce comoconcurrent programming y Clojure proporciona soporte para dicha programación.
El sistema de memoria transaccional de software (STM), expuesto a través de dosync, ref, set, alter, etc., admite compartir el estado cambiante entre subprocesos de manera sincrónica y coordinada. El sistema de agentes permite compartir el estado cambiante entre subprocesos de manera asincrónica e independiente. El sistema de átomos permite compartir el estado cambiante entre subprocesos de manera síncrona e independiente. Mientras que el sistema var dinámico, expuesto a través de def, binding, etc., admite el aislamiento de cambios de estado dentro de los hilos.
Otros lenguajes de programación también siguen el modelo de programación concurrente.
Tienen una referencia directa a los datos que se pueden modificar.
Si se requiere acceso compartido, el objeto se bloquea, el valor se cambia y el proceso continúa para el siguiente acceso a ese valor.
En Clojure no hay bloqueos, sino referencias indirectas a estructuras de datos persistentes inmutables.
Hay tres tipos de referencias en Clojure.
Vars - Los cambios se aíslan en hilos.
Refs - Los cambios se sincronizan y coordinan entre hilos.
Agents - Implica cambios independientes asincrónicos entre hilos.
Las siguientes operaciones son posibles en Clojure con respecto a la programación concurrente.
Actas
La concurrencia en Clojure se basa en transacciones. Las referencias solo se pueden cambiar dentro de una transacción. Las siguientes reglas se aplican en las transacciones.
- Todos los cambios son atómicos y aislados.
- Cada cambio en una referencia ocurre en una transacción.
- Ninguna transacción ve el efecto de otra transacción.
- Todas las transacciones se colocan dentro del bloque dosync.
Ya vimos lo que hace el bloque dosync, veámoslo de nuevo.
dosync
Ejecuta la expresión (en un do implícito) en una transacción que abarca la expresión y cualquier llamada anidada. Inicia una transacción si ninguna ya se está ejecutando en este hilo. Cualquier excepción no detectada abortará la transacción y saldrá de dosync.
A continuación se muestra la sintaxis.
Sintaxis
(dosync expression)
Parameters - 'expresión' es el conjunto de expresiones que vendrán en el bloque dosync.
Return Value - Ninguno.
Veamos un ejemplo en el que intentamos cambiar el valor de una variable de referencia.
Ejemplo
(ns clojure.examples.example
(:gen-class))
(defn Example []
(def names (ref []))
(alter names conj "Mark"))
(Example)
Salida
El programa anterior cuando se ejecuta da el siguiente 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
En el error, puede ver claramente que no puede cambiar el valor de un tipo de referencia sin antes iniciar una transacción.
Para que el código anterior funcione, tenemos que colocar el comando alter en un bloque dosync como se hace en el siguiente programa.
Ejemplo
(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)
El programa anterior produce la siguiente salida.
Salida
[John Mark]
Veamos otro ejemplo de dosync.
Ejemplo
(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)
En el ejemplo anterior, tenemos dos valores que se están cambiando en un bloque dosync. Si la transacción es exitosa, ambos valores cambiarán; de lo contrario, toda la transacción fallará.
El programa anterior produce la siguiente salida.
Salida
10 20
-10 40