ClojureCLRのスーパーアントヒル

Clojureのマルチスレッド化は、STM(ソフトウェアトランザクションメモリシステム)が実装されているため、新しいレベルの開発に取り入れられました。 デモとして、Rich Hickey(Clojureの神の著者)とDavid Miller(.NetでClojureの実装を書いた人)は、蟻塚をシミュレートする蟻を提供します。 そこにいる各アリは別々のストリームに住んでいます。 アリは一般的なフィールドのセルの周りを走り回り、食べ物を集めて蟻塚に運び、お互いに対立しません。



このプログラムでの運動の結果を一般に公開したいと思います。 この記事が、.NetプラットフォームでClojureに精通し始めている人に役立つことを願っています。



最初のリンク:



github.com/kemerovo-man/super-ants-clojure-clr

youtu.be/xZ9AGQ3L-EI

sourceforge.net/projects/clojureclr/files

github.com/clojure/clojure-clr/blob/master/Clojure/Clojure.Source/clojure/samples/ants.clj



私がしていること。

1. グラフィックス 。 元のプログラムでは、アリは単なるダッシュです。 私の昆虫はより自然に描かれています。

2. 新しいヒーロー 。 アリに加えて、アブラムシとてんとう虫がいます。

3. ヒーロー相互作用 。 草が成長し、アブラムシが草を食べ、砂糖を生産し、卵を産みます(増殖)。 てんとう虫はアブラムシと卵を食べます。 アリは途中で障害物を避けます。 (まだアリはアブラムシを草の上に引きずり、てんとう虫がアブラムシを食べるのを防ぐべきですが、これはまだ実現されていません。)

3. 動作 。 アリの行動はより複雑になり、オリジナルよりも賢くなりました。 新しいヒーローの動作を追加しました。

4. チャート 。 グラフでプロセスのダイナミクスを確認できます。 草の成長、アブラムシの豊富さ、蟻塚と周辺地域の砂糖の量。

5. 原始的な拡大鏡は詳細を見ることができます。 マウスの右ボタン。

6. マウス 。 フィールドセルをクリックすると、セルに含まれる情報がコンソールに表示されます。 マウスホイールを使用すると、昆虫の移動速度を変更できます。



初期化パラメータを変更することにより、このエコシステムの開発のさまざまなシナリオを取得できます。 草の成長が速いほど、アブラムシの増殖が速くなり、畑に多くの砂糖が表示されます。 草は砂糖では成長しないため、アブラムシのアブラムシは草の成長に影響を与えます。 起動時の昆虫のさまざまな比率(アブラムシの数、てんとう虫の数、アリの数)が新しいダイナミクスを決定します。



各昆虫は、4つのセルのみを認識します。それが配置されているセルと、自身の前に3つのセルがあり、それらはそれらの方向を向いています。

アリはフェロモントレイルに反応します。 彼らが蟻塚を失うことがないことは非常に重要です。さもなければ、彼らはそれをどこに置くべきかを見つけずに、歯に食べ物を無限に走り回ることができます フェロモンに関連するいくつかの機能を追加して、食物がどこに、アリの丘がどこにいるかを効果的にナビゲートできるようにしました。



コードの詳細。 しかし、私は遠くから始めます。

まず、atom、agent、refなど、Clojureの概念に精通する必要があります。 それらは変数マネージャーと呼ばれます。 コードは変数の値に直接アクセスするのではなく、仲介者を介してアクセスします。 Atom、agent、refは3種類の仲介者です。



(def atom1 (atom 0)) (def agent1 (agent 0)) (def ref1 (ref 0))
      
      





ここで、atom1、agent1、ref1を定義しました。 すべての初期値は0です。



 (swap! atom1 inc) (prn @atom1) ;->1 (send agent1 inc) (prn @agent1) ;->1 (dosync (alter ref1 inc)) (prn @ref1) ;->1
      
      





ここで、値を1増やすinc関数をatom1、agent1、ref1に渡します。すべての値が1になったことがわかります。



 (reset! atom1 0) (send agent1 (fn [_] 0)) (dosync (ref-set ref1 0))
      
      





ここで、atom1、agent1、ref1の現在の値を0に変更します。



仲介者間の違いは見えませんが、それは構文上のことです。



3つの関数を定義します。

 (defn atom1-change [] (prn "atom1-change") (swap! atom1 inc) (prn @atom1)) (defn agent1-change [] (prn "agent1-change") (send agent1 inc) (prn @agent1)) (defn ref1-change [] (prn "ref1-change") (dosync (alter ref1 inc)) (prn @ref1))
      
      







それらを呼び出します:

 (atom1-change) ;"atom1-change" ;1 (prn @atom1) ;1 (agent1-change) ;"agent1-change" ;0 (prn @agent1) ;1 (ref1-change) ;"ref1-change" ;1 (prn @ref1) ;1
      
      





エージェント(send agent1 inc)の場合、非同期で呼び出されることがわかります。 これにより、エージェントが他のエージェントと区別されます。



遅延機能が必要です。 .Netの標準のスリープを使用して定義し、実際の相互運用性を確認します。

 (defn sleep [ms] (. System.Threading.Thread (Sleep ms)))
      
      







1秒ごとにエージェントの値を増やすスレッドを作成します。

 (defn agent1-thread[x] (sleep 1000) (send *agent* agent1-thread) (prn x) (inc x)) (send agent1 agent1-thread)
      
      





* agent *は現在のエージェントです。

あなたが睡眠を交換して送信すると、恐ろしいことが起こるように思えます(私には思えた)。 結局、sendは再帰的かつ非同期に呼び出され、ストリームは乗算と乗算を行う必要がありますが、これは起こらず、次のように安全に記述できます。



 (defn agent1-thread[x] (send *agent* agent1-thread) (sleep 1000) (prn x) (inc x)) (send agent1 agent1-thread)
      
      





ただし、水中レーキはそうです。 最後の行(inc x)は、エージェントに保存されている値を1増やして返し、エージェントに書き込まれるのはこの最後の戻り値であることに注意してください。



オーバーフローフロースタックスタックでClojureを床に置くには、次の3つの方法があります。



 (reset! atom1 atom1) (dosync (ref-set ref1 ref1)) (send agent1 (fn [_] agent1))
      
      





ここでは、refのアトムとエージェント自体の値として記述します。 これにより、スタックオーバーフローが発生します。



多くの場合、エージェントは、エージェント自体の値を変更せずにタイマーを整理するために使用されます。例:



 (def timer (agent nil) ) (defn on-timer [_] (sleep 1000) (send *agent* on-timer) (prn "tic-tac")) (send timer on-timer)
      
      





prnがnilを返すことは注目に値します。エージェント値に書き込まれるのはこのnileです。

send関数はエージェント自体を返すので、足を踏み入れるのに最適な方法です。



 (def timer (agent nil) ) (defn on-timer [_] (sleep 1000) (prn "tic-tac") (send *agent* on-timer)) (send timer on-timer)
      
      





そしてここでは、どのタイマーが等しいか、そしてClojureの落下によるオーバーフロースタックに等しいかどうかを確認するまで、すべてが大丈夫だと思われます。



次の実例:



 (def agent1 (agent nil)) (def agent2 (agent nil)) (def timer (agent nil)) (def atom1 (atom 0)) (def atom2 (atom 0)) (defn atoms-change-thread1 [_] (reset! atom1 1) (sleep (rand-int 90)) (reset! atom2 2) (sleep (rand-int 90)) (send *agent* atoms-change-thread1) nil) (defn atoms-change-thread2 [_] (reset! atom1 3) (sleep (rand-int 90)) (reset! atom2 4) (sleep (rand-int 90)) (send *agent* atoms-change-thread2) nil) (defn on-timer [_] (prn @atom1 @atom2) (sleep 1000) (send *agent* on-timer) nil) (send agent1 atoms-change-thread1) (send agent2 atoms-change-thread2) (send timer on-timer)
      
      





ここでは、2つのアトムと値を変更する2つのストリームがあります。

最初のストリームは値1と2をアトムに書き込み、2番目のストリームは値3と4を書き込みます。

1秒に1回原子値を表示するタイマーもあります。

出力は次のようになります。



3 4

1 2

1 4

3 2

1 2

3 4

1 4

3 2



さて、最後にトランザクションについてです。 前の例を書き換えますが、アトムの代わりにrefがあります。



 (def agent1 (agent nil)) (def agent2 (agent nil)) (def timer (agent nil)) (def ref1 (ref 0)) (def ref2 (ref 0)) (defn refs-change-thread1 [_] (dosync (ref-set ref1 1) (sleep (rand-int 90)) (ref-set ref2 2) (sleep (rand-int 90))) (send *agent* refs-change-thread1) nil) (defn refs-change-thread2 [_] (dosync (ref-set ref1 3) (sleep (rand-int 90)) (ref-set ref2 4) (sleep (rand-int 90))) (send *agent* refs-change-thread2) nil) (defn on-timer [_] (prn @ref1 @ref2) (sleep 1000) (send *agent* on-timer) nil) (send agent1 refs-change-thread1) (send agent2 refs-change-thread2) (send timer on-timer)
      
      





