Clojure - Programação Simultânea

Na programação Clojure, a maioria dos tipos de dados são imutáveis, portanto, quando se trata de programação simultânea, o código que usa esses tipos de dados é bastante seguro quando o código é executado em vários processadores. Mas, muitas vezes, há um requisito para compartilhar dados e, quando se trata de dados compartilhados entre vários processadores, é necessário garantir que o estado dos dados seja mantido em termos de integridade ao trabalhar com vários processadores. Isso é conhecido comoconcurrent programming e Clojure fornece suporte para essa programação.

O sistema de memória transacional de software (STM), exposto por meio de dosync, ref, set, alter, etc. suporta o compartilhamento de estado de mudança entre threads de maneira síncrona e coordenada. O sistema do agente oferece suporte ao compartilhamento de estado de mudança entre threads de maneira assíncrona e independente. O sistema de átomos suporta o compartilhamento de estado de mudança entre threads de maneira síncrona e independente. Considerando que o sistema var dinâmico, exposto por meio de def, binding, etc. suporta o isolamento de estado de mudança dentro das threads.

Outras linguagens de programação também seguem o modelo de programação simultânea.

  • Eles têm uma referência direta aos dados que podem ser alterados.

  • Se o acesso compartilhado for necessário, o objeto será bloqueado, o valor será alterado e o processo continuará para o próximo acesso a esse valor.

Em Clojure não há bloqueios, mas referências indiretas a estruturas de dados persistentes imutáveis.

Existem três tipos de referências em Clojure.

  • Vars - As mudanças são isoladas em threads.

  • Refs - As mudanças são sincronizadas e coordenadas entre os threads.

  • Agents - Envolve mudanças independentes assíncronas entre threads.

As seguintes operações são possíveis no Clojure com relação à programação simultânea.

Transações

A simultaneidade em Clojure é baseada em transações. As referências só podem ser alteradas dentro de uma transação. As seguintes regras são aplicadas nas transações.

  • Todas as mudanças são atômicas e isoladas.
  • Cada mudança em uma referência acontece em uma transação.
  • Nenhuma transação vê o efeito feito por outra transação.
  • Todas as transações são colocadas dentro do bloco dosync.

Já vimos o que o bloco dosync faz, vamos examiná-lo novamente.

dosync

Executa a expressão (em um do implícito) em uma transação que abrange expressão e quaisquer chamadas aninhadas. Inicia uma transação se nenhuma já estiver em execução neste encadeamento. Qualquer exceção não detectada abortará a transação e sairá do dosync.

A seguir está a sintaxe.

Sintaxe

(dosync expression)

Parameters - 'expressão' é o conjunto de expressões que virão no bloco dosync.

Return Value - Nenhum.

Vejamos um exemplo em que tentamos alterar o valor de uma variável de referência.

Exemplo

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

Resultado

O programa acima, quando executado, apresenta o seguinte erro.

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

Com base no erro, você pode ver claramente que não pode alterar o valor de um tipo de referência sem primeiro iniciar uma transação.

Para que o código acima funcione, temos que colocar o comando alter em um bloco dosync como feito no programa a seguir.

Exemplo

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

O programa acima produz a seguinte saída.

Resultado

[John Mark]

Vamos ver outro exemplo de dosync.

Exemplo

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

No exemplo acima, temos dois valores que estão sendo alterados em um bloco dosync. Se a transação for bem-sucedida, ambos os valores serão alterados, caso contrário, toda a transação falhará.

O programa acima produz a seguinte saída.

Resultado

10 20
-10 40