Heim  >  Artikel  >  Backend-Entwicklung  >  Go sync.Cond, der am meisten übersehene Synchronisierungsmechanismus

Go sync.Cond, der am meisten übersehene Synchronisierungsmechanismus

DDD
DDDOriginal
2024-10-30 06:43:28233Durchsuche

Dies ist ein Auszug aus dem Beitrag; Der vollständige Beitrag ist hier verfügbar: https://victoriametrics.com/blog/go-sync-cond/

Dieser Beitrag ist Teil einer Serie über den Umgang mit Parallelität in Go:

  • Go sync.Mutex: Normal- und Hungermodus
  • Gehen Sie zu sync.WaitGroup und dem Ausrichtungsproblem
  • Go sync.Pool und die Mechanismen dahinter
  • Gehen Sie zu sync.Cond, dem am meisten übersehenen Synchronisierungsmechanismus (wir sind hier)
  • Go sync.Map: Das richtige Tool für den richtigen Job
  • Go Singleflight schmilzt in Ihrem Code, nicht in Ihrer Datenbank

In Go ist sync.Cond ein Synchronisierungsprimitiv, obwohl es nicht so häufig verwendet wird wie seine Geschwister wie sync.Mutex oder sync.WaitGroup. Sie werden es in den meisten Projekten oder sogar in den Standardbibliotheken selten sehen, wo normalerweise andere Synchronisierungsmechanismen an seine Stelle treten.

Dennoch möchten Sie als Go-Ingenieur nicht wirklich Code lesen, der sync.Cond verwendet, und keine Ahnung haben, was vor sich geht, weil es schließlich Teil der Standardbibliothek ist.

Diese Diskussion wird Ihnen helfen, diese Lücke zu schließen, und noch besser, sie wird Ihnen ein klareres Gefühl dafür vermitteln, wie es in der Praxis tatsächlich funktioniert.

Was ist sync.Cond?

Lassen Sie uns also erklären, worum es bei sync.Cond geht.

Wenn eine Goroutine darauf warten muss, dass etwas Bestimmtes passiert, wie etwa die Änderung gemeinsam genutzter Daten, kann sie „blockieren“, was bedeutet, dass sie ihre Arbeit einfach pausiert, bis sie grünes Licht zum Fortfahren erhält. Der einfachste Weg, dies zu tun, ist eine Schleife, vielleicht sogar das Hinzufügen eines time.Sleep, um zu verhindern, dass die CPU durch „Busy-Waiting“ verrückt wird.

So könnte das aussehen:

// wait until condition is true
for !condition {  
}

// or 
for !condition {
    time.Sleep(100 * time.Millisecond)
}

Das ist nicht wirklich effizient, da diese Schleife immer noch im Hintergrund läuft und CPU-Zyklen durchläuft, selbst wenn sich nichts geändert hat.

Hier kommt sync.Cond ins Spiel, eine bessere Möglichkeit, Goroutinen ihre Arbeit koordinieren zu lassen. Technisch gesehen handelt es sich um eine „Bedingungsvariable“, wenn Sie einen eher akademischen Hintergrund haben.

  • Wenn eine Goroutine darauf wartet, dass etwas passiert (darauf wartet, dass eine bestimmte Bedingung wahr wird), kann sie Wait() aufrufen.
  • Eine andere Goroutine kann, sobald sie weiß, dass die Bedingung erfüllt sein könnte, Signal() oder Broadcast() aufrufen, um die wartende(n) Goroutine(n) aufzuwecken und ihnen mitzuteilen, dass es Zeit ist, weiterzumachen.

Hier ist die grundlegende Schnittstelle, die sync.Cond bietet:

// Suspends the calling goroutine until the condition is met
func (c *Cond) Wait() {}

// Wakes up one waiting goroutine, if there is one
func (c *Cond) Signal() {}

// Wakes up all waiting goroutines
func (c *Cond) Broadcast() {}

Go sync.Cond, the Most Overlooked Sync Mechanism

Übersicht über sync.Cond

Okay, schauen wir uns ein kurzes Pseudobeispiel an. Dieses Mal geht es um ein Pokémon-Thema. Stellen Sie sich vor, wir warten auf ein bestimmtes Pokémon und möchten andere Goroutinen benachrichtigen, wenn es auftaucht.

// wait until condition is true
for !condition {  
}

// or 
for !condition {
    time.Sleep(100 * time.Millisecond)
}

In diesem Beispiel wartet eine Goroutine darauf, dass Pikachu auftaucht, während eine andere (der Produzent) zufällig ein Pokémon aus der Liste auswählt und dem Verbraucher signalisiert, wenn ein neues erscheint.

Wenn der Produzent das Signal sendet, wacht der Verbraucher auf und prüft, ob das richtige Pokémon aufgetaucht ist. Wenn ja, fangen wir das Pokémon, wenn nicht, schläft der Verbraucher wieder ein und wartet auf das nächste.

