Obsah
1. Knihovny libmill a libdill: implementace korutin a kanálů pro jazyk C
2. Několik poznámek a autorovi knihoven libmill a libdill
4. Communicating Sequential Processes
5. Konstrukce používané v moderních implementacích odvozených od CSP
6. Kanály a gorutiny v programovacím jazyce Go
7. Implementace kanálů a go-bloků v jazyce Clojure
8. Knihovna libmill se představuje
10. Vytvoření a zavolání korutin
11. Vytvoření většího množství korutin bez vzájemné synchronizace
13. Přepnutí běhu na jinou korutinu
14. Použití kanálů pro komunikaci mezi korutinami
15. Nejjednodušší použití kanálu
17. Použití konstrukce typu select
18. Obsah následujícího článku
19. Repositář s demonstračními příklady
1. Knihovny libmill a libdill: implementace korutin a kanálů pro jazyk C
Jedním z důvodů, proč se poměrně často setkáme s použitím programovacího jazyka Go, je velmi dobrá podpora pro práci s takzvanými gorutinami a taktéž s kanály, které tvoří programové prostředky určené pro komunikaci mezi nimi. Díky existenci těchto prostředků lze relativně snadno vytvářet programy využívající souběžnost a přitom netrpících problémy typu deadlock, souběžný přístup ke sdílené proměnné/paměti bez zajištění výlučnosti či atomičnosti přístupu atd. Ovšem gorutiny (i když toto je název používaný výhradně v Go) a kanály nejsou v žádném případě výsadou tohoto jediného programovacího jazyka. Můžeme se s nimi setkat například i v jazyku Clojure, částečně dokonce i v Pythonu (jedná se o knihovnu goless, která je ovšem prozatím spíše ve stadiu vývoje, což je škoda) a – což může být překvapivé – i ve starém dobrém programovacím jazyku C.
Pro tento postarší, ale stále velmi často používaný programovací jazyk totiž existuje knihovna nazvaná libmill a taktéž poněkud odlišně postavená knihovna libdill. V knihovně libmill se její autor snaží napodobit sémantiku používanou programovacím jazykem Go, knihovna libdill (pocházející ovšem od stejného autora) je naopak orientována spíše přímo na céčkové programátory a nabízí sémantiku v mnoha ohledech odlišnou od jazyka Go. Již na úvod je nutné říci, že výkonnost knihovny libmill je přitom úctyhodná – ostatně sám její autor uvádí, že je možné spustit až 20 milionů korutin a provést více než 50 milionů přepnutí kontextů za sekundu (zde pochopitelně za předpokladu, že procesor nebude provádět žádné další výpočetně náročné činnosti). Knihovna libdill je ovšem realizována odlišným způsobem a tím pádem se liší i její výkonnost. Přepnutí kontextu lze provést za cca 6 ns, korutina se vytvoří za 26 ns a poslání zprávy přes kanál může trvat okolo 40 ns.
V první části článku si ukážeme některé společné vlastnosti již existujících knihoven a jazyků založených na konceptu popsaného ve slavném článku CSP. V části druhé si pak řekneme, jaké možnosti nám nabízí právě knihovna libmill.
2. Několik poznámek a autorovi knihoven libmill a libdill
Autorem obou výše zmíněných knihoven libmill a libdill je Martin Sustrik (http://250bpm.com/), s nímž jsme se již na stránkách Rootu několikrát setkali, protože je mj. i autorem či spoluautorem knihoven ØMQ a nanomsg. Viz též následující články ze seriálu o message brokerech, v nichž jsme se těmito zajímavými a užitečnými nástroji zabývali:
- ØMQ: knihovna pro asynchronní předávání zpráv
https://www.root.cz/clanky/0mq-knihovna-pro-asynchronni-predavani-zprav/ - Další možnosti poskytované knihovnou ØMQ
https://www.root.cz/clanky/dalsi-moznosti-poskytovane-knihovnou-mq/ - Využití zařízení v knihovně ØMQ při tvorbě systému se složitější architekturou
https://www.root.cz/clanky/vyuziti-zarizeni-v-knihovne-mq-pri-tvorbe-systemu-se-slozitejsi-architekturou/ - Další možnosti nabízené knihovnou ØMQ, implementace protokolů ØMQ v čisté Javě
https://www.root.cz/clanky/dalsi-moznosti-nabizene-knihovnou-mq-implementace-protokolu-mq-v-ciste-jave/ - Implementace různých komunikačních strategií s využitím knihovny nanomsg
https://www.root.cz/clanky/implementace-ruznych-komunikacnich-strategii-s-vyuzitim-knihovny-nanomsg/ - Dokončení popisu komunikačních strategií poskytovaných knihovnou nanomsg
https://www.root.cz/clanky/dokonceni-popisu-komunikacnich-strategii-poskytovanych-knihovnou-nanomsg/
3. Souběžné provádění operací
Knihovny či nové jazykové konstrukce umožňující používání kanálů (či front) pro synchronní, popř. asynchronní komunikaci mezi různými částmi vyvíjených aplikací, se v posledních několika letech těší poměrně velké popularitě. Ta je způsobena dvěma faktory. První důvod spočívá ve snaze o zjednodušení návrhu (či porozumění) vyvíjené aplikace, zejména ve chvíli, kdy se v rámci jednoho programu předávají data (resp. objekty) mezi částmi, jejichž funkce může být dobře izolována od částí ostatních a která již z principu mají běžet souběžně (UI, hry atd.). Druhý důvod je poněkud prozaičtější – v některých situacích je nutné dosáhnout zvýšení efektivity aplikace (například zvýšit počet odpovědí, které může server vygenerovat za určitou časovou jednotku) a přitom není možné či vhodné využívat přímočaré ale dosti nízkoúrovňové řešení založené na použití většího množství vláken spravovaných systémem. Naprosto typickým příkladem jsou některé virtuální stroje JavaScriptu, které povětšinou umožňují běh aplikace v jediném vláknu (což je ovšem s ohledem na „kvalitu“ některých programových kódů spíše výhodou…).
Některé programovací jazyky, zejména pak již v první kapitole zmíněný jazyk Go, obsahují prostředky sloužící pro zajištění synchronní a asynchronní komunikace přímo v syntaxi (a samozřejmě též v sémantice) jazyka. Konkrétně v případě jazyka Go se jedná o takzvané „gorutiny“ představované klíčovým slovem go a taktéž o operace sloužící pro zápis či čtení dat z kanálů, které jsou představovány operátorem <- (ten má dva významy v závislosti na tom, zda je před operátorem uveden identifikátor představující kanál či nikoli). V programovacím jazyku Clojure však naproti tomu nebylo nutné zavádět speciální syntaxi a vlastně ani nebylo nutné zasahovat do překladače. Celá knihovna core.async, která nabízí podobný koncept, jako jazyk Go, je založena na několika běžných funkcích a hlavně pak na makrech, které manipulací s předaným programovým kódem dokážou z běžného příkazového bloku vytvořit blok zpracovávaný asynchronně (opět se zde ukazuje přednost homoikonických jazyků). Použít ji lze dokonce i v ClojureScriptu, tedy v implementaci jazyka Clojure postavené nad mnohdy „jednovláknovým“ JavaScriptem.
4. Communicating Sequential Processes
Použití asynchronních kanálů a z nich odvozených asynchronních vstupně-výstupních operací (async I/O), se mezi širší programátorskou komunitu rozšířilo pravděpodobně až s vývojem známého systému Node.js. Ve skutečnosti je však realizovaný princip mnohem starší, protože sahá až do šedesátých a sedmdesátých let minulého století, kdy se postupně vyvíjely metody umožňující lepší využití strojového času tehdejších počítačů (lepším využitím se zde nutně nemyslí škálovatelnost!). Za strojový čas se tehdy samozřejmě platilo, což přeneseně platí (sic) i dnes. Poznatky vypracovávané v šedesátých a sedmdesátých letech byly formalizovány a následně shrnuty do teorie popsané ve známém článku Communicating Sequential Processes, jejímž autorem je C.A.R Hoare. Zmíněný článek byl vydán již v roce 1978 a kvůli jeho oblibě a známosti se mnohdy setkáme pouze se zkratkou CSP. CSP měl velký vliv na design několika programovacích jazyků, mezi jinými i jazyka occam a nepřímo i jazyků Limbo, Go a Crystal.
5. Konstrukce používané v moderních implementacích odvozených od CSP
Moderní implementace CSP je většinou založena na dvou základních konstrukcích podporovaných buď přímo v programovacím jazyku nebo v jeho knihovně (popř. s využitím makrosystému daného jazyka). Jedná se o již zmíněné kanály, které lze v některých případech nakonfigurovat takovým způsobem, že fungují jako fronty či buffery (nejjednodušší kanál vlastnosti fronty nemá, proto jsou všechny operace nad ním blokující, někdy se přeneseně a poněkud nepřesně používá označení mailbox). Pro operace s kanály je typicky deklarováno několik funkcí či jazykových konstrukcí: vytvoření kanálu, zavření kanálu, zápis dat, čtení dat, čtení dat z libovolného kanálu, čtení dat s určením priority jednotlivých kanálů atd. Kanál lze použít i jako takzvané synchronizační primitivum a jak uvidíme dále, některé speciální kanály lze použít například i pro pouhé pozastavení výpočtu. Druhou konstrukcí jsou většinou takzvané go bloky (ostatně stejně se jmenují i v knihovně libmill).
Kanály umožňují snadno realizovat například konstrukci typu producent–konzument:
+-----------+ +-----------+ | producent | +-----+ | konzument | | |... >! ... |kanál} ... <! ... | | | go block | +-----+ | go block | +-----------+ +-----------+
Kanál může být využíván více producenty (i konzumenty), takže se původní schéma změní následovně:
+-----------+ | producent | | #1 |... >!......... | go block | : +-----------+ : : +-----------+ : +-----------+ | producent | +-----+ | konzument | | #2 |... >! ... |kanál} ... <! ... | | | go block | +-----+ | go block | +-----------+ : +-----------+ : +-----------+ : | producent | : | #3 |... >!........: | go block | +-----------+
Náhodný výběr 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. Kromě dvou již uvedených schémat tedy ještě přibývá třetí schéma + všechny jeho varianty:
+-----------+ | producent | +------+ | #1 |... >!.....|kanál1}....... | go block | +------+ : +-----------+ : : +-----------+ : +-----------+ | producent | +------+ : | konzument | | #2 |... >! ... |kanál2} ... alts! ... | | | go block | +------+ : | go block | +-----------+ : +-----------+ : +-----------+ : | producent | +------+ : | #3 |... >!.....|kanál3}......: | go block | +------+ +-----------+
6. Kanály a gorutiny v programovacím jazyce Go
Nejprve se před popisem možností nabízených knihovnou libmill podívejme na to, jak je problematika kanálů a gorutin řešena v programovacím jazyce Go. V Go se gorutiny vytváří a volají velmi snadno – s použitím klíčového slova go. V následujícím příkladu je ukázáno, jak lze dvakrát zavolat funkci message, a to pokaždé v samostatné gorutině běžící nezávisle na gorutině hlavní (main). Ve skutečnosti je však velmi pravděpodobné, že se tato funkce v praxi nestihne zavolat ani jednou, protože mezitím dojde k ukončení hlavní gorutiny a tím pádem i k ukončení činnosti celé aplikace:
package main import "fmt" func message(id int) { fmt.Printf("gorutina %d\n", id) } func main() { fmt.Println("main begin") go message(1) go message(2) fmt.Println("main end") }
Ve druhém příkladu je použit kanál sloužící jako komunikační médium mezi dvěma gorutinami, konkrétně mezi gorutinou hlavní a gorutinou představovanou funkcí message. Hlavní gorutina kanál vytvoří, otevře a následně čeká, až do něj druhá gorutina provede zápis. Pro zápis i čtení z kanálu, což jsou potenciálně blokující operace, slouží v programovacím jazyce Go specializované operátory:
package main import "fmt" func message(id int, channel chan int) { fmt.Printf("gorutina %d\n", id) channel <- 1 } func main() { channel := make(chan int) fmt.Println("main begin") go message(1, channel) fmt.Println("waiting...") code, status := <-channel fmt.Printf("received code: %d and status: %t\n", code, status) fmt.Println("main end") }
Ve třetím příkladu je ukázáno řešení klasické úlohy producent-konzument, v níž konzument (nebo taktéž worker) běží ve vlastní gorutině, zpracovává data posílaná do kanálu taskChannel a ve chvíli, kdy je tento kanál zavřený, ukončí se. Ovšem ještě předtím o svém ukončení informuje producenta, a to zápisem zprávy do kanálu nazvaného workerDone:
package main import "fmt" func worker(taskChannel chan int, workerDone chan bool) { fmt.Println("worker started") for { value, more := <-taskChannel if more { fmt.Printf("worker received task with parameter %d\n", value) } else { fmt.Println("finishing worker") workerDone <- true fmt.Println("worker finished") return } } } func main() { taskChannel := make(chan int) workerDone := make(chan bool) fmt.Println("main begin") go worker(taskChannel, workerDone) for i := 1; i <= 10; i++ { fmt.Printf("sending task with parameter %d\n", i) taskChannel <- i } close(taskChannel) fmt.Println("waiting for workers...") code, status := <-workerDone fmt.Printf("received code: %t and status: %t\n", code, status) fmt.Println("main end") }
V příkladu následujícím je ukázáno jednoduché použití jazykové konstrukce select-case při čekání na data, která mohou být poslána do jednoho ze dvou kanálů ch1 a ch2. V tomto programu jsou nejdříve vytvořeny dva kanály. Následně jsou spuštěny dvě gorutiny, z nichž první pošle data do prvního kanálu ch1 a druhá do druhého kanálu ch2. Nakonec v programové konstrukci select-case počkáme na to, až jsou data dostupná v libovolném z těchto kanálů (teoreticky ani nemusíme jeden z workerů-gorutin spouštět):
package main import ( "fmt" "time" ) func worker(channel chan int, worker int) { fmt.Printf("Worker %d spuštěn\n", worker) time.Sleep(2 * time.Second) channel <- 1 fmt.Printf("Worker %d ukončen\n", worker) } func main() { ch1 := make(chan int) ch2 := make(chan int) go worker(ch1, 1) go worker(ch2, 2) for true { select { case <-ch1: fmt.Println("Data z kanálu 1") case <-ch2: fmt.Println("Data z kanálu 2") default: fmt.Println("Žádná data nejsou k dispozici") } time.Sleep(1 * time.Second) } }
V posledním demonstračním příkladu, v němž použijeme konstrukci select-case, zkombinujeme jak čtení, tak i zápis do různých kanálů. I tuto kombinaci je možné v programovacím jazyku Go bez větších problémů použít. V konstrukci select-case se vybere vždy pouze jediná větev v závislosti na tom, zda jsou v prvních dvou kanálech data, popř. zda jsou druhé dva kanály prázdné či obsazené.
package main import ( "fmt" "time" ) func receiver(channel chan int, receiver int) { for true { value, ok := <-channel if ok { fmt.Printf("Příjemce %d přijal hodnotu %d\n", receiver, value) } else { fmt.Printf("Kanál je pro příjemce %d uzavřen\n", receiver) } time.Sleep(2 * time.Second) } } func sender(channel chan int, sender int) { fmt.Printf("Odesílatel %d byl spuštěn\n", sender) for i := 1; i <= 5; i++ { time.Sleep(1 * time.Second) channel <- i } fmt.Printf("Odesílatel %d byl ukončen\n", sender) } func main() { ch1 := make(chan int) ch2 := make(chan int) ch3 := make(chan int) // ch1 := make(chan int, 20) go receiver(ch1, 1) go receiver(ch1, 2) go sender(ch2, 1) go sender(ch3, 2) for i := 0; i < 20; i++ { select { case ch1 <- 0: fmt.Println("Poslána nula") case ch1 <- 1: fmt.Println("Poslána jednička") case data, ok := <-ch2: if ok { fmt.Printf("Přijata data %d z kanálu 2\n", data) } case data, ok := <-ch3: if ok { fmt.Printf("Přijata data %d z kanálu 3\n", data) } } } }
7. Implementace kanálů a go-bloků v jazyce Clojure
Nyní se podívejme na způsob implementace kanálů a go-bloku v programovacím jazyce Clojure. Zde se namísto nových klíčových slov a nových jazykových konstrukcí používají převážně makra.
V prvním demonstračním příkladu je vytvořen producent, který do kanálu postupně posílá sekvenci čísel 0 až 9, přičemž mezi čísly je určitý čekací interval simulující nějaký výpočet či I/O operaci. Konzument je naproti tomu realizován nekonečnou smyčkou čekající na data z kanálu. Pro go bloky je typické právě použití smyček, ovšem lze použít i go-loop atd.:
(ns async1.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 >! <!]) (defn wait "Pozastaveni hlavniho vlakna - simulace interaktivni prace." [] (Thread/sleep 15000)) (defn -main "Tato funkce se spusti automaticky nastrojem Leiningen." [& args] (println "Start") ; vytvorime kanal (let [channel (chan)] ; kontinualni posilani zprav do kanalu v asynchronnim bloku (go (dotimes [i 10] (Thread/sleep 1000) (>! channel i))) (println "1st go block started") ; kontinualni cteni zprav z kanalu v asynchronnim bloku (go (while true (println (<! channel)))) (println "2nd go block started")) ; chvili pockame, az se vypise cela sekvence 0 az 9 (wait) (println "Finish"))
Ve skutečnosti není použití (Thread/sleep 1000) příliš idiomatické. Mnohem lepší je použití speciálního kanálu vytvořeného zavoláním (timeout x). Tento kanál je po uplynutí určeného časového intervalu automaticky uzavřen. Co to znamená? Pokud z kanálu pouze čteme, jedná se o blokující operaci (to již víme), ovšem po uplynutí určeného času je kanál uzavřen a vrátí se hodnota nil. Čtením z kanálu typu timeout tedy můžeme elegantně realizovat efektivně implementované čekání. Upravený producent vypadá takto:
; kontinualni posilani zprav do kanalu v asynchronnim bloku (go (dotimes [i 10] ;(Thread/sleep 1000) (<! (timeout 1000)) (>! channel i)))
Kanál může být pochopitelně využíván více producenty (i konzumenty). Toto uspořádání je samozřejmě v jazyce Clojure plně podporováno, o čemž nás přesvědčí další programový kód s trojicí producentů a jediným konzumentem, který vypadá následovně:
(ns async2.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)]) (defn wait "Pozastaveni hlavniho vlakna - simulace interaktivni prace." [] (Thread/sleep 10000)) (defn -main "Tato funkce se spusti automaticky nastrojem Leiningen." [& args] (println "Start") ; vytvorime kanal (let [channel (chan)] ; kontinualni posilani zprav do kanalu v trojici asynchronnich bloku (go (while true (<! (timeout 500)) (>! channel "first"))) (go (while true (<! (timeout 1000)) (>! channel "second"))) (go (while true (<! (timeout 2000)) (>! channel "third"))) (println "producers started") ; kontinualni cteni zprav z kanalu v asynchronnim bloku (go (while true (println (<! channel)))) (println "consumer started")) ; chvili pockame, az se vypise cela sekvence 0 az 9 (wait) (println "Finish") (System/exit 0))
Použití většího množství producentů jsme si již ukázali v předchozím příkladu. Nyní si řekněme, jak může jediný konzument načítat data z většího množství kanálů. To je možné, ovšem namísto operace <! se použije funkce alts!, které se předá vektor kanálů. Funkce alts! ve výchozím nastavení náhodně vybere kanál, ze kterého bude číst. Náhodný výběr 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.
Funkce alts! je použita ve třetím demonstračním příkladu. Povšimněte si, že návratovou hodnotou této funkce je dvojice, protože je nutné nějakým způsobem vrátit jak referenci na kanál, ze kterého se čte, tak i vlastní přečtenou hodnotu. My referenci na kanál použijeme ve výstupu pro určení tisknutého prefixu:
(ns async3.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 10000)) (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)] (<! (timeout 500)) (>! channel1 i))) (go (doseq [i (range)] (<! (timeout 1000)) (>! channel2 i))) (go (doseq [i (range)] (<! (timeout 2000)) (>! channel3 i))) (println "producers started") ; kontinualni cteni zprav z kanalu v asynchronnim bloku (go (while true (let [[item channel] (alts! [channel1 channel2 channel3])] (condp = channel channel1 (println "channel #1: " item) channel2 (println "channel #2: " item) channel3 (println "channel #3: " item))))) (println "consumer started")) ; chvili pockame, az se vypise cela sekvence 0 az 9 (wait) (println "Finish") (System/exit 0))
8. Knihovna libmill se představuje
Nejprve malé shrnutí: viděli jsme, že jak v jazyce Go, tak i ve zcela odlišně pojatém programovacím jazyce Clojure, existuje několik užitečných technologií:
- Koncept korutin či gorutin
- Kanály jako komunikační médium mezi gorutinami
- Možnost číst či zapisovat do jednoho kanálu z nabízeného seznamu/vektoru na základě toho, který kanál je připravený
Všechny tyto tři technologie nalezneme i v knihovně libmill. Způsob implementace se však nutně odlišuje. V jazyce Go je práce s gorutinami součástí vlastního jazyka, v Clojure se jedná o systém funkcí a maker a v případě knihovny libmill je použit stejně znějící, ovšem interně zcela odlišný mechanismus založený taktéž na specializovaných funkcích a značně složitých makrech preprocesoru programovacího jazyka C. Ovšem z hlediska programátora se zejména Go a libmill značně podobá: stačí se podívat na porovnání uvedené přímo na stránce http://libmill.org/.
9. Překlad knihovny libmill
Nejprve je pochopitelně nutné knihovnu libmill nainstalovat. To je snadné, budete přitom potřebovat jen základní sadu nástrojů, které má většinou k dispozici prakticky každý céčkový vývojář – překladač, linker, program make, nechvalně známé autotools (autoconf) atd.
V prvním kroku stáhneme zdrojové kódy knihovny:
$ curl http://libmill.org/libmill-1.18.tar.gz
V kroku druhém rozbalíme získaný tarball:
$ tar xvfz libmill-1.18.tar.gz
Provedeme konfiguraci programu:
$ cd libmill-1.18 $ ./configure checking for a BSD-compatible install... /usr/bin/install -c checking whether build environment is sane... yes checking for a thread-safe mkdir -p... /bin/mkdir -p checking for gawk... gawk checking whether make sets $(MAKE)... yes ... ... ... config.status: creating Makefile config.status: creating libmill.pc config.status: executing depfiles commands config.status: executing libtool commands
Následuje překlad s využitím utility make:
$ make CC libmill_la-chan.lo CC libmill_la-cr.lo CC libmill_la-debug.lo CC libmill_la-ip.lo CC libmill_la-list.lo CC libmill_la-mfork.lo ... ... ... CC tutorial/step6.o CCLD tutorial/step6 CC tutorial/step7.o CCLD tutorial/step7 CC tutorial/step8.o CCLD tutorial/step8
Výsledkem by měl být (mj.) i podadresář .libs obsahující statickou i dynamickou variantu knihovny libmill.
Můžeme provést i instalaci knihovny a jejích hlavičkových soubor, nyní ovšem s právy superuživatele:
$ sudo make install
10. Vytvoření a zavolání korutin
Základním úkolem, v němž knihovnu libmill využijeme, bude přepsání následujícího prográmku takovým způsobem, aby se funkce foo a bar zavolaly každá ve své vlastní korutině:
#include <stdio.h> void foo() { puts("foo"); } void bar() { puts("bar"); } int main(void) { foo(); bar(); return 0; }
Prvním přiblížením může být použití makra go, které má podobný význam, jako stejně pojmenované klíčové slovo go v programovacím jazyce Go. Makro go se zapisuje podobně jako volání funkce, ovšem samotné volání je provedeno (v obecném případě) souběžně:
#include <stdio.h> #include "libmill.h" void foo() { puts("foo"); } void bar() { puts("bar"); } int main(void) { go(foo()); go(bar()); return 0; }
Ve skutečnosti však není výše uvedený kód za všech okolností korektní, protože korutiny pracují s vlastními daty atd. Pro zcela korektní chování je nutné před funkce volané v korutinách přidat slovo coroutine:
#include <stdio.h> #include "libmill.h" coroutine void foo() { puts("foo"); } coroutine void bar() { puts("bar"); } int main(void) { go(foo()); go(bar()); return 0; }
Překlad tohoto příkladu se provede způsobem:
$ gcc test1.c -lmill -L{cesta_k_přeložené_knihovně_libmill}
Před spuštěním je nutné zajistit, aby se našla dynamicky linkovaná knihovna libmill:
$ export LD_LIBRARY_PATH={cesta_k_přeložené_knihovně_libmill} $ ./a.out
Pro zajímavost se můžete podívat na to, jak vlastně program vypadá po zpracování preprocesorem:
$ cpp test1.c
11. Vytvoření většího množství korutin bez vzájemné synchronizace
Nic nám pochopitelně nebrání použít jednu a tu samou funkci ve více korutinách, což je ukázáno v následujícím demonstračním příkladu:
#include <stdio.h> #include "libmill.h" coroutine void print(char what) { putchar(what); } int main(void) { setvbuf(stdout, (char*)NULL, _IONBF, 0); char ch; for (ch='a'; ch<='z'; ch++) { go(print(ch)); } return 0; }
12. Koncept deadline
V knihovně libmill se setkáme i s konceptem deadline. Jedná se o absolutní časový údaj určující, v jakém okamžiku by měla být nějaká činnost ukončena. V jiných knihovnách se většinou setkáme spíše s timeoutem, což je relativní časový údaj, tedy za jak dlouho by měla být činnost ukončena. Rozdíly mezi oběma koncepty jsou většinou z pohledu konstrukce programu minimální (jen si to nesmíme poplést). Příkladem použití timeoutu může být standardní funkce:
unsigned int sleep(unsigned int seconds);
V této funkci se parametrem specifikuje, na jak dlouho má být aktuální vlákno uspáno, což znamená, že pokud budeme vždy volat tuto funkci s parametrem 1, bude vlákno uspáno na přibližně jednu sekundu, nezávisle na tom, zda byla funkce zavolána nyní, či před hodinou.
Naproti tomu v knihovně libmill existuje funkce:
void msleep(int64_t deadline);
V níž se určuje absolutní čas zadaný v tomto případě pro větší přesnost v milisekundách. Pokud budeme chtít (v tomto případě korutinu) uspat na jednu sekundu, je následující volání špatně, protože 1000 znamená absolutní čas, konkrétně jednu sekundu na samotném počátku epochy:
msleep(1000);
Korektní volání by mělo používat funkci now() vracející aktuální čas:
msleep(now() + 1000);
Použití deadline si můžeme ukázat na následujícím příkladu, který většinou skončí dříve, než stačí korutiny vypsat všechny znaky:
#include <stdio.h> #include "libmill.h" coroutine void print(char what) { msleep(now() + 1000); putchar(what); } int main(void) { setvbuf(stdout, (char*)NULL, _IONBF, 0); char ch; for (ch='a'; ch<='z'; ch++) { go(print(ch)); } return 0; }
Další, tentokrát nepatrně složitější příklad, v němž je čekání vloženo i do hlavní korutiny:
#include <stdio.h> #include "libmill.h" coroutine void print(char what) { int i; for (i=0; i<10; i++) { msleep(now() + 100); putchar(what); } } int main(void) { setvbuf(stdout, (char*)NULL, _IONBF, 0); char ch; for (ch='a'; ch<='z'; ch++) { go(print(ch)); } msleep(now() + 200); return 0; }
13. Přepnutí běhu na jinou korutinu
V knihovně libmill nalezneme i funkci:
void yield(void);
V případě, že je tato funkce zavolána v nějaké korutině, provede se přepnutí kontextu na jinou korutinu a stávající korutina je pozastavena:
#include <stdio.h> #include "libmill.h" coroutine void print(char what) { int i; for (i=0; i<10; i++) { putchar(what); yield(); msleep(now() + 100); } } int main(void) { setvbuf(stdout, (char*)NULL, _IONBF, 0); char ch; for (ch='a'; ch<='z'; ch++) { go(print(ch)); } msleep(now() + 200); return 0; }
14. Použití kanálů pro komunikaci mezi korutinami
V knihovně libmill se pro komunikaci mezi korutinami používají kanály, a to podobným způsobem, jako v již zmíněném Go či Clojure (sémantika je prakticky shodná, liší se pouze syntaxe). Musíme ovšem brát ohledy na to, že se v céčku musíme sami postarat o manuální uvolňování všech prostředků, což se týká i kanálů. Podporovány jsou tyto funkce a makra:
Funkce/makro | Stručný popis | V Go |
---|---|---|
chmake | vytvoření kanálu s uvedením typu hodnot, které se mohou přenášet | v Go je to řešeno přes make |
chs | poslání hodnoty do kanálu (send) | v Go existuje operátor |
chr | přečtení hodnoty z kanálu (receive) | v Go existuje operátor |
chdone | ukončení zápisu do kanálu | |
chclose | dealokace a uzavření kanálu | close() |
chdup | duplikace reference na kanál (nemáme GC) | řešeno v runtime |
15. Nejjednodušší použití kanálu
Následuje velmi jednoduchý příklad, v němž je použit jeden kanál pro komunikaci mezi dvojicí korutin. Korutina představovaná funkcí sender pošle do kanálu celočíselnou hodnotu odpovídající znaku ‚*‘ a následně kanál uzavře. Hlavní korutina na tuto hodnotu čeká a teprve po jejím příjmu přijatou hodnotu vypíše a ukončí se:
#include <stdio.h> #include "libmill.h" coroutine void sender(chan channel) { msleep(now() + 1000); chs(channel, int, '*'); chclose(channel); } int main() { chan channel = chmake(int, 0); puts("vytvoreni korutiny"); go(sender(chdup(channel))); puts("cekani na zpravu z korutiny"); char c = chr(channel, int); puts("prijato"); putchar(c); chclose(channel); return 0; }
16. Obdoba konstrukce select
V kapitole o Go jsme si mj. ukázali i programovou konstrukci typu select, která dokáže vybrat/poslat data do právě volného kanálu:
select { case <-ch1: fmt.Println("Data z kanálu 1") case <-ch2: fmt.Println("Data z kanálu 2") default: fmt.Println("Žádná data nejsou k dispozici") }
Tato konstrukce sice pochopitelně v programovacím jazyku C neexistuje, ale pomocí makrosystému ji lze do jisté míry nasimulovat. Pro kanály, ze kterých se provádí čtení by vypadala následovně:
choose { in(ch1, int, val): puts("Data z kanálu 1"); in(ch2, int, val): puts("Data z kanálu 2"); otherwise: puts("Žádná data nejsou k dispozici"); end }
Rozdíly jsou skutečně jen syntaktické, protože v C musíme explicitně určovat typ čtených dat a nepoužívat klíčová slova case a default, která jsou v Go „přetížena“.
Lze pochopitelně použít jak vstupní, tak i výstupní kanály:
choose { in(ch1, int, val): puts("Data z kanálu 1"); out(ch2, int, val): puts("Data poslána do kanálu 2"); ... ... ... otherwise: puts("Žádná data nejsou k dispozici"); end }
17. Použití konstrukce typu select
V dnešním posledním demonstračním příkladu je ukázáno použití dvou korutin posílajících (nezávisle na sobě) data do hlavní korutiny (první korutina je nepatrně pomalejší než korutina druhá). Každá korutina přitom pro komunikaci používá vlastní kanál, což znamená, že hlavní korutina musí využít programovou konstrukci typu select. Příklad ovšem není zcela korektní – bylo by vhodné, aby se v hlavní korutině nepoužívala počítaná smyčka, ale aby se zjišťovalo, kdy jsou ostatní korutiny ukončeny. Nápravu provedeme příště:
#include <stdio.h> #include "libmill.h" coroutine void sender1(chan channel) { int i; for (i=0; i<10; i++) { msleep(now() + 1000); chs(channel, int, '*'); } chclose(channel); } coroutine void sender2(chan channel) { int i; for (i=0; i<10; i++) { msleep(now() + 800); chs(channel, int, '?'); } chclose(channel); } int main() { chan channel1 = chmake(int, 0); chan channel2 = chmake(int, 0); puts("vytvoreni korutin"); go(sender1(chdup(channel1))); go(sender2(chdup(channel2))); setvbuf(stdout, (char*)NULL, _IONBF, 0); puts("cekani na zpravy z korutin"); int i; for (i=0; i<100; i++) { choose { in(channel1, int, val): putchar(val); in(channel2, int, val): putchar(val); otherwise: putchar('-'); end } msleep(now() + 50); } chclose(channel1); chclose(channel2); return 0; }
Příklad výstupu získaného po spuštění tohoto demonstračního příkladu:
vytvoreni korutin cekani na zpravy z korutin ----------------?---*-----------?-------*-------?-----------*---?---------------?*--------------?---
18. Obsah následujícího článku
Knihovna libmill umožňuje provádět i mnohé další triky, například při práci se síťovými sockety apod. S těmito triky a na nich postavenými koncepty se seznámíme v navazujícím článku. Taktéž se pokusíme vizualizovat souběžnost práce korutin zápisem do framebufferu.
19. Repositář s demonstračními příklady
Zdrojové kódy všech dnes použitých demonstračních příkladů byly uloženy do nového Git repositáře, který je dostupný na adrese https://github.com/tisnik/concurrency-demos (stále na GitHubu :-). V případě, že nebudete chtít klonovat celý repositář (ten je ovšem – alespoň prozatím – velmi malý, dnes má přibližně jednotky kilobajtů), můžete namísto toho použít odkazy na jednotlivé demonstrační příklady, které naleznete v následující tabulce:
# | Příklad | Popis | Adresa |
---|---|---|---|
1 | test0.c | program bez korutin | https://github.com/tisnik/concurrency-demos/blob/master/libmill/test0.c |
2 | test1.c | základní použití korutin | https://github.com/tisnik/concurrency-demos/blob/master/libmill/test1.c |
3 | test2.c | vytvoření 26 korutin | https://github.com/tisnik/concurrency-demos/blob/master/libmill/test2.c |
4 | test3.c | použití funkcí msleep a now | https://github.com/tisnik/concurrency-demos/blob/master/libmill/test3.c |
5 | test4.c | použití funkcí msleep a now | https://github.com/tisnik/concurrency-demos/blob/master/libmill/test4.c |
6 | test5.c | použití yield pro předání řízení jiné korutině | https://github.com/tisnik/concurrency-demos/blob/master/libmill/test5.c |
7 | test6.c | komunikace s využitím kanálů | https://github.com/tisnik/concurrency-demos/blob/master/libmill/test6.c |
8 | test7.c | konstrukce typu select | https://github.com/tisnik/concurrency-demos/blob/master/libmill/test7.c |
20. Odkazy na Internetu
- Libmill: Go-style concurrency in C
http://libmill.org/ - Libmill na GitHubu
https://github.com/sustrik/libmill - libdill: Structured Concurrency for C
http://libdill.org/ - libdill na GitHubu
https://github.com/sustrik/libdill - Libdill: Structured Concurrency for C (2016)
https://news.ycombinator.com/item?id=15977983 - Asynchronní programování v Clojure s využitím knihovny core.async
https://www.root.cz/clanky/asynchronni-programovani-v-clojure-s-vyuzitim-knihovny-core-async/ - Asynchronní programování v Clojure s využitím knihovny core.async (pokračování)
https://www.root.cz/clanky/asynchronni-programovani-v-clojure-s-vyuzitim-knihovny-core-async-pokracovani/ - Asynchronní programování v Clojure s využitím knihovny core.async (dokončení)
https://www.root.cz/clanky/asynchronni-programovani-v-clojure-s-vyuzitim-knihovny-core-async-dokonceni/ - Communicating sequential processes (Wikipedia)
https://en.wikipedia.org/wiki/Communicating_sequential_processes - Go Block Best Practices
https://www.clojure.org/guides/core_async_go - Clojure core.async and Go: A Code Comparison
https://blog.drewolson.org/clojure-go-comparison - 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 - FLOSS Weekly 358: Libmill
https://www.youtube.com/watch?v=zUDhcN-8oQo - Structured Concurrency Finding our way out of callback hell
https://www.youtube.com/watch?v=llYv1AitDH8 - Go part 21: Goroutines
https://golangbot.com/goroutines/ - Go part 22: Channels
https://golangbot.com/channels/ - [Go] Lightweight eventbus with async compatibility for Go
https://github.com/asaskevich/EventBus - Goroutine IDs
https://blog.sgmansfield.com/2015/12/goroutine-ids/ - Different ways to pass channels as arguments in function in go (golang)
https://stackoverflow.com/questions/24868859/different-ways-to-pass-channels-as-arguments-in-function-in-go-golang - Select waits on a group of channels
https://yourbasic.org/golang/select-explained/ - Communicating Sequential Processes. The First 25 Years
https://www.springer.com/gp/book/9783540258131 - Dokumentace k Pythonovské knihovně goless
https://goless.readthedocs.io/en/latest/ - Coroutine
https://en.wikipedia.org/wiki/Coroutine - Coroutine z Boostu
https://www.boost.org/doc/libs/1_57_0/libs/coroutine/doc/html/index.html - Coroutine2 z Boostu
https://www.boost.org/doc/libs/1_61_0/libs/coroutine2/doc/html/index.html - Concurrency is not parallelism
https://blog.golang.org/concurrency-is-not-parallelism - Why is node.js asynchronous?
https://stackoverflow.com/questions/17607280/why-is-node-js-asynchronous - Stránka projektu node.js
https://nodejs.org/en/ - CSP (elektronická podoba)
http://www.usingcsp.com/