結論はこのようなものになります

3 4

3 4

1 2

3 4

1 2

3 4

1 2

3 4

3 4

1 2



dosyncを記述する理由が明らかになりました。 これがトランザクション境界の定義です。 参照の変更はトランザクション的に発生します。 この点で、それらは他とは異なります。



refをエージェントに置き換えると、トランザクション性も見られます。 エージェントもSTMで動作し、トランザクション境界はエージェントの値を変更する機能全体です。



 (def agent1 (agent nil)) (def agent2 (agent nil)) (def timer (agent nil)) (def agent3 (agent 0)) (def agent4 (agent 0)) (defn agents-change-thread1 [_] (send agent3 (fn [_] 1)) (sleep (rand-int 90)) (send agent4 (fn [_] 2)) (sleep (rand-int 90)) (send *agent* agents-change-thread1) nil) (defn agents-change-thread2 [_] (send agent3 (fn [_] 3)) (sleep (rand-int 90)) (send agent4 (fn [_] 4)) (sleep (rand-int 90)) (send *agent* agents-change-thread2) nil) (defn on-timer [_] (prn @agent3 @agent4) (sleep 1000) (send *agent* on-timer) nil) (send agent1 agents-change-thread1) (send agent2 agents-change-thread2) (send timer on-timer)
      
      





1 2

3 4

1 2

1 2

3 4

1 2

1 2

1 2



最後に、このすべての後、あなたは蟻塚に戻ることができます。 タスクを大幅に簡素化すると、次のようになります。



 (def v [1 1 0 0 0]) (def world (vec (map (fn [x] (ref x)) v))) (defn place [x] (world x)) (def agent1 (agent 0)) (def agent2 (agent 1)) (def agent-show-world (agent nil)) (defn agent-change [x] (let [old (place x) new-coord (rand-int (count world)) new (place new-coord)] (sleep (rand-int 50)) (if (= @old 1) (do (send *agent* agent-change) (dosync (if (= @new 0) (do (ref-set old 0) (ref-set new 1) new-coord) x))) (prn "agent " *agent* "is out")))) (defn show-world [_] (sleep 1000) (send *agent* show-world) (prn (map (fn [x] (deref x)) world))) (send agent-show-world show-world) (send agent1 agent-change) (send agent2 agent-change)
      
      





おおよその結論は

(1 0 0 1 0)

(0 0 1 1 0)

(1 0 0 1 0)

(0 1 1 0 0)

(0 0 1 1 0)

(1 0 0 1 0)

(0 0 1 1 0)

(0 0 1 1 0)

(1 0 1 0 0)

(1 0 0 1 0)

(0 0 1 0 1)

(0 1 0 1 0)

(1 0 1 0 0)

(1 0 0 1 0)

(0 0 0 1 1)

(0 1 1 0 0)

(1 1 0 0 0)

(1 0 1 0 0)

(0 0 0 1 1)

(1 0 1 0 0)

(1 0 0 0 1)

(1 0 0 1 0)

(0 1 0 0 1)



ここで、ベクトルvを1の最初の2つの要素に定義し、残りはゼロです。 蟻塚のコンテキストでは、ユニットはアリであり、ゼロはフリーセルであると想定しています。 彼らは世界ベクトルを作りました-これは、値がベクトルvの要素であるrefのベクトルです。 値がワールドベクトルの座標である2つのエージェントがあります。 そして、世界のベクトルの周りにユニットを移動させる2つのストリームがあります。 トランザクションの性質により、2つのストリームが同じセルにユニットを同時に書き込むことはありません。



レコードがトランザクションにラップされるだけでなく、読み取りチェックにも注意してください。



 (dosync (if (= @new 0) (do (ref-set old 0) (ref-set new 1) new-coord) x)))
      
      





ランダムに生成された新しい座標が0の場合、つまり 空のセル、古いセル0、新しいセル1に書き込みます。したがって、アリを古い座標から新しい座標に移動します。



