Go¶
A gyakorlat anyaga¶
Az eddigi gyakorlatokon a közös memóriás párhuzamos programozási modellel foglalkoztunk a Linux IPC és a Java segítségével. Ennek során a folyamatok ugyanazokat a változókat használhatták, amelyeket akár egyidőben is megkísérelhettek olvasni és/vagy írni.
Most áttérünk az osztott memóriás párhuzamos programozásra. Általánosságban ez azt jelenti, hogy minden folyamat saját változókészlettel rendelkezik, ezeket kizárólagosan használják, azaz nem kell kezelnünk az egyidejű hozzáférést. Ebben az esetben a párhuzamosan futó folyamatok közötti kommunikáción, illetve a közöttük történő szinkronizáción (információcserén!) van a hangsúly.
A folyamatok együttműködése üzenetküldéssel történik, így folyamatok kezelése mellett szükség van olyan utasításokra, amelyek az üzenetek kezelését teszik lehetővé. Ehhez kapcsolódónak a következő fogalmak:
Az első a közvetlen és a közvetett címzés. Közvetlen címzés esetén szükség van arra, hogy a folyamatok létrehozáskor egyértelmű folyamatazonosítót kapjanak a rendszerben, ez alapján tudjuk megmondani hogy honnan hova küldjük az üzenetet. Közvetett címzés esetén nincs szükség folyamatazonosítókra, ehelyett csatornán keresztül történik a kommunikáció, a folyamatnak pedig csak a csatorna egyik végét kell látnia.
A második pedig a szinkron és aszinkron kommunikáció. Szinkron kommunikációnál a küldő bevárja a fogadót, annak megérkezésekor megtörténik az adatátvitel, majd mindkét folyamat tovább folytatja futását. Értelemszerűen aszinkron kommumnikációnál küldő folyamat nem várja be a fogadót, hanem folytatja futását. De van lehetőségünk szinkron kommunikációt aszinkron kommunikációva alakítani és fordítva is.
Golang¶
A Google által tervezett és karbantartott Go programozási nyelv leginkább a C/C++ nyelvre hasonlító szintaxissal rendelkezik, így például a kommentezési lehetőségek megegyeznek (//
egysoros, /* */
többsoros komment), de eltérések mutatkoznak például a for ciklus vagy a feltételes vezérlés szintaxisában. Erősen típusos, de nem objektumorientált nyelv, azonban struct
és interface
segítségével hasonló működés előidézhető.
A go program futtatását egy szokásos Hello World!-jellegű példa segítségével mutatjuk be. Hozzunk létre egy könyvtárat, majd ebben a könyvtárban hozzunk létre egy .go
kiterjesztésű állományt és másoljuk bele a következő kódot. A nyelvben a package-ek csak logikai csoportosítást jelöl, viszont a main package megkülönböztetett csomag, mivel a program belépési pontját jelöli.
1 2 3 4 5 6 7 8 9 10 |
|
Navigáljunk a létrehozott könyvtárba és adjuk ki a következő utasítást (amennyiben a fájl neve hello.go):
go run hello.go
: az első futtatás esetenként lassabb lehet, mivel az utasítás hatására a go egy ideiglenes bináris fájlt hoz létre, amit aztán végrehajt
Egy másik megoldás, ha direkt módon build-eljük a bináris állományt, majd ezt követően futtatjuk:
-
go build hello.go
-
./hello
Továbbá fontos, hogy a A Go fordító alapértelmezetten hibát dob, ha egy csomagot importálsz vagy egy változót hozol létre, de nem használod azt a kódban.
Függvények, változók, tömbök és vezérlési szerkezetek¶
Go-ban lehetőség van "hagyományos" függvények létrehozására, de létrehozhatunk több visszatérési értékkel rendelkező függvényeket is, amelyeket tipikusan hibakezelés során használnak. Ezenkívül van lehetőség nevesített visszatérési értékekkel rendelkező függvények létrehozására is.
A nyelvben a változók láthatósága korlátozódik package szintre (globális változók), illetve egy adott függvényen belül érvényes változókra (lokális változók). Névegyezőség esetében a lokális írja felül a globális változót!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
|
Defer¶
A defer egy speciális funkció (és kulcssszó), amely egyfajta késleltetett végrehajtásra alkalmas. A defer kulcsszóval ellátott függvényhívás argumentumai azonnal kiértékelésre kerülnek, de a függvényhívás nem hajtódik azonnal végre. Több egymást követő, defer kulcsszóval meghívott utasítások egy LIFO végrehajtást eredményeznek a defer függvényeket hívó függvény visszatérése előtt.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
A defer hasznos kis konstrukció komplexebb függvények esetén, hiszen segítségével biztosítani tudunk egy függvényhívást pontosan azelőtt, amikor a defer utasítást tartalmazó függvény visszatérne. Így tehát érdemes lehet kiszervezni a defer függvénybe például az erőforrásokat (pl. I/O) lezáró hívásokat.
Go szálkezelés¶
A Go szálkezelését az ún. goroutine
valósítja meg, új goroutine
-t a go kulcsszó segítségével tudunk létrehozni úgy, hogy egy adott függvényhívás elé kiírjuk a kulcsszót. A hivatalos dokumentáció úgy hivatkozik rá, mint egy lightweight thread. Ezek a függvények nem blokkolják az őt meghívó függvény végrehajtását, a függvény visszatérési értéke pedig nem kerül tárolásra. Fontos még megjegyezni, hogy a goroutine-t használó programok ún. concurrent programok, tehát nem feltétlenül futnak párhuzamosan. A goroutine-oknak nincs saját szála, tehát valójában sok goroutine futhat egyetlen szálon időosztás segítségével. A Go futtatókörnyezet szükség szerint dinamikusan multiplexeli a gorutinokat a szálakra, hogy az egyes gorutinok folyamatosan fussanak.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
A goroutine
-ok esetén figyelnünk kell még arra, hogy a main függvény a meghívott goroutine
-ok bejeződését nem várja meg, a programunk terminálni fog. Anonim függvény is futtatható goroutine-ként. Elsőre egy nem túl szép, de hatásos megoldás lehet egyszerűbb programoknál, ha valamilyen felhasználói interakcióhoz kötjük a main terminálását, pl. beolvasunk egy értéket:
1 2 |
|
Egy másik lehetséges eszközt a sync package biztosítja. WaitGroup használata esetén lehetőségünk van goroutine-ok befejeződését megvárni egy adott pontján a programnak. 3 függvénye van:
-
Add(int)
: a WaitGroup számlálója, tipikusan a bevárandó goroutine-ok számával hívjuk meg -
Wait()
: várakozik, amíg a számláló 0 nem lesz -
Done()
: csökkenti a számlálót 1-gyel
Csatorna¶
A Go a közvetett címzést használja, tehát ahhoz, hogy a goroutine-ok kommunikálni (és szinkronizálni) tudjanak, szükség van csatornákra. Ez egy típusos konstrukció, amelyen értékeket tudunk küldeni és fogadni a csatornaoperátorral. Nincs külön operátor a küldésre és a fogadásra, hanem a csatornaazonosító és a csatornaopárator egymáshoz viszonyított sorrendje adja meg az adatfolyam irányát; c csatorna és x érték alapján:
c <- x
: x-et ráküldjük a csatornárax := <- c
: c csatornáról fogadunk egy értéket
Használatához még kettő kulcsszóra lesz szükségünk: make és a chan.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
A csatornán való küldés szinkronküldés, azaz a fogadó blokkolódik, amíg a másik oldal, azaz a küldő nem jut el a saját szinkronizációs pontjáig, ahol leolvassa a csatornán található értékeket. A csatorna megőrzi a csatornára küldött adatok sorrendjét.
Egyirányú és pufferelt csatorna¶
Go-ban lehetőségünk van meghatározni, hogy egy létrehozott csatornát az egyik folyamat mindig csak adat fogadására, míg egy másik folyamat mindig csak adat küldésére használja. Tehát lehetőségünk van a csatornát egyirányúsítani, amellyel így szigorúbb megkötéseket tudunk tenni, összetett program esetén segíti a kód struktúrálását, fordítási időben már megjelenő tervezési hibák felderítését:
-
func a(c chan<- string)
: a függvény agrumentuma egy olyan c csatorna, amelyre csak adatot küldünk -
func b(c <-chan string)
: a függvény argumentuma egy olyan c csatorna, amelyről csak adatot fogadunk
Egy gyakori jelenség, hogy a küldő már elküldte az adatot, de a fogadó még nem olvasta le azt a csatornáról azt, mivel nem jutott el a leolvasást vezérlő kódrészletig. Ilyen esetben a programunk futása megáll, hiszen a csatornán történő kommunikáció alapértelmezetten szinkron kommunikáció. Szerencsés esetben csupán rövid ideig tart a blokkolás, de akár holtpontba is kerülhet a programunk (pl. egy érték csatornára küldése miatt, ami sosem kerül leolvasásra).
Ennek a problémának - azaz mikor a szinkron kommunikáció blokkolásra késztet egy folyamatot - egy lehetséges megoldása lehet az ún. pufferelt csatorna. Egy pufferelt csatorna létrehozása esetén annak méretét is meg kell mondanunk. Adatküldés egy pufferelt csatornára csak akkor blokkolódik, ha a puffer tele van. Ha a puffer nem üres, akkor a fogadó nem vár egy újabb adat küldésére, hanem a buffer-ból olvassa ki a soronkövetkező értéket.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
|
A fogadó folyamatok tesztelhetik is, hogy egy csatorna bezárt-e (value lesz a csatornáról fogadott érték, az ok pedig hamis, ha a csatorna már korábban bezárásra került):
value, ok := <-ch
Select¶
A select az egyik legösszetettebb Go folyamat, az ún. "gyors várakozást" valósítja meg azáltal, hogy több bejövő csatornát figyel és a legelső kommunikációs kísérletre reagál, ehhez egy switch-szerű formát használ. Mivel a program írásakor nem feltétlen tudhatjuk, hogy melyik csatornáról fog adat érkezni, ezért az select-et tartalmazó programok nemdeterminisztikusak. A holtpont veszélye sincs kizárva, mert azt sem tudhatjuk előre, hogy a felsorolt csatornákra egyáltalán fog-e érkezni adat. Ennek kiküszöbölésére külön figyelmet kell fordítanunk.
A select blokkol, amíg adatot nem küldünk vagy nem fogadunk. Ha több csatorna is "kész" (azaz van adat küldésére vagy fogadására), akkor a select véletlenszerűen választ egyet, és azt végrehajtja. A select tartalmazhat default ágat is.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
|
További érdekesség, hogy a select adatküldésre is használható, például véletlen 0-1 sorozat küldése a csatornára (hiszen ha több is végrehajtható, akkor a végrehajthatók közül véletlenszerűen kerül kiválasztásra az ág, amelyik lefut):
1 2 3 4 5 6 |
|
Csővezeték¶
Csővezeték esetén egy láncban helyezzük el a folyamatokat, amelyek egy feldolgozás munkafázisait végzik el a rajtuk áthaladó adatokon. Egyidőben több adatelem van feldolgozás alatt, mindegyik másik munkafázisban. Egy adatelemnek a teljes feldolgozáshoz végig kell járnia sorrendben az összes munkafázist:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
|
Gyakorló feladat¶
(1) Implementáljunk egy statisztika készítő programot, ami egy tömb segítségével összegzi, hogy mennyi kis- és nagybetű, illetve szám karakter került lenyomásra, minden mást eldob. A program a TAB billentyű segítségével termináljon. A program a következő goroutine-okból álljon, amelyek egyirányú, kétszeresen pufferelt, BYTE csatorna segítségével kommunikáljon:
-
A: a standard input-ról olvas és szűri a megadott feltételek alapján a karaktereket,
-
B: minden egyes fogadott érték alapján a tömb megfelelő indexével jelölt értéket növeli 1-gyel