Ping Ring del 7: Clojure m/Agenter


fredag 17. september 2010 Diverse prosjekter Ping Ring Clojure Samtidighet

Dette er del 7 i artikkelserien Ping Ring hvor jeg implementerer et og samme program i et utall ulike programmeringsspråk - for å se om det er noe å lære gjennom å gjøre det. Introduksjonen kan du lese her.

Clojure har noe som kalles agents. Jeg skjønte til å begynne med lite av hvordan jeg burde bruke dem, men etter å ha sett Rich Hickeys to og en halv timers lange demonstrasjon av concurrency i Clojure (anbefales) følte jeg meg klar for å gjøre et forsøk.

Mye av koden nedenfor er lik den du finner i del 6. En detalj er at jeg har endret hvordan jeg samler opp kommandolinje-argumentene. I orginalversjonen lagret jeg dem i et struct-map, og sendte dem rundt til alle funksjonene. Denne gangen har jeg bare lagret dem i fire ulike verdier definert globalt i namespacet, og bruker dem direkte i funksjonene (ikke helt som Thomas foreslo altså).

Matrix_Agents

Hovedforskjellen er derimot hvordan jeg har implementert den tråden som skal aktivere alarmen om det ikke kommer pings innenfor et visst tidsrom. Her har jeg nå brukt en agent, definert i linje 26. Agenten har en verdi – i utgangspunktet 0 for denne agenten – og verdien kan endres gjennom å sende den en fuksjon. Funksjonen jeg sender agenten (ved oppstart) er check-delay, definert i linje 32. Innparameter til funksjonen er alltid nå-verdien til agenten. Resultatet av funksjonen vil bli den nye verdien.

Alerter-agenten fungerer slik at den først venter så lenge som det er akseptabelt å ikke motta ping. Deretter sjekker den om det har ankommet en ping. Til dette bruker jeg et atom – en spesiell referansetype som egner seg for samtidighet – som er definert i linje 9, og som settes av listen-for-pings når en ping har blitt mottatt. Jeg har altså gått bort fra å bruke tidspunkt for når ping ble mottatt, og har nå en løsning som ligner mer på den jeg lagde i Erlang – bare i et mere behagelig språk!

Hvis ping var mottatt resetter agent-funksjonen atomet til false, og returnerer 0 (ingen alarmer aktivert). Hvis ping ikke var mottatt vil funksjonen trigge alarmen, og da bruker den agent-verdien sin til å beregne hvor mange sekunder det har gått siden sist ping (antall alarmer på rad ganger antall sekunder forsinkelse tillatt).

Men før funksjonen gjør dette – i linje 35 – køer den opp et nytt kall til seg selv. *agent* er nemlig en referanse til den agenten funksjonen kjører på. Agenter er alltid synkroniserte, dvs. at et nytt kall til agenten ikke vil starte før det forrige kallet er avsluttet. Dermed fungerer dette altså nærmest som en uendelig rekursjon.

Jeg vet ikke om jeg helt klarte å formidle hvordan agenten fungerte, men tar du en god titt på koden i tillegg burde det hele bli klarere:

1 (use 'clojure.contrib.server-socket 'clojure.contrib.duck-streams)
2 (import '(java.net Socket))
3
4 (def this-port    (Integer. (nth *command-line-args* 0)))
5 (def other-port   (Integer. (nth *command-line-args* 1)))
6 (def max-delay    (Integer. (nth *command-line-args* 2)))
7 (def initial-ping (Boolean. (nth *command-line-args* 3)))
8
9 (def has-received-ping? (atom false))
10
11 (defn ping []
12       (future (Thread/sleep 1000)
13               (try (spit
14                      (Socket. "127.0.0.1"  other-port) 
15                      (str     "PING from " this-port))
16                    (catch Exception e
17                           (println "*** Failed sending ping!")))))
18
19 (defn listen-for-pings []
20       (create-server this-port
21                      (fn [in-stream _]
22                          (reset! has-received-ping? true)
23                          (println "Received" (slurp* in-stream))
24                          (ping))))
25
26 (def alerter (agent 0)) ; value is number of alerts in a row
27
28 (defn send-alert [delay]
29       (println "*** ALERT, RING BROKEN! No ping in" delay "seconds.")
30       (ping))
31
32 (defn check-delay [alert-count]
33       (let [new-count (inc alert-count)]
34         (Thread/sleep (* max-delay 1000))
35         (send-off *agent* check-delay) ; queue another check
36         (if @has-received-ping?
37           (do (reset! has-received-ping? false) 0)
38           (do (send-alert (* max-delay new-count)) new-count))))
39
40 (defn main []
41      (println (format "**Clojure Ring Server (with Agents) (%s)" 
42                       this-port))
43      (when initial-ping (ping))
44      (send-off alerter check-delay)
45      (listen-for-pings))
46
47 (main)

Alt i alt synes jeg denne løsningen ble hakket mer elegant enn den forrige. Jeg slipper å rote med datoer, og den er faktisk også mer presis i når den utløser alarmen. For rasjonalet bak å bruke agenter må jeg henvise deg til Rick Hickey eller andre som har blogget om temaet. Dette var bare en liten demo av en av måtene de kan brukes på. Jeg lover derimot mer om samtidighet i Clojure i fremtidige blogposter.

Tidligere i serien: Introduksjon | Del 2 (C#) | Del 3 (Ruby) | Del 4 (Boo) | Del 5 (Erlang) | Del 6 (Clojure).

Kildekoden fra denne blogposten er tilgjengelig på Github. Der står du fritt til å forgrene løsningen og gjøre egne modifikasjoner om du ønsker det (for å illustrere et poeng eller lignende). Som alt annet på bloggen er koden lisensiert under Creative Commons.


comments powered by Disqus