Das Problem besteht darin, dass es eine Lücke zwischen dem Signalgeber des Produzenten und dem tatsächlichen Aufwachen des Verbrauchers gibt. In der Zwischenzeit könnte sich das Pokémon ändern, da die Consumer-Goroutine möglicherweise später als 1 ms aufwacht (selten) oder eine andere Goroutine das gemeinsam genutzte Pokémon ändert. sync.Cond sagt also im Grunde: 'Hey, etwas hat sich geändert!' Wachen Sie auf und schauen Sie sich das an, aber wenn Sie zu spät sind, könnte es sich wieder ändern.'

Wenn der Verbraucher spät aufwacht, läuft das Pokémon möglicherweise weg und die Goroutine schläft wieder ein.

„Huh, ich könnte einen Kanal verwenden, um den Pokémon-Namen oder das Signal an die andere Goroutine zu senden“

Absolut. Tatsächlich werden Kanäle im Allgemeinen gegenüber sync.Cond in Go bevorzugt, da sie einfacher, idiomatischer und den meisten Entwicklern vertraut sind.

Im obigen Fall könnten Sie den Pokémon-Namen einfach über einen Kanal senden oder einfach eine leere Struktur{} zum Signalisieren verwenden, ohne Daten zu senden. Bei unserem Problem geht es jedoch nicht nur darum, Nachrichten über Kanäle weiterzuleiten, sondern auch um den Umgang mit einem gemeinsamen Zustand.

Unser Beispiel ist ziemlich einfach, aber wenn mehrere Goroutinen auf die gemeinsam genutzte Pokemon-Variable zugreifen, schauen wir uns an, was passiert, wenn wir einen Kanal verwenden:

  • Wenn wir einen Kanal zum Senden des Pokémon-Namens verwenden, benötigen wir immer noch einen Mutex, um die gemeinsam genutzte Pokémon-Variable zu schützen.
  • Wenn wir einen Kanal nur zum Signalisieren verwenden, ist immer noch ein Mutex erforderlich, um den Zugriff auf den freigegebenen Status zu verwalten.
  • Wenn wir im Produzenten nach Pikachu suchen und es dann über den Kanal senden, benötigen wir auch einen Mutex. Darüber hinaus würden wir gegen den Grundsatz der Interessenstrennung verstoßen, bei dem der Hersteller die Logik übernimmt, die wirklich dem Verbraucher gehört.

Wenn jedoch mehrere Goroutinen gemeinsam genutzte Daten ändern, ist zum Schutz immer noch ein Mutex erforderlich. In diesen Fällen sehen Sie häufig eine Kombination aus Kanälen und Mutexes, um eine ordnungsgemäße Synchronisierung und Datensicherheit zu gewährleisten.

„Okay, aber was ist mit den Sendesignalen?“

Gute Frage! Sie können tatsächlich ein Broadcast-Signal an alle wartenden Goroutinen, die einen Kanal verwenden, nachahmen, indem Sie ihn einfach schließen (close(ch)). Wenn Sie einen Kanal schließen, werden alle Goroutinen, die von diesem Kanal empfangen, benachrichtigt. Beachten Sie jedoch, dass ein geschlossener Kanal nicht wiederverwendet werden kann. Sobald er geschlossen ist, bleibt er geschlossen.

Übrigens wurde tatsächlich darüber gesprochen, sync.Cond in Go 2 zu entfernen: Vorschlag: sync: den Cond-Typ entfernen.

"Also, wofür ist sync.Cond dann gut?"

Nun, es gibt bestimmte Szenarien, in denen sync.Cond besser geeignet sein kann als Kanäle.

  1. Mit einem Kanal können Sie entweder ein Signal an eine Goroutine senden, indem Sie einen Wert senden, oder alle Goroutinen benachrichtigen, indem Sie den Kanal schließen, aber Sie können nicht beides tun. sync.Cond bietet Ihnen eine detailliertere Kontrolle. Sie können Signal() aufrufen, um eine einzelne Goroutine aufzuwecken, oder Broadcast(), um alle aufzuwecken.
  2. Und Sie können Broadcast() so oft aufrufen, wie Sie möchten, was Kanäle nicht mehr tun können, wenn sie geschlossen sind (das Schließen eines geschlossenen Kanals löst eine Panik aus).
  3. Kanäle bieten keine integrierte Möglichkeit, gemeinsam genutzte Daten zu schützen – Sie müssten dies separat mit einem Mutex verwalten. sync.Cond hingegen bietet Ihnen einen integrierteren Ansatz, indem es Sperren und Signalisieren in einem Paket kombiniert (und eine bessere Leistung bietet).

"Warum ist die Sperre in sync.Cond eingebettet?"

Theoretisch muss eine Bedingungsvariable wie sync.Cond nicht an eine Sperre gebunden sein, damit ihre Signalisierung funktioniert.

Sie könnten die Benutzer ihre eigenen Sperren außerhalb der Bedingungsvariablen verwalten lassen, was vielleicht so klingt, als ob es mehr Flexibilität bietet. Es handelt sich nicht wirklich um eine technische Einschränkung, sondern eher um menschliches Versagen.

