Go’s sync Package: Ein Satz von Concurrency-Synchronisationstechniken
Takashi Yamamoto
Infrastructure Engineer · Leapcell

Detaillierte Erläuterung des sync
Standardbibliothekspakets in der Go-Sprache
Im Concurrent Programming der Go-Sprache bietet das sync
Standardbibliothekspaket eine Reihe von Typen zur Implementierung von Concurrent Synchronization. Diese Typen können unterschiedliche Speicherordnungsanforderungen erfüllen. Im Vergleich zu Kanälen ist die Verwendung in bestimmten Szenarien nicht nur effizienter, sondern macht auch die Codeimplementierung prägnanter und übersichtlicher. Im Folgenden werden die verschiedenen häufig verwendeten Typen im sync
-Paket und ihre Verwendungsmethoden detailliert vorgestellt.
1. sync.WaitGroup
Typ (Wait Group)
Die sync.WaitGroup
wird verwendet, um die Synchronisation zwischen Goroutinen zu erreichen, sodass eine oder mehrere Goroutinen auf den Abschluss mehrerer anderer Goroutinen warten können. Jeder sync.WaitGroup
-Wert verwaltet intern einen Zähler, und der anfängliche Standardwert dieses Zählers ist Null.
1.1 Methodeneinführung
Der Typ sync.WaitGroup
enthält drei Kernmethoden:
Add(delta int)
: Wird verwendet, um den von derWaitGroup
verwalteten Zähler zu ändern. Wenn eine positive Ganzzahldelta
übergeben wird, erhöht sich der Zähler um den entsprechenden Wert; wenn eine negative Zahl übergeben wird, verringert sich der Zähler um den entsprechenden Wert.Done()
: Ist eine äquivalente Abkürzung fürAdd(-1)
und wird normalerweise verwendet, um den Zähler um 1 zu verringern, wenn eine Goroutinenaufgabe abgeschlossen ist.Wait()
: Wenn eine Goroutine diese Methode aufruft und der Zähler Null ist, ist diese Operation ein No-Op (keine Operation); wenn der Zähler eine positive Ganzzahl ist, wechselt die aktuelle Goroutine in einen blockierten Zustand und wechselt erst dann wieder in den laufenden Zustand, wenn der Zähler Null wird, d. h. die MethodeWait()
gibt zurück.
Es ist zu beachten, dass wg.Add(delta)
, wg.Done()
und wg.Wait()
Abkürzungen für (&wg).Add(delta)
, (&wg).Done()
bzw. (&wg).Wait()
sind. Wenn der Aufruf von Add(delta)
oder Done()
dazu führt, dass der Zähler negativ wird, wird das Programm in Panik geraten.
1.2 Anwendungsbeispiel
package main import ( "fmt" "math/rand" "sync" "time" ) func main() { rand.Seed(time.Now().UnixNano()) // Erforderlich vor Go 1.20 const N = 5 var values [N]int32 var wg sync.WaitGroup wg.Add(N) for i := 0; i < N; i++ { i := i go func() { values[i] = 50 + rand.Int31n(50) fmt.Println("Done:", i) wg.Done() // <=> wg.Add(-1) }() } wg.Wait() // Es wird garantiert, dass alle Elemente initialisiert werden. fmt.Println("values:", values) }
Im obigen Beispiel setzt die Haupt-Goroutine den Zähler der Wait Group über wg.Add(N)
auf 5 und startet dann 5 Goroutinen. Jede Goroutine ruft wg.Done()
auf, um den Zähler um 1 zu verringern, nachdem die Aufgabe abgeschlossen ist. Die Haupt-Goroutine ruft wg.Wait()
auf, um zu blockieren, bis alle 5 Goroutinen ihre Aufgaben abgeschlossen haben und der Zähler 0 wird, und fährt dann mit der Ausführung des nachfolgenden Codes fort, um die Werte jedes Elements auszugeben.
Darüber hinaus kann der Aufruf der Methode Add
auch mehrmals aufgeteilt werden, wie unten gezeigt:
... var wg sync.WaitGroup for i := 0; i < N; i++ { wg.Add(1) // Wird 5 Mal ausgeführt i := i go func() { values[i] = 50 + rand.Int31n(50) wg.Done() }() } ...
Die Wait
-Methode eines *sync.WaitGroup
-Wertes kann in mehreren Goroutinen aufgerufen werden. Wenn der von dem entsprechenden sync.WaitGroup
-Wert verwaltete Zähler auf 0 sinkt, erhalten alle diese Goroutinen die Benachrichtigung und beenden den blockierten Zustand.
func main() { rand.Seed(time.Now().UnixNano()) // Erforderlich vor Go 1.20 const N = 5 var values [N]int32 var wgA, wgB sync.WaitGroup wgA.Add(N) wgB.Add(1) for i := 0; i < N; i++ { i := i go func() { wgB.Wait() // Auf die Broadcast-Benachrichtigung warten log.Printf("values[%v]=%v \n", i, values[i]) wgA.Done() }() } // Die folgende Schleife wird garantiert vor einer der obigen ausgeführt // wg.Wait-Aufrufe enden. for i := 0; i < N; i++ { values[i] = 50 + rand.Int31n(50) } wgB.Done() // Eine Broadcast-Benachrichtigung senden wgA.Wait() }
Die WaitGroup
kann nach der Rückgabe der Wait
-Methode wiederverwendet werden. Es ist jedoch zu beachten, dass, wenn die Basiszahl, die vom WaitGroup
-Wert verwaltet wird, Null ist, der Aufruf der Add
-Methode mit einem positiven ganzzahligen Argument nicht gleichzeitig mit dem Aufruf der Wait
-Methode ausgeführt werden darf, da sonst ein Data-Race-Problem auftreten kann.
2. sync.Once
Typ
Der Typ sync.Once
wird verwendet, um sicherzustellen, dass ein Codeabschnitt in einem Concurrent-Programm nur einmal ausgeführt wird. Jeder *sync.Once
-Wert hat eine Do(f func())
-Methode, die einen Parameter vom Typ func()
akzeptiert.
2.1 Methodeneigenschaften
Für einen adressierbaren sync.Once
-Wert o
kann der Aufruf der Methode o.Do()
(d. h. die Abkürzung von (&o).Do()
) mehrmals gleichzeitig in mehreren Goroutinen ausgeführt werden, und die Argumente dieser Methodenaufrufe sollten (sind aber nicht zwingend) derselbe Funktionswert sein. Von diesen Aufrufen wird nur eine der Argumentfunktionen (Werte) aufgerufen, und es wird garantiert, dass die aufgerufene Argumentfunktion beendet wird, bevor ein o.Do()
-Methodenaufruf zurückkehrt, d. h. der Code innerhalb der aufgerufenen Argumentfunktion wird ausgeführt, bevor ein o.Do()
-Methodenaufruf den Aufruf zurückgibt.
2.2 Anwendungsbeispiel
package main import ( "log" "sync" ) func main() { log.SetFlags(0) x := 0 doSomething := func() { x++ log.Println("Hello") } var wg sync.WaitGroup var once sync.Once for i := 0; i < 5; i++ { wg.Add(1) go func() { defer wg.Done() once.Do(doSomething) log.Println("world!") }() } wg.Wait() log.Println("x =", x) // x = 1 }
Im obigen Beispiel rufen zwar 5 Goroutinen alle once.Do(doSomething)
auf, aber die Funktion doSomething
wird nur einmal ausgeführt. Daher wird "Hello" nur einmal ausgegeben, während "world!" 5 Mal ausgegeben wird, und "Hello" wird definitiv vor allen 5 "world!"-Ausgaben ausgegeben.
3. sync.Mutex
(Mutex Lock) und sync.RWMutex
(Read-Write Lock) Typen
Wobei die Typen *sync.Mutex
und *sync.RWMutex
das Interface sync.Locker
implementieren. Daher beinhalten diese zwei Typen die Methoden Lock()
und Unlock()
, die verwendet werden, um Daten zu schützen und zu verhindern, dass sie gleichzeitig von mehreren Benutzern gelesen und geändert werden.
3.1 sync.Mutex
(Mutex Lock)
- Grundlegende Eigenschaften: Der Nullwert eines
Mutex
ist ein nicht gesperrter Mutex. Für einen adressierbarenMutex
-Wertm
kann er nur dann erfolgreich gesperrt werden, wenn er sich im nicht gesperrten Zustand befindet, indem die Methodem.Lock()
aufgerufen wird. Sobald der Wertm
gesperrt ist, führt ein neuer Sperrversuch dazu, dass die aktuelle Goroutine in einen blockierten Zustand wechselt, bis sie durch den Aufruf der Methodem.Unlock()
entsperrt wird.m.Lock()
undm.Unlock()
sind Abkürzungen für(&m).Lock()
bzw.(&m).Unlock()
. - Nutzungsbeispiel
package main import ( "fmt" "runtime" "sync" ) type Counter struct { m sync.Mutex n uint64 } func (c *Counter) Value() uint64 { c.m.Lock() defer c.m.Unlock() return c.n } func (c *Counter) Increase(delta uint64) { c.m.Lock() c.n += delta c.m.Unlock() } func main() { var c Counter for i := 0; i < 100; i++ { go func() { for k := 0; k < 100; k++ { c.Increase(1) } }() } // Diese Schleife dient nur Demonstrationszwecken. for c.Value() < 10000 { runtime.Gosched() } fmt.Println(c.Value()) // 10000 }
Im obigen Beispiel verwendet die Counter
-Struktur das Mutex
-Feld m
, um sicherzustellen, dass nicht gleichzeitig von mehreren Goroutinen auf das Feld n
zugegriffen und es geändert wird, wodurch die Konsistenz und Korrektheit der Daten gewährleistet wird.
3.2 sync.RWMutex
(Read-Write Mutex Lock)
- Grundlegende Eigenschaften: Das
sync.RWMutex
enthält intern zwei Sperren: eine Schreibsperre und eine Lesesperre. Zusätzlich zu den MethodenLock()
undUnlock()
verfügt der Typ*sync.RWMutex
auch über die MethodenRLock()
undRUnlock()
, die verwendet werden, um mehreren Lesern das gleichzeitige Lesen von Daten zu ermöglichen, aber zu verhindern, dass die Daten gleichzeitig von einem Writer und anderen Datenzugreifern (einschließlich Lesern und Writern) verwendet werden. Die Lesesperre vonrwm
verwaltet einen Zähler. Wenn der Aufrufrwm.RLock()
erfolgreich ist, erhöht sich der Zähler um 1; wenn der Aufrufrwm.RUnlock()
erfolgreich ist, verringert sich der Zähler um 1; ein Zähler von Null zeigt an, dass sich die Lesesperre im nicht gesperrten Zustand befindet, und ein Zähler ungleich Null zeigt an, dass sich die Lesesperre im gesperrten Zustand befindet.rwm.Lock()
,rwm.Unlock()
,rwm.RLock()
undrwm.RUnlock()
sind Abkürzungen für(&rwm).Lock()
,(&rwm).Unlock()
,(&rwm).RLock()
bzw.(&rwm).RUnlock()
. - Sperrregeln
- Die Schreibsperre von
rwm
kann nur dann erfolgreich gesperrt werden, wenn sich sowohl die Schreibsperre als auch die Lesesperre im nicht gesperrten Zustand befinden, d. h. die Schreibsperre kann jeweils nur von maximal einem Datenschreiber erfolgreich gesperrt werden, und die Schreibsperre und die Lesesperre können nicht gleichzeitig gesperrt werden. - Wenn sich die Schreibsperre von
rwm
im gesperrten Zustand befindet, führt jede neue Schreibsperr- oder Lesesperroperation dazu, dass die aktuelle Goroutine in einen blockierten Zustand wechselt, bis die Schreibsperre entsperrt wird. - Wenn sich die Lesesperre von
rwm
im gesperrten Zustand befindet, führt eine neue Schreibsperroperation dazu, dass die aktuelle Goroutine in einen blockierten Zustand wechselt; und eine neue Lesesperroperation ist unter bestimmten Bedingungen erfolgreich (die vor jeder blockierten Schreibsperroperation stattfindet), d. h. die Lesesperre kann gleichzeitig von mehreren Datenlesern gehalten werden. Wenn der von der Lesesperre verwaltete Zähler auf Null gesetzt wird, kehrt die Lesesperre in den nicht gesperrten Zustand zurück. - Um zu verhindern, dass der Datenschreiber verhungert, werden nachfolgende Lesesperroperationen blockiert, wenn sich die Lesesperre im gesperrten Zustand befindet und blockierte Schreibsperroperationen vorhanden sind; um zu verhindern, dass der Datenleser verhungert, sind zuvor blockierte Lesesperroperationen definitiv erfolgreich, wenn sich die Schreibsperre im gesperrten Zustand befindet, nachdem die Schreibsperre entsperrt wurde.
- Die Schreibsperre von
- Nutzungsbeispiel
package main import ( "fmt" "time" "sync" ) func main() { var m sync.RWMutex go func() { m.RLock() fmt.Print("a") time.Sleep(time.Second) m.RUnlock() }() go func() { time.Sleep(time.Second * 1 / 4) m.Lock() fmt.Print("b") time.Sleep(time.Second) m.Unlock() }() go func() { time.Sleep(time.Second * 2 / 4) m.Lock() fmt.Print("c") m.Unlock() }() go func () { time.Sleep(time.Second * 3 / 4) m.RLock() fmt.Print("d") m.RUnlock() }() time.Sleep(time.Second * 3) fmt.Println() }
Das obige Programm gibt höchstwahrscheinlich abdc
aus; dadurch werden die Sperrregeln der Read-Write-Sperre erklärt und überprüft. Es sollte beachtet werden, dass die Verwendung des time.Sleep
-Aufrufs im Programm zur Synchronisierung zwischen Goroutinen nicht im Produktionscode verwendet werden sollte.
In praktischen Anwendungen kann das Mutex
durch ein RWMutex
ersetzt werden, um die Ausführungseffizienz zu verbessern, wenn Leseoperationen häufig und Schreiboperationen selten sind. Ersetzen Sie beispielsweise das Mutex
im obigen Counter
-Beispiel durch ein RWMutex
:
... type Counter struct { //m sync.Mutex m sync.RWMutex n uint64 } func (c *Counter) Value() uint64 { //c.m.Lock() //defer c.m.Unlock() c.m.RLock() defer c.m.RUnlock() return c.n } ...
Darüber hinaus können sync.Mutex
- und sync.RWMutex
-Werte auch zum Implementieren von Benachrichtigungen verwendet werden, obwohl dies in Go nicht die eleganteste Implementierung ist. Zum Beispiel:
package main import ( "fmt" "sync" "time" ) func main() { var m sync.Mutex m.Lock() go func() { time.Sleep(time.Second) fmt.Println("Hi") m.Unlock() // Eine Benachrichtigung senden }() m.Lock() // Auf die Benachrichtigung warten fmt.Println("Bye") }
In diesem Beispiel wird eine einfache Benachrichtigung zwischen Goroutinen über das Mutex
implementiert, um sicherzustellen, dass "Hi" vor "Bye" ausgegeben wird. Für die Speicherreihenfolgegarantien im Zusammenhang mit sync.Mutex
- und sync.RWMutex
-Werten können Sie sich auf die entsprechenden Dokumente zu Speicherreihenfolgegarantien in Go beziehen.
Die Typen im sync
Standardbibliothekspaket spielen eine entscheidende Rolle bei der Concurrent-Programmierung der Go-Sprache. Entwickler müssen diese Synchronisierungstypen je nach spezifischen Geschäftsszenarien und Anforderungen angemessen auswählen und korrekt verwenden, um effiziente, zuverlässige und threadsichere Concurrent-Programme zu schreiben. Gleichzeitig ist es beim Schreiben von Concurrent-Code auch notwendig, ein tiefgreifendes Verständnis verschiedener Konzepte und potenzieller Probleme der Concurrent-Programmierung zu haben, wie z. B. Data Races, Deadlocks usw., und die Korrektheit und Stabilität des Programms in einer Concurrent-Umgebung durch ausreichende Tests und Verifizierungen sicherzustellen.
Leapcell: Das Beste von Serverless Web Hosting
Zum Schluss möchte ich eine Plattform empfehlen, die sich am besten für die Bereitstellung von Go-Diensten eignet: Leapcell
🚀 Entwickeln Sie mit Ihrer Lieblingssprache
Entwickeln Sie mühelos in JavaScript, Python, Go oder Rust.
🌍 Stellen Sie unbegrenzt Projekte kostenlos bereit
Zahlen Sie nur für das, was Sie nutzen – keine Anfragen, keine Gebühren.
⚡ Pay-as-You-Go, keine versteckten Kosten
Keine Leerlaufgebühren, nur nahtlose Skalierbarkeit.
📖 Entdecken Sie unsere Dokumentation
🔹 Folgen Sie uns auf Twitter: @LeapcellHQ