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.
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:
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:
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.
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.
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:
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).
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:
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ó 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:
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):
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).
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:
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.
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:
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.
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:
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:
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:
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:
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.