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