A Go programozási nyelv

Memóriamodell

Bevezetés

A Go memória modellje specifikálja azokat a feltételeket, melyek teljesülése mellett egy változó olvasása egy gorutinban garantáltan megfigyeli egy másik gorutinból az ugyanabba a változóba való írásokat.

Előbb történik

Egy gorutinon belül az olvasásoknak és az írásoknak úgy kell viselkedniük, mintha a program által előírt sorrendben hajtódnának végre. Azaz, a fordítók és a processzorok csak akkor cserélhetik fel az olvasások és az írások sorrendjét, ha sorrendmódosítás nem változtatja meg a viselkedést egy gorutinon belül. Ezen sorrendmódosítás miatt, a végrehajtási sorrendet máshogy érzékelheti egy gorutin egy másikhoz képest. Például, ha egy gorutin végrehajtja az a = 1; b = 2; parancsokat, egy másik gorutin a b frissített értékét előbb érzékelheti, mint az a frissített értékét.

Hogy specifikálhassuk az olvasások és írások követelményeit, definiáljuk az előtt történik fogalmát, egy parciális rendezést a memória műveletek végrehajtási sorrendjét egy Go programban. Ha e1 esemény az e2 esemény előtt történik, akkor azt mondjuk, hogy az e2 az e1 után történik. Továbbá, ha e1 nem történik az e2 előtt sem utána, akkor azt mondjuk, hogy az e1 és az e2 egyszerre történnek.

Egy gorutinon belül a előtt történik sorrendet a program határozza meg.

A v változó r olvasásának megengedett, hogy megfigyelje a v változó w írását, ha a következő két feltétel teljesül:

  1. w az r előtt történik.
  2. Nincs más w' írás a v-be, ami a w után, de r előtt történik.

Hogy garantálhassuk, hogy v-nek r olvasása megfigyeljen egy v-be történő w írást, biztosítani kell, hogy w az egyetlen írás, amit r megfigyelhet. Azaz, r garantáltan megfigyeli w-t, ha a következő két feltétel teljesül:

  1. w r előtt történik.
  2. Minden más v-be való írás vagy w előtt vagy r után történik.

Az utóbbi két feltétel erősebb mint az első: megköveteli, hogy nem történnek más írások egyszerre w-el illetve r-el.

Egy gorutinon belül, ahol nincs konkurencia, a két definíció azonos: egy r olvasás megfigyeli a legfrissebb w írást a v változóba. Ha több gorutin hozzáfér egy megosztott v változóhoz, akkor szinkronizációs eseményekkel kell létesíteniük az előtt-történik feltételeket, hogy biztosítsák, hogy az olvasások megfigyelik a kívánt írásokat.

A v változó inicializálása 0-val egy írásként viselkedik ebben a modellben.

Egy gépi szónál nagyobb olvasások/írások több gépi szó méretű műveletnek tekintődnek és nincs előírva ezek sorrendje.

Szinkronizáció

Inicializáció

A program inicializálása egy gorutinban fut le, és az ekkor létrejött gorutinok nem indulnak el addig, amíg az inicializáció be nem fejeződik.

Ha a p csomag importálja a q csomagot, akkor a q csomag inicializáló függvényei a p bármely függvénye előtt történnek.

A main.main függvény az összes init függvény lefutása után indul el.

Az init függvények során létrehozott gorutinok az init függvények befejezése után történnek.

Gorutin létrehozása

A go utasítás, amely elindít egy új gorutint, a gorutin kezdete előtt történik.

Például a következő programban:

var a string func f() { print(a) } func hello() { a = "hello, world" go f() }

helló függvény hívása után valamikor ki fog íródni a "hello, world" üzenet valamikor a jövőben (akár a hello függvény visszatérése után).

Goroutin megsemmisülése

A gorutin kilépése nem garantált, hogy bármely esemény előtt megtörténik. Például a következő programban:

var a string func hello() { go func() { a = "hello" }() print(a) }

az a változó írása nincs követve semmiféle szinkronizációs eseménnyel, így nem garantált, hogy az új értéke megfigyelhető lesz másik gorutinban. Sőt, egy agresszív fordító ki is törölheti az egész go utasítást.

Ha azt szeretnénk, hogy egy gorutin hatásai egy másik gorutinban is megfigyelhetőek legyenek, használjuk szinkronizációs szerkezeteket, mint például egy zár vagy egy csatorna, hogy egy relatív sorrendet létesítsünk.

Csatorna-kommunikáció

Csatorna-kommunikáció a fő szinkronizációs módszer gorutinok között. Minden egyes küldés egy csatornán párosítva van egy hozzátartozó fogadással többnyire egy másik gorutinban.

Minden küldés egy csatornán a hozzá tartozó fogadás előtt történik.

A következő program:

var c = make(chan int, 10) var a string func f() { a = "hello, world" c <- 0 } func main() { go f() <-c print(a) }

