„Wenn ein Arbeiter seine Arbeit gut machen will, muss er zuerst seine Werkzeuge schärfen.“ – Konfuzius, „Die Gespräche des Konfuzius. Lu Linggong“
Titelseite > Programmierung > Go sync.Pool und die Mechanismen dahinter

Go sync.Pool und die Mechanismen dahinter

Veröffentlicht am 06.11.2024
Durchsuche:492

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


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

  • Go sync.Mutex: Normal- und Hungermodus
  • Go sync.WaitGroup und das Ausrichtungsproblem
  • Go sync.Pool und die Mechanismen dahinter (Wir sind hier)
  • Go sync.Cond, der am meisten übersehene Synchronisierungsmechanismus

Im VictoriaMetrics-Quellcode verwenden wir häufig sync.Pool, und es passt ehrlich gesagt hervorragend zu unserem Umgang mit temporären Objekten, insbesondere Bytepuffern oder Slices.

Es wird häufig in der Standardbibliothek verwendet. Zum Beispiel im Paket „encoding/json“:

package json

var encodeStatePool sync.Pool

// An encodeState encodes JSON into a bytes.Buffer.
type encodeState struct {
    bytes.Buffer // accumulated output

    ptrLevel uint
    ptrSeen  map[any]struct{}
}

In diesem Fall wird sync.Pool verwendet, um *encodeState-Objekte wiederzuverwenden, die den Prozess der Codierung von JSON in einen bytes.Buffer übernehmen.

Anstatt diese Objekte nach jeder Verwendung einfach wegzuwerfen, was dem Garbage Collector nur mehr Arbeit machen würde, verstecken wir sie in einem Pool (sync.Pool). Wenn wir das nächste Mal etwas Ähnliches brauchen, nehmen wir es einfach aus dem Pool, anstatt etwas völlig Neues zu machen.

Im Paket net/http finden Sie auch mehrere sync.Pool-Instanzen, die zur Optimierung von E/A-Vorgängen verwendet werden:

package http

var (
    bufioReaderPool   sync.Pool
    bufioWriter2kPool sync.Pool
    bufioWriter4kPool sync.Pool
)

Wenn der Server Anforderungstexte liest oder Antworten schreibt, kann er schnell einen vorab zugewiesenen Leser oder Schreiber aus diesen Pools abrufen und zusätzliche Zuweisungen überspringen. Darüber hinaus sind die beiden Autorenpools *bufioWriter2kPool und *bufioWriter4kPool so eingerichtet, dass sie unterschiedliche Schreibanforderungen erfüllen.

