Clojure状態まとめ

状態とは,ある時系列におけるある時点での 同一実体 である.

Clojureにおいて値は immutable であり persistent である.

しかし,変更不可なオブジェクトに対して変更可能な参照を作成することで変更不可のものを管理することができる.

Clojureでは,値と同一実体を明確に区別して扱う. 4つの参照型を用意している.

  • ref: 協調的,同期的な変更を管理.
  • atom: 独立的, 同期的な変更を管理.
  • agent: 非同期な変更を管理.
  • var: スレッドローカルな変更を管理.

Clojure参照型

Clojure: atom

Clojureにおける atom は非協調的(Independent)で同期的(Syncronous)な変更を管理する.

Basics

  • reset!: atomの更新ではトランザクションは不要. reset! 関数を用いる.
  • swap!: 関数はatomを引数にとり更新した値を返す関数を適用するための関数.
;; atomの宣言
(def foo (atom 0))
 
;; atomがbindされた.
foo
 
;; 参照先の値をリーダマクロ@で読む
@foo
 
;; 値の更新
(reset! foo 2)
 
;; 関数値の設定
(swap! foo (fn [_] (+ 1 1)))

💡Clojure atomはロックフリー

しばしばClojureのエレガントさの1つとしてClojureは並行プログラミングを簡単にするということが挙げられるが, その確信はしばしばロックフリーと表現される.

📝共有メモリモデルにおいて値のwriteが走っているときにそのreadをブロックするが, atomはreadだけならロックしない. これによってあるスレッドが書き込みをしているときに別の処理がまたされることはない.

https://www.braveclojure.com/zombie-metaphysics/

Unlike futures, delays, and promises, dereferencing an atom (or any other reference type) will never block. When you dereference futures, delays, and promises, it’s like you’re saying “I need a value now, and I will wait until I get it,” so it makes sense that the operation would block. However, when you dereference a reference type, it’s like you’re saying “give me the value I’m currently referring to,” so it makes sense that the operation doesn’t block, because it doesn’t have to wait for anything.


ロックフリーとは速いということだ. ナルトにでてくるロック・リーの八門敦煌を思い出そう.

従来型の共有メモリ式ロック(locking)

いちおうJavaの共有メモリ式の方法も locking というシンタックスでできる.

https://clojuredocs.org/clojure.core/locking

loopのなかでdef atomし続けるとメモリ枯渇

安全のために, 状態管理は🔧Clojure Integrantに任せたり, dynamicみたいな明示的な宣言をしたほうがいいのかも.

(def ^:dynamic *time* (atom 0))

References

Clojure: ref

Clojureにおける ref は協調的(Corrdinate)で同期的(Syncronous)な変更を管理する. つまり,複数の参照を同時に更新する.

Software transactinal memory(STM) をClojureで実現するためのシンタックス.

refで宣言した値を読むには deref をつかう. リーダマクロである @ をつかって略記する.

参照先のオブジェクトを変更するには, ref-set or alter をつかう.

commute をつかうと, alterに順序保証ができる.


Clojure: agent

Clojureにおける agent は非協調的(Independent)で非同期的(Asyncronous)な変更を管理する.

deref orリーダーマクロ @ を利用して値を読む. 値を読むだけならどのスレッドからでも調停や協調なしに(待つことなく), 即時に参照し放題.

send メソッドを利用することで値を更新する. 正確には, send メソッドの引数でagentを更新するための関数値を与えると,Clojureはその処理をスレッドプールでの処理エンキューしてあとはClojureがよろしく実行してくれる.

send-off を利用すると, スレッドプールが必要に応じて動的に拡張されるのでわかりやすく言えばすぐに実行される.ファイル書き込みなどのようなBlocking I/O, 1つの処理によってスレッドが止まってしまう場合に自動で別のスレッドが作られて実行される.

スレッドプールのスレッドをつかってClojureが暇なときに実行するため, いつagentの値が更新されたかわからない. 待ち合わせには await を利用する.

agentの値の更新化失敗するとそれ以後のagentの更新はできなくなる. フタをしてしまうようなもの. agent-errors に失敗内容が入っている. また set-error-mode! メソッドで :continue を指定すると続行することもできる.

agentのスレッドプール名

nは数値が入る. sendの方は数値の上限が固定であり, send-offのほうは可変かつ寿命がある.

  • send: clojure-send-pool-n
  • send-off: clojure-send-off-pool-n

Clojure: deref

Clojureのコードで多用されている @(アットマーク)はderefのリーダマクロ.

状態の読み出しに利用できるリーダマクロだが, 何でも読める.atomだろうとrefだろうとagentだろう, futureもpromiseも.

Clojure Threading Macros だと deref を使うのかな? best practiceがわからない.

💡deferとはdereferenceの略

ずっとderefについて謎だったが, これはどうもdereferenceというコンピュータ・サイエンスの用語.

もともとはdereferencing pointerというようなC言語におけるメモリ上の実体を指すポインタ. 派生して, データの参照から実体を取得みたいなニュアンスになった.

Clojur State Topics

多数スレッドのwriteによるリトライでSTMの性能が落ちる

少し読み切れてないところもあるが, コメント欄も含めて議論されている.

ClojureのSTMは使い物にならない – JUMPERZ.NET Blog

STMは共有リソースに複数のスレッドが書き込みをするときに, 書き込み先が他のスレッドに既に更新されてれば失敗してやり直すという仕組みなため, リトライがたくさん発生するような条件では遅くなる.

これを一般化するならば, マルチスレッドで共有資源に書き込むならば, キューイング & シングルスレッドで書き込むほうが効率がいい.

Clojure State Insights

atomでのreset!とswap!の違い

reset!とswap!は互換性がある. Clojure Style Guide には, Prefer swap! over reset! というルールもある. とりあえず迷ったらswap!でよい.


別の観点では, swap!はVectorやMapに対する操作で多用する. conj, assoc, update-inなどとと組み合わせる. これによって, あるデータ構造の一部を変更することができる.

(swap! orders assoc (:id msg) msg)

複数の状態を一度にupdateはrefと dosync

はじめ, 状態の属性を複数持とうとした時, atomにMapをbindするのかと思ったが, そもそもrefとdosyncでupdateすればよいのだった.

refとdosyncはそもそもシングルスレッドでしか参照しない前提のときも使うものなのかな? 論理的には更新で矛盾が生じないのだけど.