A Go programozási nyelv

Konkurrens programozás

Üzenetküldéses modell

A Go nyelv a konkurenciát a legtöbb esetben üzenetküldéssel, és nem osztott memória használatával valósítja meg. Erre a nyelvbe beépített csatorna típust használja. A csatornán keresztüli kommunikáció a szinkronizáció egyetlen módja a nyelvben.

A gorutinok

A gorutinok a legfontosabb, konkurenciát támogató eszközök a nyelvben. Ezek olyan függvények, amik közös címtérben, a többi gorutinnal párhuzamosan futnak. A fogalom hasonlít például a szálakhoz vagy a folyamatokhoz, de egyikkel sem egyezik meg pontosan.

A gorutinok az operációs rendszer különböző szálaira képződhetnek le, de ezt a nyelv kezeli, a komplexitás nagy részét elrejti a programozó elől. Egy függvényhívás új gorutinban indul, ha a neve elég a go kulcsszót tesszük. Azaz:

go függvénynév(paraméterlista)

Például:

go list.Sort()

Ilyenkor a rendezés külön gorutinban indul, nem blokkolja az őt meghívó gorutin futását. Amikor a hívott függvény visszatér, a gorutin automatikusan megszűnik. A visszatérési értékek, ha voltak, eldobásra kerülnek.

A go kulcsszó után függvény literált is írhatunk. Mivel ezek zárványok, a változókat, amiket a gorutin használ, nem szabadítja fel a szemétgyűjtő.

Szinkronizáció és kommunikáció

Különböző gorutinok közti szinkronizációra és kommunikációra csatorna változókon keresztül van lehetőség (ld. Csatorna fejezet). Az előző példa, szinkronizációval kiegészítve:

c := make(chan int) // csatorna létrehozása // rendezi a listát és egy csatornán jelzi, amikor elkészült go func() { list.Sort() c <- 1 // az érték nem számít }() doSomethingForAWhile() <-c // csak szinkronizációra használja a csatornát, a küldött értéket nem használja fel

Az adatok fogadása egy csatornáról blokkol, ha a csatorna üres. Adatok küldése nem bufferelt csatornáknál akkor blokkol, ha már van valami a csatornán, egyébként csak ha megtelt a buffer.

A csatornák szemaforként is használhatók, ha például egy buffer nélküli csatornára egy gorutin egy értéket tesz, a kritikus szekció után pedig leveszi azt, a függvény többi példánya nem léphet be vele egyszerre a kritikus szekcióba, mert a csatorna tele van, azaz blokkol. Bufferelt csatornával megvalósítható, hogy tetszőleges számú gorutin lehessen egyszerre a szekcióban.