トランザクションでレコードのみをラップすると、スレッドセーフになりません。



 (defn agent-change [x] (let [old (place x) new-coord (rand-int (count v)) new (place new-coord)] (sleep (rand-int 50)) (if (= @old 1) (do (send *agent* agent-change) (if (= @new 0) (do (dosync (ref-set old 0) (ref-set new 1)) new-coord) x)) (prn "agent " *agent* "is out"))))
      
      







この関数のバリアントでは、出力は次のようになります。

(0 0 0 1 1)

(1 0 1 0 0)

(0 0 1 0 1)

(1 0 0 0 1)

「Agent」#<Agent @ 549043:4>「is out」

(1 0 0 0 0)

(0 0 1 0 0)

(0 0 1 0 0)

(0 1 0 0 0)

(0 0 0 1 0)

(0 0 1 0 0)



つまり 同じ場所にあるユニットで同時に記録されたストリームは、1つのストリームがこのユニットを新しい場所に移動し、2番目のストリームはそのユニットが残ってカールした場所でそのユニットを見つけられませんでした。 それだけです 彼はアリであり、死にました。 むしろ、2つのアリは一緒に融合しました。



マルチスレッドに関しては、おそらくそれだけです。



グラフィックの実装方法:



 (def ant-vert-bitmap '(0 0 0 0 0 0 0 0 0 2 2 0 0 0 1 1 0 0 0 0 0 0 0 0 0 1 1 0 2 2 2 2 0 1 0 0 0 0 0 0 0 0 0 0 1 0 0 1 2 3 3 2 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 2 2 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 1 5 5 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 4 4 1 0 0 0 1 0 0 0 0 0 0 0 0 0 1 0 0 0 1 1 0 0 0 1 0 1 0 0 0 0 0 0 0 0 0 1 0 1 5 5 1 0 1 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 1 4 4 1 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 1 5 5 5 5 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 5 5 5 5 1 1 1 0 0 1 0 0 0 0 0 0 0 1 1 1 5 4 4 5 1 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 1 1 1 1 0 0 0 0 1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)) (def ant-diag-bitmap '(0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 2 2 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 2 3 2 1 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 1 2 2 1 0 1 0 0 0 0 0 0 0 0 1 0 0 0 1 5 5 1 1 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 4 5 1 0 0 0 0 0 0 0 0 1 1 1 0 1 0 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 1 0 1 1 1 5 1 0 0 0 0 0 0 0 0 0 1 0 0 0 1 1 1 5 4 1 1 0 0 0 0 0 0 0 0 1 0 1 0 1 5 5 5 1 5 1 0 1 1 0 0 0 0 0 0 0 0 0 1 5 5 5 5 5 1 0 0 0 0 1 0 0 0 0 0 0 0 0 1 5 4 5 5 5 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 5 4 4 5 5 1 0 0 1 0 0 0 0 0 0 0 0 0 0 1 5 5 5 5 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0))
      
      