Eine manuelle Verwaltung kann leicht zu Fehlern führen, da das Muster nicht wirklich intuitiv ist. Sie müssen den Mutex entsperren, bevor Sie Wait() aufrufen, und ihn dann wieder sperren, wenn die Goroutine aufwacht. Dieser Vorgang kann sich umständlich anfühlen und ist ziemlich fehleranfällig, etwa wenn man vergisst, zum richtigen Zeitpunkt zu sperren oder zu entsperren.

Aber warum scheint das Muster etwas abweichend zu sein?

Normalerweise müssen Goroutinen, die cond.Wait() aufrufen, einen gemeinsamen Status in einer Schleife überprüfen, wie folgt:

// wait until condition is true
for !condition {  
}

// or 
for !condition {
    time.Sleep(100 * time.Millisecond)
}

Die in sync.Cond eingebettete Sperre hilft uns bei der Abwicklung des Sperr-/Entsperrvorgangs und macht den Code sauberer und weniger fehleranfällig. Wir werden das Muster bald im Detail besprechen.

Wie benutzt man es?

Wenn Sie sich das vorherige Beispiel genau ansehen, werden Sie ein konsistentes Muster im Consumer bemerken: Wir sperren den Mutex immer, bevor wir auf die Bedingung warten (.Wait()), und wir entsperren ihn, nachdem die Bedingung erfüllt ist.

Außerdem packen wir die Wartebedingung in eine Schleife, hier ist eine Auffrischung:

// Suspends the calling goroutine until the condition is met
func (c *Cond) Wait() {}

// Wakes up one waiting goroutine, if there is one
func (c *Cond) Signal() {}

// Wakes up all waiting goroutines
func (c *Cond) Broadcast() {}

Cond.Wait()

Wenn wir Wait() auf einer sync.Cond aufrufen, weisen wir die aktuelle Goroutine an, zu warten, bis eine Bedingung erfüllt ist.

Das passiert hinter den Kulissen:

  1. Die Goroutine wird zu einer Liste anderer Goroutinen hinzugefügt, die ebenfalls auf diese Bedingung warten. Alle diese Goroutinen sind blockiert, was bedeutet, dass sie nicht weitermachen können, bis sie durch einen Signal()- oder Broadcast()-Aufruf „aufgeweckt“ werden.
  2. Der entscheidende Teil hier ist, dass der Mutex gesperrt werden muss, bevor Wait() aufgerufen wird, da Wait() etwas Wichtiges tut: Es gibt die Sperre automatisch frei (ruft Unlock() auf), bevor es die Goroutine in den Ruhezustand versetzt. Dadurch können andere Goroutinen die Sperre ergreifen und ihre Arbeit erledigen, während die ursprüngliche Goroutine wartet.
  3. Wenn die wartende Goroutine geweckt wird (durch Signal() oder Broadcast()), nimmt sie die Arbeit nicht sofort wieder auf. Zuerst muss die Sperre erneut erworben werden (Lock()).

Go sync.Cond, the Most Overlooked Sync Mechanism

Die sync.Cond.Wait()-Methode

Hier sehen Sie, wie Wait() unter der Haube funktioniert:

// wait until condition is true
for !condition {  
}

// or 
for !condition {
    time.Sleep(100 * time.Millisecond)
}

Auch wenn es einfach ist, können wir vier Hauptpunkte mitnehmen:

  1. Es gibt einen Prüfer, der das Kopieren der Cond-Instanz verhindert. Wenn Sie dies tun, würde dies zu Panik führen.
  2. Durch den Aufruf von cond.Wait() wird der Mutex sofort entsperrt, daher muss der Mutex vor dem Aufruf von cond.Wait() gesperrt werden, andernfalls kommt es zu einer Panik.
  3. Nach dem Aufwecken sperrt cond.Wait() den Mutex erneut, was bedeutet, dass Sie ihn erneut entsperren müssen, nachdem Sie mit den freigegebenen Daten fertig sind.
  4. Der Großteil der Funktionalität von sync.Cond wird in der Go-Laufzeit mit einer internen Datenstruktur namens notifyList implementiert, die ein Ticket-basiertes System für Benachrichtigungen verwendet.

Aufgrund dieses Sperr-/Entsperrverhaltens gibt es ein typisches Muster, dem Sie bei der Verwendung von sync.Cond.Wait() folgen, um häufige Fehler zu vermeiden:

// Suspends the calling goroutine until the condition is met
func (c *Cond) Wait() {}

// Wakes up one waiting goroutine, if there is one
func (c *Cond) Signal() {}

// Wakes up all waiting goroutines
func (c *Cond) Broadcast() {}

Go sync.Cond, the Most Overlooked Sync Mechanism

Das typische Muster für die Verwendung von sync.Cond.Wait()

„Warum nicht einfach c.Wait() direkt ohne Schleife verwenden?“


Dies ist ein Auszug aus dem Beitrag; Der vollständige Beitrag ist hier verfügbar: https://victoriametrics.com/blog/go-sync-cond/

Das obige ist der detaillierte Inhalt vonGo sync.Cond, der am meisten übersehene Synchronisierungsmechanismus. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn