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