📜 ⬆️ ⬇️

Super anthill on ClojureCLR

Clojure's multithreading has been taken to a new level of development, because it implements the STM (The software transactional memory system) memory change transactions. As a demonstration, Rich Hickey (the divine author of Clojure) and David Miller (the man who wrote the Clojure implementation under .Net) offer the ants program, which simulates an anthill. Each ant lives there in a separate stream. Ants run in the cells of the common field, collect food, carry it to the anthill and do not conflict with each other.

The result of my exercises with this program is what I want to bring to the general review. I hope the article will be useful to those who are starting to get acquainted with Clojure on the .Net platform.

First links:

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
')
What has been done by me.
1. Graphics . In the original program, the ant is only a dash. My insects are more natural.
2. New heroes . In addition to the ants I have implemented aphids and ladybugs.
3. The interaction of heroes . The grass grows, the aphid eats grass, produces sugar and lays eggs (multiplies). Ladybirds eat aphids and eggs. Ants avoid obstacles on the way. (Even the ants have to drag aphids on the grass and stop the ladybugs from devouring the aphids, but this is not yet implemented.)
3. Behavior . The behavior of the ants became more complicated, they became smarter than in the original. Added behavior of new heroes.
4. Charts . Visible dynamics of the processes on the graphs. Growth of grass, the number of aphids, the amount of sugar in the anthill and in the surrounding space.
5. A primitive magnifier can look at the details. Right mouse button.
6. Mouse . When you click on the cell field in the console displays the information contained in the cell. Mouse wheel, you can change the speed of movement of insects.

By changing the initialization parameters, you can get different scenarios for the development of this ecosystem. The faster the grass grows, the faster the aphids reproduce and the more sugar appears on the field. Grass does not grow on sugar; therefore, the vigorous activity of aphids affects the growth of grass. Different ratios of insects at launch (how many aphids, how many ladybirds, how many ants) determine the new dynamics.

Each insect sees only four cells: the one on which it is located, and three cells in front of itself, and guides them.
Ants respond to the pheromone trace. It is very important for them not to lose an anthill, otherwise they can run endlessly with food “in their mouths”, not finding where to put it. I added a few pheromone features to them so that they can efficiently navigate where the food is, where the anthill is.

Closer to the code. But I'll start from afar.
First you need to get acquainted with such concepts in Clojure as atom, agent and ref. They can be called variable managers. The code does not directly refer to the value of the variable, but through an intermediary. Atom, agent and ref are three types of intermediary.

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

Here we defined atom1, agent1 and ref1. The initial value of all is 0.

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

Here we pass the function inc to atom1, agent1 and ref1 which increases the value by 1 and see that the values ​​everywhere become 1.

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

Here we change the current values ​​of atom1, agent1 and ref1 to 0.

While the difference between intermediaries is not visible, it is only syntactic.

We define three functions:
 (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)) 


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

We see that in the case of an agent (send agent1 inc) it is called asynchronously. This agent is different from the rest.

We will need a delay function. We define it using the standard Sleep from .Net and see the interop in action.
 (defn sleep [ms] (. System.Threading.Thread (Sleep ms))) 


Create a stream that will increase the value of the agent every second.
 (defn agent1-thread[x] (sleep 1000) (send *agent* agent1-thread) (prn x) (inc x)) (send agent1 agent1-thread) 

* agent * is the current agent.
It may seem (it seemed to me) that if you swap sleep and send, something terrible will happen. After all, send is called recursively and asynchronously, the stream should multiply and multiply, but this does not happen and you can safely write like this:

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

Underwater rake, however, is. Notice that the last line (inc x) returns the value stored in the agent, incremented by 1, and it is this last return value that is written to the agent.

Now there are three ways to put Clojure on the floor with an overflow stack overflou:

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

Here we write the values ​​of the atom of the ref and the agent of them. This leads to a stack of overflow.

Often agents are used to organize timers without changing the value of the agents themselves, for example:

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

It is worth noting here that prn returns nil and this particular nil is written to the value of the agent.
Since the send function returns the agent itself, a great way to shoot yourself in the foot:

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

And here, it seems, even everything will be all right, until you want to see what the timer is equal to, and it is equal to the stack of overflou with the fall of Clojure.

The following illustrative example:

 (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) 

Here we have two atoms and two streams that change their values.
The first stream writes values ​​1 and 2 to atoms, the second stream writes values ​​3 and 4.
There is also a timer, which once per second displays the values ​​of atoms.
The output will be something like this:

3 4
12
14
3 2
12
3 4
14
3 2

Now, finally about transactions. Let's rewrite the previous example, but instead of atoms we will have refs.

 (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) 

The output will be something like this
3 4
3 4
12
3 4
12
3 4
12
3 4
3 4
12

Now it becomes clear why to write dosync. This is the definition of transaction boundaries. Reflex changes are transactional. This is how they differ from the rest.

Although, if we replace the refs with the agents, we will also see transactionality. Agents also operate in the STM and the scope of the transaction is the entire function of changing the value of the agent.

 (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) 

12
3 4
12
12
3 4
12
12
12

Finally, after all this, you can return to the anthill. If you greatly simplify the task, then it will look like this:

 (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) 

The approximate output would be
(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)

Here we defined the vector v in it the first two elements of the unit, and the rest - the zeros. In the context of an anthill, we will assume that units are ants, and zeros are free cells. Made a vector world - this is a vector of refs whose values ​​are the elements of the vector v. We have two agents whose values ​​are the coordinates of the world vector. And there are two streams moving units along the vector world. Due to the transactionality, the two streams will not simultaneously record a unit in the same cell.

Note that the transaction wraps not only the record, but also the read-check:

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

If the new, randomly generated, coordinate is 0, i.e. an empty cell, then we write to the old cell 0, and to the new one. Thus we move the ant from the old coordinate to the new one.

If you only wrap a record in a transaction, it will not be thread-safe.

 (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")))) 


With this version of the function, the output will be something like this:
(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)

Those. the streams recorded simultaneously a unit in the same place, then one stream stole this unit in a new place, and the second did not find its unit in the place where it left and curled up. Everything. There was an ant and was gone. Rather, two ants merged into one.

As for multithreading, this is probably all.

How graphics are implemented:

 (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)) 

This is an ant. There are functions of rotation and flip of these matrices. Further, all this is rendered in .Net Bitmap:

 (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)) 


All images are cached. When drawing a frame, everything is taken from the cache.

Well, as for the Windows Form and Chart, the code is:

 (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)) 


I hope it was not boring.

Thanks for attention.

Source: https://habr.com/ru/post/256017/


All Articles