Zobrazují se příspěvky se štítkemconcurrency. Zobrazit všechny příspěvky
Zobrazují se příspěvky se štítkemconcurrency. Zobrazit všechny příspěvky

25. března 2017

Clojure concurrency: Vars

Významnou vlastností Clojure jsou neměnitelné datové struktury. Je to taková dvojsečná vlastnost (i když benefity výrazně převažují). Na jednu stranu to vývoj zjednodušuje, protože se nemusíme bát, že se nám data změní pod rukama.

Nadruhou stranu vyvstává otázka, jak s neměnitelnými daty pracovat - nic v našem světě není neměnné (a všechno jednou pomine). Jak tedy Clojure řeší změnu stavu a s tím související konkurenci?

K dispozici jsou čtyři mechanizmy, jak měnit stav:
  • Vars jako globální úložiště, pro eventuální per-thread změny.
  • Atoms pro synchronní, nekoordinované změny.
  • Refs pro synchronní, v transakci koordinované změny.
  • Agents pro asynchronní, nekoordinované změny.
Postupně bych se chtěl podívat na všechny způsoby, ale dnes začneme tím nejjednodušším, čemu se žádný začátečník v Clojure nevyhne.

Vars

Var je způsob, jakým Clojure odkazuje na úložiště. Toto úložiště obsahuje "standardní", neměnná data. Var, ale můžeme dočasně přesměrovat na jiné úložiště. Podstatnou vlastností je, že toto dočasné přesměrování - binding - je vidět jenom v rámci aktuálního vlákna, ostatní vlákna vidí pořád původní hodnotu.

Statické Vars

Klasické Var se vytvoří speciálním formem def: (def x 42). Takto definováno, je Var statické - pokud bychom ho chtěli svázat s jiným úložištěm, vyhodí Clojure výjimku IllegalStateException.


Dynamické Vars

Pokud chceme, aby Var ukazovalo na jiné úložiště, musíme ho explicitně definovat jako dynamické pomocí instrukce ^:dynamic. Samotné přesměrování se provádí makrem binding. Po dobu trvání bloku binding směřuje Var na nové úložiště, aby se po jeho skončení vrátilo ke své původní hodnotě.

Dle konvence se dynamické Vars uzavírají do *earmuffs*. Označují se tak věci, určené pro re-binding.


Vars jsou globální

Vars jsou globální - viditelné pro všechny thready. Pokud tedy vytvoříme Var v jiném vlákně, je dosažitelné také ze všech ostatních vláken.

V následujícím příkladu vytvoříme Var v jiném vlákně pomocí makra future:


Namespace

V rámci namespace, v němž byly vytvořeny, jsou Vars přístupné svým názvem. Pokud se přepneme do jiného namespacu, je potřeba na Var odkazovat plně kvalifikovaným názvem, nebo ho "naimportovat" z původního namespace pomocí funkce refer.


Funkce jsou také Vars

Ve Vars se neukládají jenom data, ale také funkce. Všechno výše napsané tedy platí pro funkce úplně stejně, včetně toho, že je můžeme dynamicky re-bindovat.

To může být zajímavý mechanismus, jak v runtimu dynamicky měnit chování funkcí. Nicméně, užívejte s mírou - jazyk to umožňuje, ale je to výjmečné řešení.


A kde je ta konkurence?

Se samotnými Vars si moc konkurentního programování neužijeme. Je to ale základní stavební kámen, od kterého se odvíjejí ostatní způsoby. Konec konců, všechny v úvodu zmíněné mechanizmy - Atomy, Refs a Agenti - jsou ve výsledku uloženy ve Vars.

GitHub projekt

Clojure concurrency chci pojmout jako mini-seriál (viz Související články níže), který bude podložený projektem na GitHubu, kam budou postupně přibývat jednotlivé příklady:

Související články

  • TBD: Clojure concurrency: Atoms
  • TBD: Clojure concurrency: Refs
  • TBD: Clojure concurrency: Agents

Starší související články

29. července 2011

Jak měnit neměnitelné. Refs

Clojure používá neměnitelná (immutable) data/objekty. Pokud potřebujeme měnitelná (mutable) data, řeší to Clojure "měnitelnou referencí na neměnitelný objekt" :-) Jedním z prostředků, které toto řeší jsou Refs: jsou to transakční reference, které umožňují bezpečné sdílení měnitelných úložišť pomocí systému STM (Software Transactional Memory).

Měnitelná reference (ref) se vytvoří funkcí ref, její hodnotu vrací buď funkce deref, nebo makro @:
(def my-ref (ref "immutable data"))
; #'user/my-ref
my-ref
; #<ref@18352d8: "immutable data">
@my-ref
; "immutable data"
(deref my-ref)
; "immutable data"
Pokud chceme referenci nastavit na jinou (neměnitelnou) hodnotu, slouží k tomu funkce ref-set:
(ref-set my-ref "another immutable data")
; IllegalStateException No transaction running  clojure.lang.LockingTransaction.getEx (LockingTransaction.java:208)
Jejda! Zapomněli jsme na transakci :-)
(dosync (ref-set my-ref "another immutable data"))
; "another immutable data"
@my-ref
; "another immutable data"
Pokud chceme provést čtení hodnoty a zároveň její změnu v jednom kroku (= aplikovat na hodnotu funkci), je vhodné použít funkci alter:
(dosync (alter my-ref #(apply str (reverse %))))
; "atad elbatummi rehtona"
@my-ref
; "atad elbatummi rehtona"
Na reference je také možné přidat validace:
(def counter (ref 0 :validator number?))
; #'user/counter
(dosync (ref-set counter "string"))
; IllegalStateException Invalid reference state  clojure.lang.ARef.validate (ARef.java:33)
@counter
; 0
(dosync (ref-set counter 42))
; 42
@counter
; 42
O Refs jsem už jednou (trochu) psal. Z dnešního pohledu k tomu mám dvě výhrady:
  • místo ref-set jsem měl použít alter,
  • místo reference jsem měl použít atom.
ref-set je vhodné použít tehdy, pokud přiřazuji novou hodnotu (nepočítám ji). alter tehdy, pokud nad hodnotou provádím nějakou funkci (např. inkrementace, přidání hodnoty do kolekce apod.).

Při rozhodování, jestli použít ref nebo atom je podstatné, jestli využiju transakci - v transakci můžu updatovat více referencí. Pokud budu měnit pouze jedinou hodnotu (bez vazby na cokoli jiného) je vhodnější použít atom (a o těch až někdy příště).