var sem = make(chan int, MaxOutstanding) func handle(r *Request) { sem <- 1 // Megvárja, hogy legyen hely a csatorna bufferében process(r) // Kritikus szekció <-sem // Végzett, kivesz egy elemet a csatornából } func Serve(queue chan *Request) { for { req := <-queue go handle(req) // nem várja meg, hogy a kezelő végezzen } }

Mivel a csatornákon akár csatornákat is át lehet küldeni, gyakori, hogy a dolgozó gorutin ilyen módon kapja meg azt a csatornát, amelyiken az eredményt vissza kell küldenie a hívónak.

Párhuzamosítás

A leírás készítésekor (2012. május), a nyelv implementációi alapbeállításként nem párhuzamosítják a programokat. Ha több processzort is ki akarunk használni, azt explicit közölni kell a fordítóval vagy a futtató környezettel. Ha n darab processzort szeretnénk használni, a következőket tehetjük:

Segítségünkre lehet a runtime.NumCPU() függvény, ami a gépen található logikai processzorok számát adja vissza.

Sajnos a követelmény még a Go v1.0-ban is fennáll, de a nyelv későbbi implementációiban várhatóan megszűnik majd.

Alacsonyabb szintű konkurrencia - a sync csomag

Atomi műveletek gyűjteménye - sync/atomic

Az atomic csomag alacsonyszintű atomi primitív memóriaműveleteket nyújt, melyek szinkronizációs algoritmusok megvalósításánál lehetnek hasznosak. A csomagban található műveletek használata nagy odafigyelést igényel, ezért azok általános célú használata kerülendő, helyette csatornákon keresztüli kommunikációval érdemes szinkronizálni. A pontos magyarázata nem található meg a csomag dokumentációjában, az ok ami miatt ez a figyelmeztetés jelen van az, hogy ezek az utasítások önmagukban véve nem jól szinkronizáltak, csak annyit garantálnak, hogy a memóriaterületeken elvégzett műveletek nem interferálnak párhuzamosan végrehajtódó utasításokkal, viszont a műveletek eredményének a goroutine-okban való láthatósága (observing) nem garantált, azaz az elvégzett írás nem áll happens-before relációban az esetleges későbbi olvasásokkal.

A lentebb található függvények több típusra is elérhetőek azonos jelentéssel, ezért csak a szignatórájuk sémáját írom le. Ezekre a típusokra elérhetőek:

Add

Atomi összeadásművelet (a 32 bites adatmozgatás atomicitása architektúránként változhat, ezért erre is meg kellett valósítani).

func Add[típus](addr *[típus], delta [típus]) (new [típus])

A neki megfelelő nem atomi utasítássorozat:

*addr += delta return *addr

CompareAndSwap

Egy adott memóriacímen felcserél két értéket, hogyha a címen található érték megegyezik az átadott régi értékkel, majd visszatér a csere sikerességét jelentő bool értékkel.

func CompareAndSwap[típus](addr *[típus], old, new [típus]) (swapped bool)

A neki megfelelő nem atomi utasítássorozat:

if *addr == old { *addr = new return true } return false

Load

Az addr által mutatott memóriacímen található érték atomi betöltése és visszaadása visszatérési értékként.

func Load[típus](addr *[típus]) (val [típus])

Store

A val változó értékének atomi eltárolása az addr memóriacímen.

func Store[típus](addr *[típus], val [típus])

Swap

A CompareAndSwap ellenőrzés nélküli változata, egy addr által mutatott címre betölti az új értéket és visszatér a korábban ott lévő értékkel.

func Swap[típus](addr *[típus], new [típus]) (old [típus])

A neki megfelelő nem atomi utasítássorozat:

old = *addr *addr = new return old

Randevú, feltétel változó (condition variable) - Cond

A Cond típus a goroutine-ok közötti randevú típusú szinkronzációt valósít meg. Egy eseményre várakozást, vagy az adott esemény bejelentését (várakozó goroutine-ok értesítését) teszi lehetővé. Egy Cond objektumot a használatának megkezdése után nem szabad másolni, csak cím szerint átadni.

Műveletei

Használat (számítási szálak várakoztatása, majd egyszerre elindítása):
var m sync.Mutex c := sync.NewCond(&m) for i:=0 ; i < runtime.GOMAXPROCS(0); i++ { go func() { c.Wait() Compute() }() } PrepareComputation() c.Broadcast()

Egyszer végrehajtható műveletek

A Once osztály egy művelet számára biztosítja, hogy az csak egyetlen egyszer fut le még konkurrens környezetben is.

Használat (az "Only once" csak egyszer íródik ki):
var once sync.Once onceBody := func() { fmt.Println("Only once") } done := make(chan bool) for i := 0; i < 10; i++ { go func() { once.Do(onceBody) done <- true }() } for i := 0; i < 10; i++ { <-done }

Kölcsönös kizárás, zárolás - Mutex, RWMutex

A konkurrens programozásban az alábbi két alapvető zárolási módszer terjedt el: Ez a két típus valósítja meg Go-ban a Locker interface-t:
type Locker interface { Lock() Unlock() }
sync.Mutex
Műveletei

sync.RWMutex

Olvasási és írási szintek elkülönítése, az RWMutex-ünkkel védeni kívánt objektumhoz tetszőleges számú olvasó hozzáférhet egyszerre, az olvasások csak az írást zárják ki, viszont ha írásra zárolják, akkor az minden további írást vagy olvasást is kizár a feloldásig. Az olvasási zár írási zárrá upgrade-je nem támogatott, a Go 1 memóriamodelljében és az RWMutex jelenlegi implementációjában nem valósítható meg atomi RUnlockAndLock művelet (részletek).

Műveletei

Csoportos várakozás - WaitGroup

A WaitGroup egy olyan szinkronizációs típus, mellyel egyszerre több goroutine befejeződését (vagy egy adott álapotba jutását) lehet bevárni.
Műveletei

Használat (http kérések párhuzamos lebonyolítása és bevárása, enélkül a főprogram várakozás nélkül kilépne):
var wg sync.WaitGroup var urls = []string{ "http://www.golang.org/", "http://www.google.com/", "http://www.somestupidname.com/", } for _, url := range urls { // Increment the WaitGroup counter. wg.Add(1) // Launch a goroutine to fetch the URL. go func(url string) { // Decrement the counter when the goroutine completes. defer wg.Done() // Fetch the URL. http.Get(url) }(url) } // Wait for all HTTP fetches to complete. wg.Wait()