func bufioWriterPool(size int) *sync.Pool {
    switch size {
    case 2 



Okay, das ist genug vom Intro.

Heute befassen wir uns mit dem, worum es bei sync.Pool geht, mit der Definition, wie es verwendet wird, was unter der Haube vor sich geht und mit allem, was Sie sonst noch wissen möchten.

Übrigens, wenn Sie etwas Praktischeres wollen, gibt es einen guten Artikel unserer Go-Experten, der zeigt, wie wir sync.Pool in VictoriaMetrics verwenden: Techniken zur Leistungsoptimierung in Zeitreihendatenbanken: sync.Pool für CPU-gebundene Vorgänge

Was ist sync.Pool?

Um es einfach auszudrücken: sync.Pool in Go ist ein Ort, an dem Sie temporäre Objekte für die spätere Wiederverwendung aufbewahren können.

Aber hier ist die Sache: Sie haben keine Kontrolle darüber, wie viele Objekte im Pool bleiben, und alles, was Sie dort hineinlegen, kann jederzeit und ohne Vorwarnung entfernt werden, und Sie werden wissen, warum, wenn Sie den letzten Abschnitt lesen.

Der gute Punkt ist, dass der Pool threadsicher aufgebaut ist, sodass mehrere Goroutinen gleichzeitig darauf zugreifen können. Keine große Überraschung, wenn man bedenkt, dass es Teil des Synchronisierungspakets ist.

"Aber warum machen wir uns die Mühe, Objekte wiederzuverwenden?"

Wenn viele Goroutinen gleichzeitig ausgeführt werden, benötigen diese häufig ähnliche Objekte. Stellen Sie sich vor, go f() mehrmals gleichzeitig auszuführen.

Wenn jede Goroutine ihre eigenen Objekte erstellt, kann die Speichernutzung schnell ansteigen und dies stellt eine Belastung für den Garbage Collector dar, da er alle diese Objekte bereinigen muss, sobald sie nicht mehr benötigt werden.

Diese Situation erzeugt einen Zyklus, in dem eine hohe Parallelität zu einer hohen Speichernutzung führt, was dann den Garbage Collector verlangsamt. sync.Pool soll dabei helfen, diesen Teufelskreis zu durchbrechen.

type Object struct {
    Data []byte
}

var pool sync.Pool = sync.Pool{
    New: func() any {
        return &Object{
            Data: make([]byte, 0, 1024),
        }
    },
}

Um einen Pool zu erstellen, können Sie eine New()-Funktion bereitstellen, die ein neues Objekt zurückgibt, wenn der Pool leer ist. Diese Funktion ist optional. Wenn Sie sie nicht angeben, gibt der Pool nur Null zurück, wenn er leer ist.

Im obigen Snippet besteht das Ziel darin, die Object-Strukturinstanz wiederzuverwenden, insbesondere den darin enthaltenen Slice.

Die Wiederverwendung des Slice trägt dazu bei, unnötiges Wachstum zu reduzieren.

Wenn das Slice beispielsweise während der Verwendung auf 8192 Bytes anwächst, können Sie seine Länge auf Null zurücksetzen, bevor Sie es wieder in den Pool einfügen. Das zugrunde liegende Array hat immer noch eine Kapazität von 8192. Wenn Sie es also das nächste Mal benötigen, stehen diese 8192 Bytes zur Wiederverwendung bereit.

func (o *Object) Reset() {
    o.Data = o.Data[:0]
}

func main() {
    testObject := pool.Get().(*Object)

    // do something with testObject

    testObject.Reset()
    pool.Put(testObject)
}

Der Ablauf ist ziemlich klar: Sie holen sich ein Objekt aus dem Pool, verwenden es, setzen es zurück und legen es dann wieder in den Pool ein. Das Zurücksetzen des Objekts kann entweder vor dem Zurücklegen oder direkt nach der Entnahme aus dem Pool erfolgen, ist jedoch nicht zwingend erforderlich, sondern eine gängige Praxis.

Wenn Sie kein Fan der Verwendung von Typzusicherungen pool.Get().(*Object) sind, gibt es mehrere Möglichkeiten, dies zu vermeiden:

  • Verwenden Sie eine dedizierte Funktion, um das Objekt aus dem Pool abzurufen:
func getObjectFromPool() *Object {
    obj := pool.Get().(*Object)
    return obj
}
  • Erstellen Sie Ihre eigene generische Version von sync.Pool:
type Pool[T any] struct {
    sync.Pool
}

func (p *Pool[T]) Get() T {
    return p.Pool.Get().(T)
}

func (p *Pool[T]) Put(x T) {
    p.Pool.Put(x)
}

func NewPool[T any](newF func() T) *Pool[T] {
    return &Pool[T]{
        Pool: sync.Pool{
            New: func() interface{} {
                return newF()
            },
        },
    }
}

Der generische Wrapper bietet Ihnen eine typsicherere Möglichkeit, mit dem Pool zu arbeiten und Typzusicherungen zu vermeiden.

Beachten Sie bitte, dass aufgrund der zusätzlichen Indirektionsebene ein wenig Mehraufwand entsteht. In den meisten Fällen ist dieser Overhead minimal, aber wenn Sie sich in einer sehr CPU-empfindlichen Umgebung befinden, ist es eine gute Idee, Benchmarks durchzuführen, um zu sehen, ob es sich lohnt.

Aber Moment, es steckt noch mehr dahinter.

sync.Pool und Allocation Trap

Wenn Sie aus vielen früheren Beispielen, einschließlich denen in der Standardbibliothek, bemerkt haben, ist das, was wir im Pool speichern, normalerweise nicht das Objekt selbst, sondern ein Zeiger auf das Objekt.

Lassen Sie mich anhand eines Beispiels erklären, warum:

var pool = sync.Pool{
    New: func() any {
        return []byte{}
    },
}

func main() {
    bytes := pool.Get().([]byte)

    // do something with bytes
    _ = bytes

    pool.Put(bytes)
}

Wir verwenden einen Pool von []Byte. Wenn Sie einen Wert an eine Schnittstelle übergeben, kann dies im Allgemeinen (wenn auch nicht immer) dazu führen, dass der Wert auf dem Heap platziert wird. Dies geschieht auch hier, nicht nur bei Slices, sondern bei allem, was Sie an pool.Put() übergeben und das kein Zeiger ist.

Wenn Sie die Escape-Analyse verwenden:

// escape analysis
$ go build -gcflags=-m

bytes escapes to heap

Nun, ich sage nicht, dass unsere variablen Bytes auf den Heap verschoben werden, ich würde sagen: „Der Wert der Bytes entweicht über die Schnittstelle auf den Heap“.

Um wirklich zu verstehen, warum dies geschieht, müssen wir uns mit der Funktionsweise der Escape-Analyse befassen (was wir möglicherweise in einem anderen Artikel tun werden). Wenn wir jedoch einen Zeiger auf pool.Put() übergeben, gibt es keine zusätzliche Zuweisung:

var pool = sync.Pool{
    New: func() any {
        return new([]byte)
    },
}

func main() {
    bytes := pool.Get().(*[]byte)

    // do something with bytes
    _ = bytes

    pool.Put(bytes)
}

Führen Sie die Escape-Analyse erneut aus. Sie werden sehen, dass es keine Escapes mehr auf den Heap gibt. Wenn Sie mehr wissen möchten, gibt es ein Beispiel im Go-Quellcode.

sync.Pool-Interna

Bevor wir uns damit befassen, wie sync.Pool tatsächlich funktioniert, lohnt es sich, sich mit den Grundlagen des PMG-Planungsmodells von Go vertraut zu machen. Dies ist wirklich das Rückgrat, warum sync.Pool so effizient ist.

Es gibt einen guten Artikel, der das PMG-Modell mit einigen Bildern aufschlüsselt: PMG-Modelle in Go

Wenn Sie sich heute faul fühlen und nach einer vereinfachten Zusammenfassung suchen, bin ich für Sie da:

PMG steht für P (logische pProzessoren), M (mMaschinen-Threads) und G (gOroutinen). Der entscheidende Punkt ist, dass auf jedem logischen Prozessor (P) jeweils nur ein Maschinenthread (M) laufen kann. Und damit eine Goroutine (G) ausgeführt werden kann, muss sie an einen Thread (M) angehängt werden.

Go sync.Pool and the Mechanics Behind It

PMG-Modell

Das lässt sich auf zwei wichtige Punkte reduzieren:

  1. Wenn Sie über n logische Prozessoren (P) verfügen, können Sie bis zu n Goroutinen parallel ausführen, solange mindestens n Maschinenthreads (M) verfügbar sind.
  2. Es kann jeweils nur eine Goroutine (G) auf einem einzelnen Prozessor (P) ausgeführt werden. Wenn also ein P1 mit einem G beschäftigt ist, kann kein anderer G auf diesem P1 laufen, bis der aktuelle G entweder blockiert wird, fertig ist oder etwas anderes passiert, das ihn freigibt.

Aber die Sache ist die, ein sync.Pool in Go ist nicht nur ein großer Pool, sondern besteht tatsächlich aus mehreren „lokalen“ Pools, von denen jeder an einen bestimmten Prozessorkontext oder P, also die Laufzeit von Go, gebunden ist jederzeit verwalten.

Go sync.Pool and the Mechanics Behind It

Lokale Pools

Wenn eine Goroutine, die auf einem Prozessor (P) läuft, ein Objekt aus dem Pool benötigt, überprüft sie zunächst ihren eigenen P-lokalen Pool, bevor sie woanders sucht.


Der vollständige Beitrag ist hier verfügbar: https://victoriametrics.com/blog/go-sync-pool/

Freigabeerklärung Dieser Artikel ist abgedruckt unter: https://dev.to/func25/go-syncpool-and-the-mechanics-behind-it-52c1?1 Bei Verstößen wenden Sie sich bitte an [email protected], um ihn zu löschen
Neuestes Tutorial Mehr>

Haftungsausschluss: Alle bereitgestellten Ressourcen stammen teilweise aus dem Internet. Wenn eine Verletzung Ihres Urheberrechts oder anderer Rechte und Interessen vorliegt, erläutern Sie bitte die detaillierten Gründe und legen Sie einen Nachweis des Urheberrechts oder Ihrer Rechte und Interessen vor und senden Sie ihn dann an die E-Mail-Adresse: [email protected] Wir werden die Angelegenheit so schnell wie möglich für Sie erledigen.

Copyright© 2022 湘ICP备2022001581号-3