Heim >Backend-Entwicklung >Golang >Einführung in das Speichermodell der Go-Sprache
Das Speichermodell von Go beschreibt detailliert die Bedingungen, unter denen „ein Lesevorgang für eine Variable in einer Goroutine einen Schreibvorgang für die Variable in einer anderen Goroutine erkennen kann“.
Passiert vorher
Für eine Goroutine muss die Leistung der Lese- und Schreibvorgänge von Variablen so sein, wie es vom geschriebenen Code erwartet wird. Mit anderen Worten, ohne die Leistung des Programms zu ändern, können der Compiler und der Prozessor die Reihenfolge der Operationen von Variablen ändern, um den Code zu optimieren, das heißt: Anweisungen werden in der falschen Reihenfolge neu angeordnet.
Wenn jedoch zwei verschiedene Goroutinen mit derselben Variablen arbeiten, kann es sein, dass unterschiedliche Goroutinen aufgrund der Befehlsumordnung ein inkonsistentes Verständnis der Reihenfolge der Variablenoperationen haben. Wenn beispielsweise eine Goroutine a = 1; b = 2; ausführt, bemerkt eine andere Goroutine möglicherweise, dass Variable b vor Variable a geändert wird.
Um dieses Mehrdeutigkeitsproblem zu lösen, führt die Go-Sprache das Konzept des Vorhergehenden ein, das zur Beschreibung der Reihenfolge von Speicheroperationen verwendet wird. Wenn Ereignis e1 vor Ereignis e2 eintritt, sagen wir, dass Ereignis e2 nach e1 eintritt.
Wenn Ereignis e1 nicht vor Ereignis e2 und nicht nach e2 eintritt, sagen wir, dass die Ereignisse e1 und e2 gleichzeitig auftreten.
Für eine einzelne Goroutine stimmt die Reihenfolge der vorherigen Ereignisse mit der Reihenfolge des Codes überein.
Wenn die folgenden Bedingungen erfüllt sind, kann ein „Leseereignis r“ auf Variable v ein anderes „Schreibereignis w“ auf Variable v wahrnehmen:
1 „Schreibereignis w“ passiert vorher „Ereignis r lesen“.
2. Es gibt kein Schreibereignis w in die Variable v, das gleichzeitig nach w und vor r auftritt.
Um sicherzustellen, dass das Leseereignis r das Schreibereignis für die Variable v erkennen kann, müssen wir zunächst sicherstellen, dass w das einzige Schreibereignis für die Variable v ist. Gleichzeitig müssen folgende Bedingungen erfüllt sein:
1. „Ereignis w schreiben“ erfolgt vor „Ereignis r lesen“.
2. Andere Zugriffe auf die Variable v müssen vor „Write Event W“ oder nach „Read Event R“ erfolgen.
Der zweite Satz von Bedingungen ist strenger als der erste Satz von Bedingungen. Weil es erfordert, dass es im Programm keine anderen Lesevorgänge geben kann, bei denen w und r parallel ausgeführt werden.
Damit beide Bedingungssätze in einer einzigen Goroutine gleichwertig sind, stellen Leseereignisse sicher, dass Schreibereignisse in Variablen erkannt werden. Für die gemeinsam genutzte Variable v zwischen zwei Goroutinen müssen wir jedoch die Vorher-Zustand-Bedingung durch Synchronisierungsereignisse garantieren (dies ist eine notwendige Bedingung dafür, dass Leseereignisse Schreibereignisse erkennen).
Zu diesem Speicherbetriebsmodell gehört auch die automatische Initialisierung der Variablen v auf Null.
Lesen und schreiben Sie Daten, die die Länge eines Maschinenworts überschreiten, und die Reihenfolge ist nicht garantiert.
Synchronisation
Initialisierung
Die Initialisierung des Programms erfolgt in einer separaten Goroutine-Implementierung. Während der Initialisierung erstellte Goroutinen werden gestartet, nachdem die erste zur Initialisierung verwendete Goroutine die Ausführung abgeschlossen hat.
Wenn Paket p Paket q importiert, wird die Initialisierungsfunktion init von Paket q vor der Initialisierung von Paket p ausgeführt.
Die Einstiegsfunktion main.main des Programms wird gestartet, nachdem alle Init-Funktionen ausgeführt wurden.
Neu erstellte Goroutinen in einer beliebigen Init-Funktion werden ausgeführt, nachdem alle Init-Funktionen abgeschlossen sind.
Erstellung von Goroutine
Die go-Anweisung, mit der die Goroutine gestartet wird, läuft vor der Goroutine.
Zum Beispiel das folgende Programm:
var a string; func f() { print(a); } func hello() { a = "hello, world"; go f(); }
Durch Aufrufen der Funktion „Hallo“ wird zu einem bestimmten Zeitpunkt „Hallo Welt“ ausgegeben (möglicherweise nach der Rückkehr der Funktion „Hallo“).
Kanalkommunikation Pipe-Kommunikation
Pipeline-Kommunikation ist die Hauptmethode zur Synchronisierung zwischen zwei Goroutinen. Im Allgemeinen führen verschiedene Goroutinen Lese- und Schreibvorgänge in derselben Pipe aus, wobei eine Goroutine in die Pipe schreibt und die andere Goroutine Daten aus der Pipe liest.
Der Sendevorgang auf der Pipe erfolgt, bevor der Empfang der Pipe abgeschlossen ist.
Zum Beispiel kann dieses Programm:
var c = make(chan int, 10) var a string func f() { a = "hello, world"; c <- 0; } func main() { go f(); <-c; print(a); }
dafür sorgen, dass „hello, world“ ausgegeben wird. Da die Zuweisung von a erfolgt, bevor Daten an Pipe c gesendet werden, und der Sendevorgang der Pipe erfolgt, bevor der Pipe-Empfang abgeschlossen ist. Daher wurde beim Drucken ein zugewiesen.
Empfangen Sie Daten von einer ungepufferten Pipe, bevor Sie Daten an die Pipe senden.
Das Folgende ist ein Beispielprogramm:
var c = make(chan int) var a string func f() { a = "hello, world"; <-c; } func main() { go f(); c <- 0; print(a); }
kann auch die Ausgabe von „hello, world“ sicherstellen. Da die Zuweisung von a erfolgt, bevor Daten von der Pipe empfangen werden, und der Vorgang zum Empfangen von Daten von der Pipe erfolgt, bevor das Senden an die ungepufferte Pipe abgeschlossen ist. Daher wurde beim Drucken ein zugewiesen.
Wenn Sie eine gepufferte Pipe verwenden (z. B. c = make(chan int, 1)), gibt es keine Garantie dafür, dass das Ergebnis „Hallo, Welt“ ausgegeben wird (es kann eine leere Zeichenfolge sein, aber es wird auf keinen Fall eine unbekannte Zeichenfolge sein oder zum Absturz des Programms führen).
Sperre
Paketsynchronisierung implementiert zwei Arten von Sperren: sync.Mutex und sync.RWMutex.
对于任意 sync.Mutex 或 sync.RWMutex 变量l。 如果 n < m ,那么第n次 l.Unlock() 调用在第 m次 l.Lock()调用返回前发生。
例如程序:
var l sync.Mutex var a string func f() { a = "hello, world"; l.Unlock(); } func main() { l.Lock(); go f(); l.Lock(); print(a); }
可以确保输出“hello, world”结果。因为,第一次 l.Unlock() 调用(在f函数中)在第二次 l.Lock() 调用(在main 函数中)返回之前发生,也就是在 print 函数调用之前发生。
For any call to l.RLock on a sync.RWMutex variable l, there is an n such that the l.RLock happens (returns) after the n'th call to l.Unlock and the matching l.RUnlock happens before the n+1'th call to l.Lock.
Once
包once提供了一个在多个goroutines中进行初始化的方法。多个goroutines可以 通过 once.Do(f) 方式调用f函数。但是,f函数 只会被执行一次,其他的调用将被阻塞直到唯一执行的f()返回。once.Do(f) 中唯一执行的f()发生在所有的 once.Do(f) 返回之前。
有代码:
var a string func setup() { a = "hello, world"; } func doprint() { once.Do(setup); print(a); } func twoprint() { go doprint(); go doprint(); }
调用twoprint会输出“hello, world”两次。第一次twoprint 函数会运行setup唯一一次。
错误的同步方式
注意:变量读操作虽然可以侦测到变量的写操作,但是并不能保证对变量的读操作就一定发生在写操作之后。
例如:
var a, b int func f() { a = 1; b = 2; } func g() { print(b); print(a); } func main() { go f(); g(); }
函数g可能输出2,也可能输出0。
这种情形使得我们必须回避一些看似合理的用法。
这里用Double-checked locking的方法来代替同步。在例子中,twoprint函数可能得到错误的值:
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(); }
在doprint函数中,写done暗示已经给a赋值了,但是没有办法给出保证这一点,所以函数可能输出空的值。
另一个错误陷阱是忙等待:
var a string var done bool func setup() { a = "hello, world"; done = true; } func main() { go setup(); for !done { } print(a); }
我们没有办法保证在main中看到了done值被修改的同时也 能看到a被修改,因此程序可能输出空字符串。更坏的结果是,main 函数可能永远不知道done被修改,因为在两个线程之间没有同步操作,这样main 函数永远不能返回。
下面的用法本质上也是同样的问题.
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); }
即使main观察到了 g != nil 条件并且退出了循环,但是任何然 不能保证它看到了g.msg的初始化之后的结果。
更多go语言知识请关注PHP中文网go语言教程栏目。
Das obige ist der detaillierte Inhalt vonEinführung in das Speichermodell der Go-Sprache. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!