1. Asynchronní programování v Clojure s využitím knihovny core.async (pokračování)
Knihovna core.async, s níž jsme se seznámili v předchozí části seriálu o programovacím jazyku Clojure, nabízí programátorům kromě takzvaných kanálů a „go bloků“ i další funkce a makra, s nimiž se seznámíme dnes. Nejprve si řekneme více informací o funkci nazvané alts!, o níž jsme se již ve stručnosti zmínili minule. Připomeňme si, že této funkci se předá vektor kanálů a ve výchozím nastavení tato funkce náhodně vybere kanál, ze kterého bude číst. Bude se jednat o první kanál nabízející hodnotu, takže tato funkce „zaparkuje“ (zastaví) provádění go bloku jen tehdy, pokud jsou všechny kanály prázdné. Náhodný výběr neprázdného kanálu je zde důležitý, protože pokud by se kanály neustále vybíraly podle uvedeného pořadí, mohlo by docházet k efektu, který je znám pod termínem „vyhladovění“ (starvation) u těch kanálů, které se ve vektoru nachází na posledních pozicích.
Díky existenci funkce alts! je možné implementovat následující topologii propojení různých go bloků s využitím několika kanálů. Samozřejmě je možné v případě potřeby vytvořit i složitější topologii, k čemuž může dopomoci i funkce pipe:
+-----------+ | producent | +------+ | #1 |... >!.....|kanál1}....... | go block | +------+ : +-----------+ : : +-----------+ : +-----------+ | producent | +------+ : | konzument | | #2 |... >! ... |kanál2} ... alts! ... | | | go block | +------+ : | go block | +-----------+ : +-----------+ : +-----------+ : | producent | +------+ : | #3 |... >!.....|kanál3}......: | go block | +------+ +-----------+
Poznámka: termínem „zaparkování“ se v kontextu knihovny clojure.async myslí stav, kdy je běh nějakého go bloku pozastaven a příslušné vlákno je uvolněno pro další použití. Jednou z předností této knihovny je totiž fakt, že se pro go bloky používá pouze určitý počet vláken, které se mezi bloky sdílí a dochází tak k efektivnějšímu využití vláken. Připomene nám to diagram z předchozí části ukazující rozdíl mezi klasickým multivláknovým přístupem a přístupem použitým v knihovně core.async:
Klasický přístup:
Vlákno 1........worker1........worker1........worker1........worker1 Vlákno 2........worker2........worker2........worker2........worker2 Vlákno 3........worker3........worker3........worker3........worker3 Vlákno 4........worker4........worker4........worker4........worker4
core.async:
Vlákno 1.worker1.worker4.worker1.worker3...worker1.worker3.worker2.worker4 Vlákno 2..worker2.worker3..worker1...worker4.worker2......worker1.worker3
Pojďme si nyní vyzkoušet, zda je tvrzení o náhodném výběru kanálu pravdivé. Napoví nám příklad se třemi producenty, z nichž každý ve svém go bloku zapisuje prvky do svého kanálu channel1, channel2 či channel3 (zápis se teoreticky nikdy nezastaví, protože prvky se generují nekonečnými lazy sekvencemi). Konzument je zde jeden – náhodně vybírá kanál, ze kterého se má prvek přečíst a počítá si statistiku, ze kterého kanálu bylo čtení provedeno. Konzumenta jsem napsal schválně poměrně rozvláčným způsobem, v praxi by ale šel naprogramovat lépe (například s použitím map, kde by klíči byly právě kanály):
; z kanalu se celkem precte 1000 hodnot (go ; citace pouzite pro vyhodnoceni statistiky cteni (let [counter1 (atom 0) counter2 (atom 0) counter3 (atom 0)] ; precteme 1000 hodnot (dotimes [n 1000] ;(<! (timeout 1)) ; zkuste odkomentovat (let [[item channel] (alts! [channel1 channel2 channel3])] ; vyhodnoceni, ze ktereho kanalu se cteni provedlo (condp = channel channel1 (swap! counter1 inc) channel2 (swap! counter2 inc) channel3 (swap! counter3 inc)))) ; vypis ziskane statistiky (println "Channel #1 read " @counter1 "times") (println "Channel #2 read " @counter2 "times") (println "Channel #3 read " @counter3 "times")))
Úplný zdrojový kód celého příkladu pak vypadá následovně:
(ns async6.core (:gen-class)) ; nacteme vsechny potrebne funkce, makra a symboly z knihovny ; (schvalne se nenacitaji vsechny funkce, protoze by jejich jmena ; prepsala takove zakladni funkce, jako je map apod.) (require '[clojure.core.async :refer (go chan >! <! timeout alts!)]) (defn wait "Pozastaveni hlavniho vlakna - simulace interaktivni prace." [] (Thread/sleep 5000)) (defn -main "Tato funkce se spusti automaticky nastrojem Leiningen." [& args] (println "Start") ; vytvorime kanaly (let [channel1 (chan) channel2 (chan) channel3 (chan)] ; kontinualni posilani zprav do trech kanalu v trojici asynchronnich bloku (go (doseq [i (range)] (>! channel1 i))) (go (doseq [i (range)] (>! channel2 i))) (go (doseq [i (range)] (>! channel3 i))) (println "producers started") ; z kanalu se celkem precte 1000 hodnot (go ; citace pouzite pro vyhodnoceni statistiky cteni (let [counter1 (atom 0) counter2 (atom 0) counter3 (atom 0)] ; precteme 1000 hodnot (dotimes [n 1000] ;(<! (timeout 1)) ; zkuste odkomentovat (let [[item channel] (alts! [channel1 channel2 channel3])] ; vyhodnoceni, ze ktereho kanalu se cteni provedlo (condp = channel channel1 (swap! counter1 inc) channel2 (swap! counter2 inc) channel3 (swap! counter3 inc)))) ; vypis ziskane statistiky (println "Channel #1 read " @counter1 "times") (println "Channel #2 read " @counter2 "times") (println "Channel #3 read " @counter3 "times"))) ; chvili pockame (wait) (println "Finish") (System/exit 0)))
Pokud vás zajímá statistika pro tisíc prvků, podívejme se na výstup ze tří běhů:
Start producers started Channel #1 read 300 times Channel #2 read 350 times Channel #3 read 350 times Finish
Start producers started Channel #1 read 341 times Channel #2 read 331 times Channel #3 read 328 times Finish
Start producers started Channel #1 read 322 times Channel #2 read 327 times Channel #3 read 351 times Finish
Z výsledků je patrné, že se skutečně kanály vybíraly náhodně, protože počet výběrů každého kanálu se blíží k hodnotě 1000/3.
2. Určení priorit při použití funkce alts!
Ve skutečnosti jsou však možnosti funkce alts! ještě větší. Pokud se přidá nepovinný parametr :priority s hodnotou nastavenou na true (či na libovolnou hodnotu, která není nepravdivá), bude se kanál pro čtení vybírat v tom pořadí, v jakém je zadán ve vektoru. Pokud tedy bude první kanál neustále plný, bude se využívat pouze ten a u ostatních kanálů dojde k jevu známému pod termínem „starvation“ (hladovění). Podívejme se na druhou verzi konzumenta:
; z kanalu se celkem precte 1000 hodnot (go ; citace pouzite pro vyhodnoceni statistiky cteni (let [counter1 (atom 0) counter2 (atom 0) counter3 (atom 0)] ; precteme 1000 hodnot (dotimes [n 1000] ;(<! (timeout 1)) ; zkuste odkomentovat ; -------------------------------------------------------------------------- (let [[item channel] (alts! [channel1 channel2 channel3] :priority true)] ; -------------------------------------------------------------------------- ; vyhodnoceni, ze ktereho kanalu se cteni provedlo (condp = channel channel1 (swap! counter1 inc) channel2 (swap! counter2 inc) channel3 (swap! counter3 inc)))) ; vypis ziskane statistiky (println "Channel #1 read " @counter1 "times") (println "Channel #2 read " @counter2 "times") (println "Channel #3 read " @counter3 "times")))
Výsledky běhu pro takto změněného konzumenta ukazují, že první kanál je skutečně využívám mnohem častěji, než kanál druhý a ten je využíván častěji, než kanál třetí (vzhledem k tomu, že je konzument velmi rychlý, nemusí být v prvním kanále žádný prvek a proto také se v některých případech provádí čtení ze druhého a přinejhorším i ze třetího kanálu):
Start producers started Channel #1 read 647 times Channel #2 read 243 times Channel #3 read 110 times Finish
Start producers started Channel #1 read 623 times Channel #2 read 299 times Channel #3 read 78 times Finish
Start producers started Channel #1 read 728 times Channel #2 read 217 times Channel #3 read 55 times Finish
Můžeme si položit otázku, co se stane ve chvíli, kdy je producent používající první kanál rychlejší než konzument. Bude se vůbec provádět čtení z dalších kanálů? To lze snadno zajistit zpomalením konzumenta (odkomentováním vyznačeného řádku kódu):
; z kanalu se celkem precte 1000 hodnot (go ; citace pouzite pro vyhodnoceni statistiky cteni (let [counter1 (atom 0) counter2 (atom 0) counter3 (atom 0)] ; precteme 1000 hodnot (dotimes [n 1000] (<! (timeout 1)) ; pozdrzeni konzumenta ; -------------------------------------------------------------------------- (let [[item channel] (alts! [channel1 channel2 channel3] :priority true)] ; -------------------------------------------------------------------------- ; vyhodnoceni, ze ktereho kanalu se cteni provedlo (condp = channel channel1 (swap! counter1 inc) channel2 (swap! counter2 inc) channel3 (swap! counter3 inc)))) ; vypis ziskane statistiky (println "Channel #1 read " @counter1 "times") (println "Channel #2 read " @counter2 "times") (println "Channel #3 read " @counter3 "times")))
Výsledek mluví za vše, proto ho nemá cenu široce komentovat (aneb – starvation v praxi):
Start producers started Channel #1 read 1000 times Channel #2 read 0 times Channel #3 read 0 times Finish
3. První demonstrační příklad: použití funkce alts! s určením priorit
Následuje výpis úplného zdrojového kódu dnešního prvního demonstračního příkladu (celý projekt naleznete na GitHubu, konkrétně na adrese https://github.com/tisnik/clojure-examples/tree/master/async6):
(ns async6.core (:gen-class)) ; nacteme vsechny potrebne funkce, makra a symboly z knihovny ; (schvalne se nenacitaji vsechny funkce, protoze by jejich jmena ; prepsala takove zakladni funkce, jako je map apod.) (require '[clojure.core.async :refer (go chan >! <! timeout alts!)]) (defn wait "Pozastaveni hlavniho vlakna - simulace interaktivni prace." [] (Thread/sleep 5000)) (defn -main "Tato funkce se spusti automaticky nastrojem Leiningen." [& args] (println "Start") ; vytvorime kanaly (let [channel1 (chan) channel2 (chan) channel3 (chan)] ; kontinualni posilani zprav do trech kanalu v trojici asynchronnich bloku (go (doseq [i (range)] (>! channel1 i))) (go (doseq [i (range)] (>! channel2 i))) (go (doseq [i (range)] (>! channel3 i))) (println "producers started") ; z kanalu se celkem precte 1000 hodnot (go ; citace pouzite pro vyhodnoceni statistiky cteni (let [counter1 (atom 0) counter2 (atom 0) counter3 (atom 0)] ; precteme 1000 hodnot (dotimes [n 1000] ;(<! (timeout 1)) ; zkuste odkomentovat (let [[item channel] (alts! [channel1 channel2 channel3] :priority true)] ; vyhodnoceni, ze ktereho kanalu se cteni provedlo (condp = channel channel1 (swap! counter1 inc) channel2 (swap! counter2 inc) channel3 (swap! counter3 inc)))) ; vypis ziskane statistiky (println "Channel #1 read " @counter1 "times") (println "Channel #2 read " @counter2 "times") (println "Channel #3 read " @counter3 "times"))) ; chvili pockame (wait) (println "Finish") (System/exit 0)))
Projektový soubor:
(defproject async6 "0.1.0-SNAPSHOT" :description "FIXME: write description" :url "http://example.com/FIXME" :license {:name "Eclipse Public License" :url "http://www.eclipse.org/legal/epl-v10.html"} :dependencies [[org.clojure/clojure "1.7.0"] [org.clojure/core.async "0.2.374"]] :main ^:skip-aot async6.core :target-path "target/%s" :profiles {:uberjar {:aot :all}})
4. Vytvoření kanálu s vlastnostmi bufferu (fronty)
Kanály, které jsme až doposud pro komunikaci mezi go bloky používali, mohly obsahovat pouze jediný prvek, takže se jejich chování dá shrnout takto:
- Zápis do prázdného kanálu je neblokující operace (kanál se zápisem zaplní, další zápis již tedy bude blokující, pokud nedojde ke čtení).
- Zápis do neprázdného kanálu je blokující operace.
- Čtení z prázdného kanálu je blokující operace (počká se na další zápis).
- Čtení z neprázdného kanálu je neblokující operace (kanál se vyprázdní).
Docházíme tedy k již známému schématu:
+-----------+ +-----------+ | producent | +-----+ | konzument | | |... >! ... |kanál} ... <! ... | | | go block | +-----+ | go block | +-----------+ +-----------+
Kanál však může mít i větší kapacitu – potom vlastně pracuje jako buffer či fronta, což znamená, že i větší počet zápisů nemusí být nutně blokující operace:
+-----------+ +-----------+ | producent | +------------+ | konzument | | |... >! ... |kanál+fronta} ... <! ... | | | go block | +------------+ | go block | +-----------+ +-----------+
Jak se takový kanál vytvoří? Ve skutečnosti velmi snadno, protože stačí jeho kapacitu uvést už při konstrukci kanálu nepovinným parametrem předaným do funkce chan:
(let [channelX (chan 42)] ... ... ... )
5. Druhý demonstrační příklad: práce s kanálem s nenulovou kapacitou
V dalším demonstračním příkladu budeme stále používat tři producenty, podobně jako v příkladu prvním, ovšem jeden producent bude zapisovat do kanálu s větší kapacitou, zatímco ostatní dva producenti se budou muset spokojit s normálním kanálem bez bufferu:
; vytvorime kanaly (let [channel1 (chan) channel2 (chan 100) ; tento kanal ma prirazen buffer o velikosti 100 prvku channel3 (chan)] ... ... ... )
Co to bude znamenat v praxi? Velmi pravděpodobně dojde k tomu, že právě tento kanál bude při čtení konzumentem v daný okamžik neprázdný a bude tak vybírán častěji. Můžeme se o tom přesvědčit (konzument nepoužívá priority):
Start producers started Channel #1 read 206 times Channel #2 read 573 times Channel #3 read 221 times Finish
Start producers started Channel #1 read 140 times Channel #2 read 728 times Channel #3 read 132 times Finish
Start producers started Channel #1 read 191 times Channel #2 read 599 times Channel #3 read 210 times Finish
Skutečně – ze druhého kanálu je čtení prováděno výrazně častěji, než z prvního či třetího kanálu.
Podívejme se nyní na úplný zdrojový kód tohoto demonstračního příkladu:
(ns async7.core (:gen-class)) ; nacteme vsechny potrebne funkce, makra a symboly z knihovny ; (schvalne se nenacitaji vsechny funkce, protoze by jejich jmena ; prepsala takove zakladni funkce, jako je map apod.) (require '[clojure.core.async :refer (go chan >! <! timeout alts!)]) (defn wait "Pozastaveni hlavniho vlakna - simulace interaktivni prace." [] (Thread/sleep 5000)) (defn -main "Tato funkce se spusti automaticky nastrojem Leiningen." [& args] (println "Start") ; vytvorime kanaly (let [channel1 (chan) channel2 (chan 100) ; tento kanal ma prirazen buffer o velikosti 100 prvku channel3 (chan)] ; kontinualni posilani zprav do trech kanalu v trojici asynchronnich bloku (go (doseq [i (range)] (>! channel1 i))) (go (doseq [i (range)] (>! channel2 i))) (go (doseq [i (range)] (>! channel3 i))) (println "producers started") ; z kanalu se celkem precte 1000 hodnot (go ; citace pouzite pro vyhodnoceni statistiky cteni (let [counter1 (atom 0) counter2 (atom 0) counter3 (atom 0)] ; precteme 1000 hodnot (dotimes [n 1000] ;(<! (timeout 1)) ; zkuste odkomentovat (let [[item channel] (alts! [channel1 channel2 channel3])] ; vyhodnoceni, ze ktereho kanalu se cteni provedlo (condp = channel channel1 (swap! counter1 inc) ; s velkou pravdepodobnosti se prvek precte z tohoto kanalu ; (protoze ma data 'predpripravena') channel2 (swap! counter2 inc) channel3 (swap! counter3 inc)))) ; vypis ziskane statistiky (println "Channel #1 read " @counter1 "times") (println "Channel #2 read " @counter2 "times") (println "Channel #3 read " @counter3 "times"))) ; chvili pockame (wait) (println "Finish") (System/exit 0)))
Projektový soubor:
(defproject async7 "0.1.0-SNAPSHOT" :description "FIXME: write description" :url "http://example.com/FIXME" :license {:name "Eclipse Public License" :url "http://www.eclipse.org/legal/epl-v10.html"} :dependencies [[org.clojure/clojure "1.7.0"] [org.clojure/core.async "0.2.374"]] :main ^:skip-aot async7.core :target-path "target/%s" :profiles {:uberjar {:aot :all}})
6. Kanál s „dropping bufferem“
V některých případech může být výhodné používat kanál s nastavenou (nenulovou) kapacitou, ovšem s modifikovaným chováním – ve chvíli, kdy je již buffer kanálu zaplněn, bude další zápis do něj ignorován (a bude tedy za všech okolností neblokující, ovšem na úkor ztráty dat). Příkladem může být kanál naplňovaný událostmi z GUI – většinou nemá smysl se snažit provést naprosto všechny operace, které uživatel zadal (například nervózním poklepáváním na tlačítko OK). V tomto případě pomůže použití kanálu s bufferem typu dropping buffer. Název tohoto bufferu naznačuje jeho funkci: pokud je kanál plný, další zápisy do něj budou ignorovány, o čemž se producent samozřejmě dozví (přes návratovou hodnotu funkce typu „put“).
+-----------+ +-----------+ | producent | +------------+ | konzument | | |... >!.....|kanál+fronta} ... <! ... | | | go block | : +------------+ | go block | +-----------+ : +-----------+ : : v /dev/null
Kanál s takovýmto chováním se nastavuje následujícím způsobem:
; vytvorime kanal s dropping bufferem o zadane kapacite (let [channel (chan (dropping-buffer 10))] ... ... ... )
Poznámka: povšimněte si, že funkce dropping-buffer je parametrem funkce chan. Je častou chybou se pokoušet vytvořit kanál takto:
; ŠPATNĚ!!! vytvorime kanal s dropping bufferem o zadane kapacite (let [channel (dropping-buffer 10)] ... ... ... )
Poznámka 2: vyzkoušejte si ve smyčce REPL rozdíl mezi posledními dvěma příkazy (druhý příkaz vytváří buffer použitý v běžné frontě):
(require '[clojure.core.async :refer (buffer dropping-buffer unblocking-buffer?)]) (unblocking-buffer? (buffer 10)) (unblocking-buffer? (dropping-buffer 10))
7. Třetí demonstrační příklad: práce s kanálem s „dropping bufferem“
Předchozí demonstrační příklad nyní přepíšeme tak, že se v něm bude nacházet jediný producent a jediný konzument. Pro komunikaci mezi producentem a konzumentem se bude používat kanál s přidruženým dropping bufferem. Producent vypadá takto:
; poslani zprav do kanalu, celkem 1000 hodnot od 0 do 999 (go (doseq [i (range 0 1000)] (>! channel i)) (close! channel)))
Konzument je implementován takovým způsobem, že si ve smyčce vytváří vektor s přečtenými daty (smyčka je představována dvojicí loop-recur):
; cteni zprav z kanalu (to zahajime drive, at je jistota ; ze zapisy neskonci moc brzo) (go (loop [result []] (<! (timeout 1)) (let [item (<! channel)] ; pokud je kanal zavreny, vrati se nil (if item ; v pripade, ze se prvek precetl (recur (conj result item)) ; prida se do kolekce (println result))))) ; jinak koncime
Podívejme se na výstup tohoto příkladu, na němž se přesně ukazuje chování dropping bufferu ve chvíli, kdy producent generuje větší množství dat, než může konzument zpracovat. Prvních deset položek (jde o kapacitu bufferu) se zapíše a zpracuje, potom však konzument nebude dostatečně rychle číst data a tudíž dojde k jejich ztrátě („drop“). Výsledek může vypadat takto (jedenáctou položku konzument ještě zpracoval, dvanáctou už nestihl, dále je situace jen horší):
Start consumer started producer started [0 1 2 3 4 5 6 7 8 9 10 15 32 67 102 129 139 200 260 318 378 448 510 576 658 749 839 852 943] Finish
Start consumer started producer started [0 1 2 3 4 5 6 7 8 9 10 25 126 207 305 386 516 607 718 857 993] Finish
Start consumer started producer started [0 1 2 3 4 5 6 7 8 9 10 39 84 118 136 217 274 352 407 484 536 603 667 742 821 912] Finish
Úplný zdrojový kód tohoto – v pořadí již třetího – demonstračního příkladu vypadá následovně:
(ns async8.core (:gen-class)) ; nacteme vsechny potrebne funkce, makra a symboly z knihovny ; (schvalne se nenacitaji vsechny funkce, protoze by jejich jmena ; prepsala takove zakladni funkce, jako je map apod.) (require '[clojure.core.async :refer (go chan >! <! timeout dropping-buffer close!)]) (defn wait "Pozastaveni hlavniho vlakna - simulace interaktivni prace." [] (Thread/sleep 5000)) (defn -main "Tato funkce se spusti automaticky nastrojem Leiningen." [& args] (println "Start") ; vytvorime kanal s dropping bufferem o zadane kapacite (let [channel (chan (dropping-buffer 10))] ; cteni zprav z kanalu (to zahajime drive, at je jistota ; ze zapisy neskonci moc brzo) (go (loop [result []] (<! (timeout 1)) (let [item (<! channel)] ; pokud je kanal zavreny, vrati se nil (if item ; v pripade, ze se prvek precetl (recur (conj result item)) ; prida se do kolekce (println result))))) ; jinak koncime (println "consumer started") ; poslani zprav do kanalu, celkem 1000 hodnot od 0 do 999 (go (doseq [i (range 0 1000)] (>! channel i)) (close! channel))) (println "producer started") ; chvili pockame (wait) (println "Finish") (System/exit 0))
Projektový soubor:
(defproject async8 "0.1.0-SNAPSHOT" :description "FIXME: write description" :url "http://example.com/FIXME" :license {:name "Eclipse Public License" :url "http://www.eclipse.org/legal/epl-v10.html"} :dependencies [[org.clojure/clojure "1.7.0"] [org.clojure/core.async "0.2.374"]] :main ^:skip-aot async8.core :target-path "target/%s" :profiles {:uberjar {:aot :all}})
8. Kanál se „sliding bufferem“
Chování dropping bufferu popsaného v předchozích dvou kapitolách je charakterizováno tím, že v případě zaplněného kanálu dochází na straně producenta (tj. kódu, který do kanálu zapisuje) ke ztrátě dat – nová data prostě budou ignorována. V knihovně clojure.core však ještě existuje jeden typ kanálu, jehož chování je vlastně zcela opačné, i když se opět jedná o jednu z možných variant fronty. Tento alternativní typ kanálu používá takzvaný „sliding buffer“, který se vyznačuje tím, že v případě zaplnění kanálu dochází při dalším zápisu k postupnému přepisu již uložených prvků, přičemž se z kanálu vymaže nejdříve zapsaný prvek (prvky tedy z bufferu „přepadávají“). Skutečně se tedy jedná o jednu z variant fronty, ovšem s tím, že z fronty vedou dva výstupy: jeden je čtený konzumentem a druhý vede (eufemicky řečeno) do /dev/null:
+-----------+ +-----------+ | producent | +------------+ | konzument | | |... >! ... |kanál+fronta} ... <! ... | | | go block | +------------+... | go block | +-----------+ : +-----------+ : : v /dev/null
Kanál s těmito charakteristikami se vytvoří následovně:
; vytvorime kanal se sliding bufferem o zadane kapacite (let [channel (chan (sliding-buffer 10))] ... ... ... )
Poznámka: povšimněte si, že funkce sliding-buffer je parametrem funkce chan. Je častou chybou se pokoušet vytvořit kanál takto:
; ŠPATNĚ!!! vytvorime kanal se sliding bufferem o zadane kapacite (let [channel (sliding-buffer 10)] ... ... ... )
Poznámka 2: vyzkoušejte si ve smyčce REPL rozdíl mezi posledními dvěma příkazy (druhý příkaz vytváří buffer použitý v běžné frontě):
(require '[clojure.core.async :refer (buffer dropping-buffer sliding-buffer?)]) (unblocking-buffer? (buffer 10)) (unblocking-buffer? (sliding-buffer 10))
9. Čtvrtý demonstrační příklad: práce s kanálem se „sliding bufferem“
Opět provedeme úpravu předchozího demonstračního příkladu, a to přepisem typu bufferu z dropping bufferu na sliding buffer:
; vytvorime kanal s sliding bufferem o zadane kapacite (let [channel (chan (sliding-buffer 10))]
Úprava je sice jednoduchá, ovšem chování producenta a konzumenta se zcela změní, což si ihned ověříme (konzument je opět výrazně pomalejší než producent, takže nutně dojde ke ztrátě dat, protože zápis je nyní neblokující operace):
Start consumer started producer started [0 5 33 73 117 167 212 256 305 350 409 477 537 561 622 699 786 870 951 990 991 992 993 994 995 996 997 998 999] Finish
Start consumer started producer started [0 9 16 39 63 103 145 187 223 268 315 363 415 476 529 602 674 750 818 898 976 989 990 991 992 993 994 995 996 997 998 999] Finish
Start consumer started producer started [0 8 41 117 164 206 252 341 400 480 607 674 730 893 983 990 991 992 993 994 995 996 997 998 999] Finish
Povšimněte si, že posledních deset hodnot se přečetlo korektně a ve správném pořadí, a to právě z toho důvodu, že se jedná o poslední hodnoty zapsané do kanálu.
Úplný zdrojový kód dnešního v pořadí již třetího demonstračního příkladu vypadá následovně:
(ns async9.core (:gen-class)) ; nacteme vsechny potrebne funkce, makra a symboly z knihovny ; (schvalne se nenacitaji vsechny funkce, protoze by jejich jmena ; prepsala takove zakladni funkce, jako je map apod.) (require '[clojure.core.async :refer (go chan >! <! timeout sliding-buffer close!)]) (defn wait "Pozastaveni hlavniho vlakna - simulace interaktivni prace." [] (Thread/sleep 5000)) (defn -main "Tato funkce se spusti automaticky nastrojem Leiningen." [& args] (println "Start") ; vytvorime kanal s sliding bufferem o zadane kapacite (let [channel (chan (sliding-buffer 10))] ; cteni zprav z kanalu (to zahajime drive, at je jistota ; ze zapisy neskonci moc brzo) (go (loop [result []] (<! (timeout 1)) (let [item (<! channel)] ; pokud je kanal zavreny, vrati se nil (if item ; v pripade, ze se prvek precetl (recur (conj result item)) ; prida se do kolekce (println result))))) ; jinak koncime (println "consumer started") ; poslani zprav do kanalu, celkem 1000 hodnot od 0 do 999 (go (doseq [i (range 0 1000)] (>! channel i)) (close! channel))) (println "producer started") ; chvili pockame (wait) (println "Finish") (System/exit 0))
Projektový soubor:
(defproject async9 "0.1.0-SNAPSHOT" :description "FIXME: write description" :url "http://example.com/FIXME" :license {:name "Eclipse Public License" :url "http://www.eclipse.org/legal/epl-v10.html"} :dependencies [[org.clojure/clojure "1.7.0"] [org.clojure/core.async "0.2.374"]] :main ^:skip-aot async9.core :target-path "target/%s" :profiles {:uberjar {:aot :all}})
10. Repositář s demonstračními příklady
Všechny čtyři demonstrační příklady, které jsme si v dnešním článku popsali, byly uloženo do Git repositáře dostupného na adrese https://github.com/tisnik/clojure-examples. V tabulce zobrazené pod tímto odstavcem naleznete na zdrojové kódy demonstračních příkladů přímé odkazy:
# | Příklad/knihovna | Github |
---|---|---|
1 | async6 | https://github.com/tisnik/clojure-examples/tree/master/async6 |
2 | async7 | https://github.com/tisnik/clojure-examples/tree/master/async7 |
3 | async8 | https://github.com/tisnik/clojure-examples/tree/master/async8 |
4 | async9 | https://github.com/tisnik/clojure-examples/tree/master/async9 |
11. Odkazy na předchozí části tohoto seriálu
- Clojure 1: Úvod
http://www.root.cz/clanky/clojure-aneb-jazyk-umoznujici-tvorbu-bezpecnych-vicevlaknovych-aplikaci-pro-jvm/ - Clojure 2: Symboly, kolekce atd.
http://www.root.cz/clanky/clojure-aneb-jazyk-umoznujici-tvorbu-bezpecnych-vicevlaknovych-aplikaci-pro-jvm-2-cast/ - Clojure 3: Funkcionální programování
http://www.root.cz/clanky/clojure-aneb-jazyk-umoznujici-tvorbu-bezpecnych-vicevlaknovych-aplikaci-pro-jvm-3-cast-funkcionalni-programovani/ - Clojure 4: Kolekce, sekvence a lazy sekvence
http://www.root.cz/clanky/clojure-aneb-jazyk-umoznujici-tvorbu-bezpecnych-vicevlaknovych-aplikaci-pro-jvm-4-cast-kolekce-sekvence-a-lazy-sekvence/ - Clojure 5: Sekvence, lazy sekvence a paralelní programy
http://www.root.cz/clanky/clojure-a-bezpecne-aplikace-pro-jvm-sekvence-lazy-sekvence-a-paralelni-programy/ - Clojure 6: Podpora pro paralelní programování
http://www.root.cz/clanky/programovaci-jazyk-clojure-6-futures-nejsou-jen-financni-derivaty/ - Clojure 7: Další funkce pro paralelní programování
http://www.root.cz/clanky/programovaci-jazyk-clojure-7-dalsi-podpurne-prostredky-pro-paralelni-programovani/ - Clojure 8: Identity, stavy, neměnné hodnoty a reference
http://www.root.cz/clanky/programovaci-jazyk-clojure-8-identity-stavy-nemenne-hodnoty-a-referencni-typy/ - Clojure 9: Validátory, pozorovatelé a kooperace s Javou
http://www.root.cz/clanky/programovaci-jazyk-clojure-9-validatory-pozorovatele-a-kooperace-mezi-clojure-a-javou/ - Clojure 10: Kooperace mezi Clojure a Javou
http://www.root.cz/clanky/programovaci-jazyk-clojure-10-kooperace-mezi-clojure-a-javou-pokracovani/ - Clojure 11: Generátorová notace seznamu/list comprehension
http://www.root.cz/clanky/programovaci-jazyk-clojure-11-generatorova-notace-seznamu-list-comprehension/ - Clojure 12: Překlad programů z Clojure do bajtkódu JVM I:
http://www.root.cz/clanky/programovaci-jazyk-clojure-12-preklad-programu-z-clojure-do-bajtkodu-jvm/ - Clojure 13: Překlad programů z Clojure do bajtkódu JVM II:
http://www.root.cz/clanky/programovaci-jazyk-clojure-13-preklad-programu-z-clojure-do-bajtkodu-jvm-pokracovani/ - Clojure 14: Základy práce se systémem maker
http://www.root.cz/clanky/programovaci-jazyk-clojure-14-zaklady-prace-se-systemem-maker/ - Clojure 15: Tvorba uživatelských maker
http://www.root.cz/clanky/programovaci-jazyk-clojure-15-tvorba-uzivatelskych-maker/ - Clojure 16: Složitější uživatelská makra
http://www.root.cz/clanky/programovaci-jazyk-clojure-16-slozitejsi-uzivatelska-makra/ - Clojure 17: Využití standardních maker v praxi
http://www.root.cz/clanky/programovaci-jazyk-clojure-17-vyuziti-standardnich-maker-v-praxi/ - Clojure 18: Základní techniky optimalizace aplikací
http://www.root.cz/clanky/programovaci-jazyk-clojure-18-zakladni-techniky-optimalizace-aplikaci/ - Clojure 19: Vývojová prostředí pro Clojure
http://www.root.cz/clanky/programovaci-jazyk-clojure-19-vyvojova-prostredi-pro-clojure/ - Clojure 20: Vývojová prostředí pro Clojure (Vimu s REPL)
http://www.root.cz/clanky/programovaci-jazyk-clojure-20-vyvojova-prostredi-pro-clojure-integrace-vimu-s-repl/ - Clojure 21: ClojureScript aneb překlad Clojure do JS
http://www.root.cz/clanky/programovaci-jazyk-clojure-21-clojurescript-aneb-preklad-clojure-do-javascriptu/ - Leiningen: nástroj pro správu projektů napsaných v Clojure
http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure/ - Leiningen: nástroj pro správu projektů napsaných v Clojure (2)
http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-2/ - Leiningen: nástroj pro správu projektů napsaných v Clojure (3)
http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-3/ - Leiningen: nástroj pro správu projektů napsaných v Clojure (4)
http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-4/ - Leiningen: nástroj pro správu projektů napsaných v Clojure (5)
http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-5/ - Leiningen: nástroj pro správu projektů napsaných v Clojure (6)
http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-6/ - Programovací jazyk Clojure a databáze (1.část)
http://www.root.cz/clanky/programovaci-jazyk-clojure-a-databaze-1-cast/ - Pluginy pro Leiningen
http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-pluginy-pro-leiningen/ - Programovací jazyk Clojure a knihovny pro práci s vektory a maticemi
http://www.root.cz/clanky/programovaci-jazyk-clojure-a-knihovny-pro-praci-s-vektory-a-maticemi/ - Programovací jazyk Clojure a knihovny pro práci s vektory a maticemi (2)
http://www.root.cz/clanky/programovaci-jazyk-clojure-a-knihovny-pro-praci-s-vektory-a-maticemi-2/ - Programovací jazyk Clojure: syntéza procedurálních textur s využitím knihovny Clisk
http://www.root.cz/clanky/programovaci-jazyk-clojure-synteza-proceduralnich-textur-s-vyuzitim-knihovny-clisk/ - Programovací jazyk Clojure: syntéza procedurálních textur s využitím knihovny Clisk (2)
http://www.root.cz/clanky/programovaci-jazyk-clojure-synteza-proceduralnich-textur-s-vyuzitim-knihovny-clisk-2/ - Programovací jazyk Clojure: syntéza procedurálních textur s využitím knihovny Clisk (dokončení)
http://www.root.cz/clanky/programovaci-jazyk-clojure-synteza-proceduralnich-textur-s-vyuzitim-knihovny-clisk-dokonceni/ - Seesaw: knihovna pro snadnou tvorbu GUI v jazyce Clojure
http://www.root.cz/clanky/seesaw-knihovna-pro-snadnou-tvorbu-gui-v-jazyce-clojure/ - Seesaw: knihovna pro snadnou tvorbu GUI v jazyce Clojure (2)
http://www.root.cz/clanky/seesaw-knihovna-pro-snadnou-tvorbu-gui-v-jazyce-clojure-2/ - Seesaw: knihovna pro snadnou tvorbu GUI v jazyce Clojure (3)
http://www.root.cz/clanky/seesaw-knihovna-pro-snadnou-tvorbu-gui-v-jazyce-clojure-3/ - Programovací jazyk Clojure a práce s Gitem
http://www.root.cz/clanky/programovaci-jazyk-clojure-a-prace-s-gitem/ - Programovací jazyk Clojure a práce s Gitem (2)
http://www.root.cz/clanky/programovaci-jazyk-clojure-a-prace-s-gitem-2/ - Programovací jazyk Clojure – triky při práci s řetězci
http://www.root.cz/clanky/programovaci-jazyk-clojure-triky-pri-praci-s-retezci/ - Programovací jazyk Clojure – triky při práci s kolekcemi
http://www.root.cz/clanky/programovaci-jazyk-clojure-triky-pri-praci-s-kolekcemi/ - Programovací jazyk Clojure – práce s mapami a množinami
http://www.root.cz/clanky/programovaci-jazyk-clojure-prace-s-mapami-a-mnozinami/ - Programovací jazyk Clojure – základy zpracování XML
http://www.root.cz/clanky/programovaci-jazyk-clojure-zaklady-zpracovani-xml/ - Programovací jazyk Clojure – testování s využitím knihovny Expectations
http://www.root.cz/clanky/programovaci-jazyk-clojure-testovani-s-vyuzitim-knihovny-expectations/ - Programovací jazyk Clojure – některé užitečné triky použitelné (nejenom) v testech
http://www.root.cz/clanky/programovaci-jazyk-clojure-nektere-uzitecne-triky-pouzitelne-nejenom-v-testech/ - Enlive – výkonný šablonovací systém pro jazyk Clojure
http://www.root.cz/clanky/enlive-vykonny-sablonovaci-system-pro-jazyk-clojure/ - Nástroj Leiningen a programovací jazyk Clojure: tvorba vlastních knihoven pro veřejný repositář Clojars
http://www.root.cz/clanky/nastroj-leiningen-a-programovaci-jazyk-clojure-tvorba-vlastnich-knihoven-pro-verejny-repositar-clojars/ - Novinky v Clojure verze 1.8.0
http://www.root.cz/clanky/novinky-v-clojure-verze-1–8–0/ - Asynchronní programování v Clojure s využitím knihovny core.async
http://www.root.cz/clanky/asynchronni-programovani-v-clojure-s-vyuzitim-knihovny-core-async/
12. Odkazy na Internetu
- Communicating sequential processes
https://en.wikipedia.org/wiki/Communicating_sequential_processes - Clojure core.async
http://www.infoq.com/presentations/clojure-core-async - core.async API Reference
https://clojure.github.io/core.async/ - Clojure core.async Channels
http://clojure.com/blog/2013/06/28/clojure-core-async-channels.html - core.async examples
https://github.com/clojure/core.async/blob/master/examples/walkthrough.clj - Timothy Baldridge – Core.Async
https://www.youtube.com/watch?v=enwIIGzhahw - Designing Front End Applications with core.async
http://go.cognitect.com/core_async_webinar_recording - Mastering Concurrent Processes with core.async
http://www.braveclojure.com/core-async/ - LispCast: Clojure core.async
https://www.youtube.com/watch?v=msv8Fvtd6YQ - Julian Gamble – Applying the paradigms of core.async in ClojureScript
https://www.youtube.com/watch?v=JUrOebC5HmA - Zip archiv s Clojure 1.8.0
http://repo1.maven.org/maven2/org/clojure/clojure/1.8.0/clojure-1.8.0.zip - Clojure 1.8 is now available
http://clojure.org/news/2016/01/19/clojure18 - Changes to Clojure in Version 1.8
https://github.com/clojure/clojure/blob/master/changes.md - Socket Server REPL
http://dev.clojure.org/display/design/Socket+Server+REPL - CLJ-1671: Clojure socket server
http://dev.clojure.org/jira/browse/CLJ-1671 - CLJ-1449: Add clojure.string functions for portability to ClojureScript
http://dev.clojure.org/jira/browse/CLJ-1449 - Launching a Socket Server
http://clojure.org/reference/repl_and_main#_launching_a_socket_server - API for clojure.string
http://clojure.github.io/clojure/branch-master/clojure.string-api.html - Clojars:
https://clojars.org/ - Seznam knihoven na Clojars:
https://clojars.org/projects - Clojure Cookbook: Templating HTML with Enlive
https://github.com/clojure-cookbook/clojure-cookbook/blob/master/07_webapps/7–11_enlive.asciidoc - An Introduction to Enlive
https://github.com/swannodette/enlive-tutorial/ - Enlive na GitHubu
https://github.com/cgrand/enlive - Expectations: příklady atd.
http://jayfields.com/expectations/ - Expectations na GitHubu
https://github.com/jaycfields/expectations - Lein-expectations na GitHubu
https://github.com/gar3thjon3s/lein-expectations - Testing Clojure With Expectations
https://semaphoreci.com/blog/2014/09/23/testing-clojure-with-expectations.html - Clojure testing TDD/BDD libraries: clojure.test vs Midje vs Expectations vs Speclj
https://www.reddit.com/r/Clojure/comments/1viilt/clojure_testing_tddbdd_libraries_clojuretest_vs/ - Testing: One assertion per test
http://blog.jayfields.com/2007/06/testing-one-assertion-per-test.html - Rewriting Your Test Suite in Clojure in 24 hours
http://blog.circleci.com/rewriting-your-test-suite-in-clojure-in-24-hours/ - Clojure doc: zipper
http://clojuredocs.org/clojure.zip/zipper - Clojure doc: parse
http://clojuredocs.org/clojure.xml/parse - Clojure doc: xml-zip
http://clojuredocs.org/clojure.zip/xml-zip - Clojure doc: xml-seq
http://clojuredocs.org/clojure.core/xml-seq - Parsing XML in Clojure
https://github.com/clojuredocs/guides - Clojure Zipper Over Nested Vector
https://vitalyper.wordpress.com/2010/11/23/clojure-zipper-over-nested-vector/ - Understanding Clojure's PersistentVector implementation
http://blog.higher-order.net/2009/02/01/understanding-clojures-persistentvector-implementation - Understanding Clojure's PersistentHashMap (deftwice…)
http://blog.higher-order.net/2009/09/08/understanding-clojures-persistenthashmap-deftwice.html - Assoc and Clojure's PersistentHashMap: part ii
http://blog.higher-order.net/2010/08/16/assoc-and-clojures-persistenthashmap-part-ii.html - Ideal Hashtrees (paper)
http://lampwww.epfl.ch/papers/idealhashtrees.pdf - Clojure home page
http://clojure.org/ - Clojure (downloads)
http://clojure.org/downloads - Clojure Sequences
http://clojure.org/sequences - Clojure Data Structures
http://clojure.org/data_structures - The Structure and Interpretation of Computer Programs: 2.2.1 Representing Sequences
http://mitpress.mit.edu/sicp/full-text/book/book-Z-H-15.html#%_sec2.2.1 - The Structure and Interpretation of Computer Programs: 3.3.1 Mutable List Structure
http://mitpress.mit.edu/sicp/full-text/book/book-Z-H-22.html#%_sec3.3.1 - Clojure – Functional Programming for the JVM
http://java.ociweb.com/mark/clojure/article.html - Clojure quick reference
http://faustus.webatu.com/clj-quick-ref.html - 4Clojure
http://www.4clojure.com/ - ClojureDoc (rozcestník s dokumentací jazyka Clojure)
http://clojuredocs.org/ - Clojure (na Wikipedia EN)
http://en.wikipedia.org/wiki/Clojure - Clojure (na Wikipedia CS)
http://cs.wikipedia.org/wiki/Clojure - SICP (The Structure and Interpretation of Computer Programs)
http://mitpress.mit.edu/sicp/ - Pure function
http://en.wikipedia.org/wiki/Pure_function - Funkcionální programování
http://cs.wikipedia.org/wiki/Funkcionální_programování - Čistě funkcionální (datové struktury, jazyky, programování)
http://cs.wikipedia.org/wiki/Čistě_funkcionální - Clojure Macro Tutorial (Part I, Getting the Compiler to Write Your Code For You)
http://www.learningclojure.com/2010/09/clojure-macro-tutorial-part-i-getting.html - Clojure Macro Tutorial (Part II: The Compiler Strikes Back)
http://www.learningclojure.com/2010/09/clojure-macro-tutorial-part-ii-compiler.html - Clojure Macro Tutorial (Part III: Syntax Quote)
http://www.learningclojure.com/2010/09/clojure-macro-tutorial-part-ii-syntax.html - Tech behind Tech: Clojure Macros Simplified
http://techbehindtech.com/2010/09/28/clojure-macros-simplified/ - Fatvat – Exploring functional programming: Clojure Macros
http://www.fatvat.co.uk/2009/02/clojure-macros.html - Eulerovo číslo
http://cs.wikipedia.org/wiki/Eulerovo_číslo - List comprehension
http://en.wikipedia.org/wiki/List_comprehension - List Comprehensions in Clojure
http://asymmetrical-view.com/2008/11/18/list-comprehensions-in-clojure.html - Clojure Programming Concepts: List Comprehension
http://en.wikibooks.org/wiki/Clojure_Programming/Concepts#List_Comprehension - Clojure core API: for macro
http://clojure.github.com/clojure/clojure.core-api.html#clojure.core/for - cirrus machina – The Clojure for macro
http://www.cirrusmachina.com/blog/comment/the-clojure-for-macro/ - Riastradh's Lisp Style Rules
http://mumble.net/~campbell/scheme/style.txt - Dynamic Languages Strike Back
http://steve-yegge.blogspot.cz/2008/05/dynamic-languages-strike-back.html - Scripting: Higher Level Programming for the 21st Century
http://www.tcl.tk/doc/scripting.html - Java Virtual Machine Support for Non-Java Languages
http://docs.oracle.com/javase/7/docs/technotes/guides/vm/multiple-language-support.html - Třída java.lang.String
http://docs.oracle.com/javase/7/docs/api/java/lang/String.html - Třída java.lang.StringBuffer
http://docs.oracle.com/javase/7/docs/api/java/lang/StringBuffer.html - Třída java.lang.StringBuilder
http://docs.oracle.com/javase/7/docs/api/java/lang/StringBuilder.html - StringBuffer versus String
http://www.javaworld.com/article/2076072/build-ci-sdlc/stringbuffer-versus-string.html - Threading macro (dokumentace k jazyku Clojure)
https://clojure.github.io/clojure/clojure.core-api.html#clojure.core/-> - Understanding the Clojure → macro
http://blog.fogus.me/2009/09/04/understanding-the-clojure-macro/ - clojure.inspector
http://clojure.github.io/clojure/clojure.inspector-api.html - The Clojure Toolbox
http://www.clojure-toolbox.com/ - Unit Testing in Clojure
http://nakkaya.com/2009/11/18/unit-testing-in-clojure/ - Testing in Clojure (Part-1: Unit testing)
http://blog.knoldus.com/2014/03/22/testing-in-clojure-part-1-unit-testing/ - API for clojure.test – Clojure v1.6 (stable)
https://clojure.github.io/clojure/clojure.test-api.html - Leiningen: úvodní stránka
http://leiningen.org/ - Leiningen: Git repository
https://github.com/technomancy/leiningen - leiningen-win-installer
http://leiningen-win-installer.djpowell.net/ - Clojure.org: Vars and the Global Environment
http://clojure.org/Vars - Clojure.org: Refs and Transactions
http://clojure.org/Refs - Clojure.org: Atoms
http://clojure.org/Atoms - Clojure.org: Agents as Asynchronous Actions
http://clojure.org/agents - Transient Data Structureshttp://clojure.org/transients