garantáltan kiírja a "hello, world" üzenetet. Az a-ba való írás a c-re való küldés előtt történik, ami a hozzátartozó fogadás előtt történik, ami a kiírás előtt történik.

Egy fogadás egy nem pufferelt csatornáról a csatornán lévő küldés befejezése előtt történik.

A következő program (ugyanaz, mint előbb, csak a küldés és fogadás fel van cserélve egy és nem pufferelt csatornát tartalmaz):

var c = make(chan int) var a string func f() { a = "hello, world" <-c } func main() { go f() c <- 0 print(a) }

is garantáltan kiírja a "hello, world" üzenetet. Az a-ba való órás a c-n való fogadás előtt történik, ami a hozzátartozó küldés előtt történik, ami a kiírás előtt történik.

Ha a csatorna pufferelt lett volna (c = make(chan int, 1)), akkor a programnak nem feltétlenül kellene kiírnia a "hello, world" üzenetet. (Üres üzenetet írhatna ki, nem "viszlát, világ"-ot, nem is szállhat el).

Zárak

A sync csomag implementál két zártípust: sync.Mutex és sync.RWMutex (olvasó-író mutex) típusokat.

Minden l sync.Mutex illetve sync.RWMutex típusú váltózóhoz és n < m-hez, az n-edik hívása az l.Unlock() függvénynek az l.Lock() függvény m-edik hívása előtt történik.

A következő program:

var l sync.Mutex var a string func f() { a = "hello, world" l.Unlock() } func main() { l.Lock() go f() l.Lock() print(a) }

garantáltan kiírja a "hello, world" üzenetet. Az l.Unlock() első hívása az l.Lock() második hívása előtt történik, ami a kiírás előtt történik.

A sync.RWMutex típusú l változó l.RLock() hívásához van egy olyan n, amire az l.RLock() a l.Unlock() n. hívása után történik meg és a hozzátartozó l.RUnlock() a (n+1). l.Lock() hívás előtt történik.

Once

A sync csomag egy biztonságos szerkezetet biztosít az inicializáláshoz több gorutinos esetekben is az Once típuson keresztül. Több szál is lefuttathatja a once.Do(f) függvényt egy konkrét f-re, de az f() csak egyszer fog lefutni, és a többi hívás blokkolni fog, amíg az f() vissza nem tér.

Az f()-nek egy hívása a once.Do(f)-ből a once.Do(f) előtt történik (előtt tér vissza az f).

A következő programban:

var a string var once sync.Once func setup() { a = "hello, world" } func doprint() { once.Do(setup) print(a) } func twoprint() { go doprint() go doprint() }

a kétszeres doprint hívás miatt a "hello, world" üzenet kétszer íródik ki. Az első hívás lefuttatja a setup függvényt egyszer, de a második doprint hívás nem futtatja le a setup()-ot.

Hibás szinkronizáció

Megjegyzendő, hogy egy r olvasás megfigyelhet egy w írást, amely egyszerre történik vele. Ha ez meg is esik, nem következik ebből, hogy az r utáni olvasások megfigyelhetnek w előtti írásokat.

A következő programban:

var a, b int func f() { a = 1 b = 2 } func g() { print(b) print(a) } func main() { go f() g() }

megtörténhet, hogy a g előbb 2-t majd 0-t ír ki.

Sőt, ez érvénytelenít néhány ismert kifejezést.

A duplán ellenőrzött zárás egy próbálkozás a szinkronizálás költségeinek a csökkentésére. Például a twoprint programot a következőképpen lehetne hibásan megírni:

var a string var done bool func setup() { a = "hello, world" done = true } func doprint() { if !done { once.Do(setup) } print(a) } func twoprint() { go doprint() go doprint() }

de itt nincs semmi garancia a doprint-ben arra, hogy done való írás megfigyelése implikálná az a-ba való írás megfigyelését. Ez a verzió kiírhat egy üres sztringet a "hello, world" helyett.

Egy másik hibás kifejezés a tevékeny várakozás egy értékre, mint például itt:

var a string var done bool func setup() { a = "hello, world" done = true } func main() { go setup() for !done { } print(a) }

Mint előbb, itt sincs semmi garancia, hogy a main függvényben a done írásának megfigyelési implikálná az a írásának megfigyelését, ezért ezt a program is egy üres sztringet is ki tudna írni. Sőt rosszabb, mivel nincs garancia arra, hogy a done-ba való írás valaha is meg lesz figyelve a main által, mivel nincs szinkronizációs esemény a két szál között. Nem garantált, hogy a ciklus a main-ben végezni fog.

Ennek egy finomabb változata a következő program:

type T struct { msg string } var g *T func setup() { t := new(T) t.msg = "hello, world" g = t } func main() { go setup() for g == nil { } print(g.msg) }

Még ha a main megfigyeli is a g != nil-t és kilép a ciklusból, nem garantált, hogy a g.msg inicializált értékét meg fogja figyelni.

Forrás: http://golang.org/doc/go_mem.html.