これはアリです。 これらの行列には回転機能と反転機能があります。 次に、これらすべてが.Netビットマップでレンダリングされます。



 (defn render-bitmap [bitmap bit color] (let [bit-pos (positions #{bit} bitmap) rendered-bitmap (Bitmap. bitmaps-dim bitmaps-dim)] (doseq [b bit-pos] (let [dy (quot b bitmaps-dim) dx (rem b bitmaps-dim)] (.SetPixel rendered-bitmap dx dy color))) rendered-bitmap))
      
      







すべての写真がキャッシュされます。 フレームをレンダリングするとき、すべてがキャッシュから取得されます。



さて、Windowsフォームとチャートに関して、コードは次のとおりです。



 (def current-wins (atom nil)) (def win-app (agent nil)) (def winforms-app-inited? (atom false)) (def chart (atom nil)) (def series (atom nil)) (defn get-series [series-name] (first (filter (fn [x] (if (= (. x Name) series-name) true false)) @series))) (defn add-xy [series-name xy] (let [series (get-series series-name)] (when series (.AddXY (.Points series) xy) (when (> (.Count (.Points series)) 500) (.RemoveAt (.Points series) 0))))) (defn create-series [chart] (let [series1 (. chart Series)] (.Add series1 "herb") (.Add series1 "sugar") (.Add series1 "anthill-sugar") (.Add series1 "aphises") (doseq [s series1] (doto s (.set_ChartType SeriesChartType/Spline) (.set_IsVisibleInLegend true) )) (reset! series series1) (doto (get-series "herb") (.set_Color Color/Green) (.set_LegendText "Herb") ) (doto (get-series "sugar") (.set_Color Color/White) (.set_LegendText "Free sugar") ) (doto (get-series "anthill-sugar") (.set_Color (Color/FromArgb 255 115 61 0)) (.set_LegendText "Anthill sugar") ) (doto (get-series "aphises") (.set_Color (ControlPaint/Light Color/Green)) (.set_LegendText "Aphises") ))) (defn chart-update[chart] (add-xy "anthill-sugar" @world-time (anthill-sugar-calc)) (add-xy "herb" @world-time (herb-calc)) (add-xy "sugar" @world-time (free-sugar-calc)) (add-xy "aphises" @world-time (count @aphises)) (let [chart-areas (. chart ChartAreas) chart-area (first chart-areas) axis-x (. chart-area AxisX)] (doto axis-x (.set_Minimum (if (> @world-time 500) (- @world-time 500) 0)) (.set_Maximum (if (> @world-time 500) @world-time 500))))) (defn create-form [] (let [form (Form.) panel (Panel.) animation-timer (Timer.) world-timer (Timer.) chart1 (Chart.) series1 (. chart1 Series)] (doto chart1 (.set_Name "chart1") (.set_Location (new Point size 0)) (.set_Size (Size. size size)) (.set_BackColor (ControlPaint/Light bgcolor))) (.Add (. chart1 ChartAreas) "MainChartArea") (.Add (. chart1 Legends) "Legend") (doto (first (. chart1 ChartAreas)) (.set_BackColor bgcolor)) (doto (first (. chart1 Legends)) (.set_BackColor bgcolor)) (create-series chart1) (reset! chart chart1) (chart-update chart1) (let [chart-areas (. chart1 ChartAreas) chart-area (first chart-areas) axis-x (. chart-area AxisX) axis-y (. chart-area AxisY)] (doto axis-x (.set_IsStartedFromZero true)) (doto axis-y (.set_IsStartedFromZero true))) (doto panel (.set_Location (new Point 0 0)) (.set_Name "panel1") (.set_Size (Size. size size)) (.add_Click (gen-delegate EventHandler [sender args] (when (= (.Button args) MouseButtons/Right) (swap! show-lens? (fn [x] (not x)))) (when (= (.Button args) MouseButtons/Left) (let [mouse-x (@mouse-pos 0) mouse-y (@mouse-pos 1) x (/ mouse-x scale) y (/ mouse-y scale) p (place [xy])] (prn [xy] @p) (.Focus panel))))) (.add_MouseMove (gen-delegate MouseEventHandler [sender args] (reset! mouse-pos [ (* (quot (.X args) scale) scale) (* (quot (.Y args) scale) scale)]))) (.add_MouseWheel (gen-delegate MouseEventHandler [sender args] (let [f (fn [x] (let [new-sleep (+ x (* 50 (/ (.Delta args) 120)))] (if (> new-sleep 0) new-sleep 0)))] (swap! ant-sleep-ms f) (swap! ladybug-sleep-ms f) (swap! aphis-sleep-ms f) (prn @ant-sleep-ms))))) (doto animation-timer (.set_Interval animation-sleep-ms) (.set_Enabled true) (.add_Tick (gen-delegate EventHandler [sender args] (do (when @buf-graph (.Render (@buf-graph 0) (@buf-graph 1))) (reset! rectangles-in-cells []) (reset! rendered-bitmaps []) (let [v (vec (for [x (range dim) y (range dim)] @(place [xy])))] (dorun (for [x (range dim) y (range dim)] (render-place (v (+ (* x dim) y)) xy))) (reset! buf-graph (render panel)) (when @show-lens? (reset! buf-graph (render-lens)))))))) (doto world-timer (.set_Interval 5000) (.set_Enabled true) (.add_Tick (gen-delegate EventHandler [sender args] (swap! world-time inc) (chart-update chart1)))) (doto (.Controls form) (.Add panel) (.Add chart1)) (doto form (.set_ClientSize (Size. (* size 2) size)) (.set_Text "Super Ants")) form)) (defn init-winforms-app [] (when-not @winforms-app-inited? (Application/EnableVisualStyles) (Application/SetCompatibleTextRenderingDefault false) (reset! winforms-app-inited? true))) (defn start-gui [x] (init-winforms-app) (reset! current-wins (create-form)) (Application/Run @current-wins))
      
      







退屈ではなかったと思います。



ご清聴ありがとうございました。



All Articles