In diesem Artikel wird hauptsächlich die interne Scheduler-Implementierungsarchitektur des Go-Programms (G-P-M-Modell) vorgestellt, um eine extrem hohe Parallelitätsleistung zu erreichen, und wie der Go-Scheduler mit Thread-Blockierungsszenarien umgeht, um die Nutzung von Rechenressourcen zu maximieren.
So machen wir unser System schneller
Mit der rasanten Entwicklung der Informationstechnologie wird die Rechenleistung eines einzelnen Servers immer stärker, was das Programmiermodell dazu zwingt Der Wechsel vom vorherigen seriellen Zeilenmodus wird auf das gleichzeitige Modell aktualisiert.
Parallelitätsmodelle umfassen E/A-Multiplexing, Multiprozess und Multithreading. Jedes dieser Modelle hat seine eigenen Vor- und Nachteile. Die meisten modernen, komplexen Architekturen mit hoher Parallelität verwenden mehrere Modelle zusammen Nutzen Sie in verschiedenen Szenarien Stärken und vermeiden Sie Schwächen, um die Leistung des Servers zu maximieren.
Multithreading hat sich aufgrund seiner Leichtigkeit und Benutzerfreundlichkeit zum am häufigsten verwendeten Parallelitätsmodell in der gleichzeitigen Programmierung entwickelt, einschließlich später abgeleiteter Coroutinen und anderer Unterprodukte, die ebenfalls darauf basieren.
Parallelität ≠ Parallel
Parallelität und Parallelität sind unterschiedlich.
Auf einem einzelnen CPU-Kern realisieren Threads den Aufgabenwechsel über Zeitscheiben oder geben Kontrollrechte auf, um den Zweck zu erreichen, mehrere Aufgaben „gleichzeitig“ auszuführen. Tatsächlich wird jedoch jeweils nur eine Aufgabe ausgeführt, und andere Aufgaben werden über einen Algorithmus in die Warteschlange gestellt.
Multi-Core-CPU ermöglicht die gleichzeitige Ausführung mehrerer Threads im selben Prozess im wahrsten Sinne des Wortes.
Prozess, Thread, Coroutine
Prozess: Der Prozess ist die Grundeinheit der Ressourcenzuweisung im System und verfügt über einen unabhängigen Speicherplatz.
Thread: Thread ist die Grundeinheit der CPU-Planung und -Verteilung. Threads werden an Prozesse angehängt, und jeder Thread teilt sich die Ressourcen des übergeordneten Prozesses.
Coroutine: Coroutine ist ein leichter Thread im Benutzermodus. Die Planung von Coroutinen wird vollständig vom Benutzer gesteuert. Das Umschalten zwischen Coroutinen erfordert nur das Speichern des Kontexts der Aufgabe, ohne Kernel-Overhead.
Thread-Kontextwechsel
Aufgrund von Interrupt-Verarbeitung, Multitasking, Benutzermoduswechsel und anderen Gründen wechselt die CPU von einem Thread zum anderen, und der Umschaltvorgang muss durchgeführt werden Der Status des aktuellen Prozesses wird gespeichert und der Status eines anderen Prozesses wiederhergestellt.
Kontextwechsel sind teuer, da das Austauschen von Threads im Kern viel Zeit in Anspruch nimmt. Die Kontextwechsellatenz hängt von verschiedenen Faktoren ab und kann zwischen 50 und 100 Nanosekunden liegen. Wenn man bedenkt, dass die Hardware durchschnittlich 12 Anweisungen pro Nanosekunde und Kern ausführt, kann ein Kontextwechsel 600 bis 1200 Anweisungen an Latenz kosten. Tatsächlich nimmt der Kontextwechsel viel Zeit des Programms in Anspruch, um Anweisungen auszuführen.
Wenn ein kernübergreifender Kontextwechsel vorliegt, kann dies dazu führen, dass der CPU-Cache ungültig wird (die Kosten für den Zugriff der CPU auf Daten aus dem Cache betragen etwa 3 bis 40 Taktzyklen und die Kosten für den Zugriff). Da die Übertragung von Daten aus dem Hauptspeicher etwa 100 bis 300 Taktzyklen dauert, sind die Umstellungskosten in diesem Szenario höher.
Golang ist für Parallelität konzipiert
Seit seiner offiziellen Veröffentlichung im Jahr 2009 hat Golang aufgrund seiner extrem hohen Laufgeschwindigkeit und effizienten Entwicklungseffizienz schnell Marktanteile erobert. Golang unterstützt Parallelität auf Sprachebene und verwendet die leichtgewichtige Coroutine Goroutine, um die gleichzeitige Ausführung von Programmen zu realisieren.
Goroutine ist sehr leichtgewichtig, was sich hauptsächlich in den folgenden zwei Aspekten widerspiegelt:
Die Kosten für die Kontextumschaltung sind gering: Die Kontextumschaltung bei Goroutine erfordert nur die Wertänderung von drei Registern (PC / SP / DX); Im Gegensatz dazu erfordert die Kontextumschaltung von Threads eine Modusumschaltung (Umschaltung vom Benutzermodus in den Kernelmodus) und die Aktualisierung von 16 Registern, PC, SP... und anderen Registern.
Geringe Speichernutzung: Der Thread-Stack-Speicherplatz beträgt normalerweise 2 MB, der minimale Goroutine-Stack-Speicherplatz beträgt 2 KB.
Das Golang-Programm kann problemlos den 10-W-Goroutine-Betrieb unterstützen, und wenn die Anzahl der Threads 1 KB erreicht, hat die Speichernutzung 2 GB erreicht.
Go-Scheduler-Implementierungsmechanismus:
Das Go-Programm verwendet den Scheduler, um die Ausführung von Goroutine im Kernel-Thread zu planen, Goroutine ist jedoch nicht direkt an den Betriebssystem-Thread M gebunden. Maschine Anstatt zu laufen, dient der P-Prozessor (logischer Prozessor) im Goroutine-Scheduler als „Vermittler“, um Kernel-Thread-Ressourcen zu erhalten.
Das Go-Scheduler-Modell wird normalerweise als G-P-M-Modell bezeichnet. Es umfasst 4 wichtige Strukturen, nämlich G, P, M und Sched:
G: Goroutine entspricht einer G-Struktur .Body, G speichert Goroutines laufende Stapel-, Status- und Aufgabenfunktionen, die wiederverwendet werden können.
G ist kein Ausführungsorgan. Jedes G muss an P gebunden sein, um für die Ausführung geplant zu werden.
P: Prozessor, der einen logischen Prozessor darstellt. P entspricht einem CPU-Kern. G kann nur geplant werden, wenn er an P gebunden ist. Für M stellt P die relevante Ausführungsumgebung (Kontext) bereit, z. B. den Speicherzuordnungsstatus (mcache), die Aufgabenwarteschlange (G) usw.
Die Anzahl von P bestimmt die maximale Anzahl von G, die im System parallelisiert werden können (Voraussetzung: die Anzahl der physischen CPU-Kerne >= die Anzahl von P).
Die Anzahl von P wird durch die vom Benutzer eingestellten GoMAXPROCS bestimmt, aber egal wie groß die GoMAXPROCS-Einstellung ist, die maximale Anzahl von P beträgt 256.
M: Maschine, OS-Kernel-Thread-Abstraktion, stellt die Ressource dar, die tatsächlich Berechnungen durchführt. Nach dem Binden eines gültigen P gelangt es in die Zeitplanschleife und der Mechanismus der Zeitplanschleife stammt grob aus der globalen Warteschlange und der lokalen Warteschlange Warteschlange erhalten in. Die Anzahl von
M ist variabel und wird von Go Runtime angepasst. Um zu verhindern, dass das System zu viele Betriebssystem-Threads aufgrund der Erstellung zu vieler einplant, beträgt die aktuelle Standardhöchstgrenze 10.000.
M behält nicht den Zustand von G bei, der die Grundlage für die Planung von G in M darstellt.
Sched: Go-Scheduler, der Warteschlangen verwaltet, in denen M und G sowie einige Statusinformationen des Planers gespeichert sind.
Der Scheduler-Schleifenmechanismus besteht grob darin, G aus verschiedenen Warteschlangen und der lokalen Warteschlange von P abzurufen, zum Ausführungsstapel von G zu wechseln und die Funktion von G auszuführen, Goexit zum Bereinigen aufzurufen und zu M zurückzukehren, also wiederholt.
Um die Beziehung zwischen M, P und G zu verstehen, können Sie die Beziehung anhand des klassischen Modells eines Gopher-Wagens, der Steine bewegt, veranschaulichen:
The Gopher's Die Aufgabe besteht darin, dass auf der Baustelle eine Reihe von Ziegeln liegen, und der Gopher transportiert die Ziegel mit einem Wagen zum Zunder zum Brennen. M kann als Gopher im Bild betrachtet werden, P ist das Auto und G sind die im Auto installierten Steine.
Nachdem wir nun die Beziehung zwischen den dreien herausgefunden haben, konzentrieren wir uns nun darauf, wie der Gopher Ziegelsteine trägt.
Prozessor (P):
Erstellt einen Stapel von Autos (P) basierend auf dem vom Benutzer festgelegten GoMAXPROCS-Wert.
Goroutine(G):
Das Schlüsselwort Go wird verwendet, um eine Goroutine zu erstellen, was der Herstellung eines Bausteins (G) und dem anschließenden Platzieren dieses Bausteins (G) im aktuellen This entspricht Auto ist in (P).
Maschine (M):
Der Maulwurf (M) kann nicht extern erstellt werden. Es sind jedoch zu viele Steine (G) und zu wenige Maulwürfe vorhanden. Wenn es zufällig ein freies Auto (P) gibt, das nicht genutzt wird, dann leihen Sie sich noch ein paar Erdhörnchen (M) von woanders aus, bis alle Autos (P) aufgebraucht sind.
Hier ist ein Prozess, bei dem das Maulwurf (M) nicht ausreicht und das Maulwurf (M) von einer anderen Stelle entlehnt wird. Dieser Prozess besteht darin, einen Kernel-Thread (M) zu erstellen.
Es ist zu beachten, dass Gophers (M) keine Steine ohne Karren (P) transportieren können. Die Anzahl der Karren (P) bestimmt die Anzahl der Gophers (M), die in Go arbeiten können Programm ist die Anzahl der aktiven Threads;
Im Go-Programm stellen wir das G-P-M-Modell durch die folgende Abbildung dar:
P steht für „parallel“ „ Der logische Prozessor läuft, jedes P ist einem Systemthread M zugeordnet, und G repräsentiert die Go-Coroutine.
Im Go-Scheduler gibt es zwei verschiedene Ausführungswarteschlangen: die globale Ausführungswarteschlange (GRQ) und die lokale Ausführungswarteschlange (LRQ).
Jedes P hat eine LRQ, die zur Verwaltung der Goroutinen verwendet wird, die im Kontext von P ausgeführt werden sollen. Diese Goroutinen werden abwechselnd durch das an P gebundene M in den Kontext gewechselt. GRQ gilt für Goroutinen, die noch nicht P zugewiesen sind.
Wie aus der obigen Abbildung ersichtlich ist, kann die Anzahl von G viel größer sein als die Anzahl von M. Mit anderen Worten, das Go-Programm kann eine kleine Anzahl von Threads auf Kernelebene verwenden, um die Parallelität zu unterstützen einer großen Anzahl von Goroutinen. Mehrere Goroutinen teilen sich die Rechenressourcen des Kernel-Threads M durch Kontextwechsel auf Benutzerebene, es kommt jedoch zu keinem Leistungsverlust durch den Thread-Kontextwechsel für das Betriebssystem.
Um die Thread-Computing-Ressourcen voll auszunutzen, wendet der Go-Scheduler die folgenden Planungsstrategien an:
Aufgabendiebstahl (Arbeitsdiebstahl)
Wir wissen, dass die Realität so ist Einige Goroutinen laufen schnell und andere langsam, was definitiv zu dem Problem führt, dass Go zu Tode beschäftigt und untätig ist. Go erlaubt definitiv nicht die Existenz von Fishing P und nutzt zwangsläufig die Rechenressourcen voll aus .
Um die Parallelverarbeitungsfähigkeiten von Go zu verbessern und die Gesamtverarbeitungseffizienz zu erhöhen, ermöglicht der Scheduler, dass die G-Ausführung von GRQ oder LRQ anderer Ps abgerufen wird, wenn die G-Aufgaben zwischen den einzelnen Ps unausgeglichen sind.
Blockierung reduzieren
Was passiert, wenn die ausführende Goroutine Thread M blockiert? Wird Goroutine in LRQ auf P keine Planung erhalten können?
Das Blockieren in Go ist hauptsächlich in die folgenden 4 Szenarien unterteilt:
Szenario 1: Goroutine wird aufgrund von Atom-, Mutex- oder Kanaloperationsaufrufen blockiert, und der Scheduler wechselt das aktuell blockierte Goroutine Go Andere Goroutinen auf LRQ ausschalten und neu planen.
Szenario 2: Goroutine wird aufgrund von Netzwerkanforderungen und E/A-Vorgängen blockiert. Was werden unsere G und M in diesem Fall tun?
Das Go-Programm stellt einen Netzwerk-Poller (NetPoller) zur Verfügung, um Netzwerkanfragen und E/A-Vorgänge zu verarbeiten. Sein Hintergrund verwendet kqueue (MacOS), epoll (Linux) oder iocp (Windows), um die E/A-Multiplex-Nutzung zu implementieren.
Durch die Verwendung von NetPoller zum Tätigen von Netzwerksystemaufrufen kann der Planer verhindern, dass Goroutine M blockiert, während er diese Systemaufrufe durchführt. Dadurch kann M andere Goroutinen in Ps LRQ ausführen, ohne ein neues M zu erstellen. Hilft, die Planungslast des Betriebssystems zu reduzieren.
Die folgende Abbildung zeigt, wie es funktioniert: G1 wird auf M ausgeführt, und es warten 3 Goroutinen darauf, auf LRQ ausgeführt zu werden. Der Netzwerk-Poller ist inaktiv und tut nichts.
Als nächstes möchte G1 einen Netzwerksystemaufruf durchführen, daher wird er zum Netzwerkpoller verschoben und verarbeitet den asynchronen Netzwerksystemaufruf. M kann dann weitere Goroutinen aus dem LRQ ausführen. Zu diesem Zeitpunkt wird der Kontext von G2 auf M umgeschaltet.
Schließlich wird der asynchrone Netzwerksystemaufruf durch den Netzwerkpoller abgeschlossen und G1 wird zurück zum LRQ von P verschoben. Sobald G1 M in den Kontext wechseln kann, kann der Go-bezogene Code, für den es verantwortlich ist, erneut ausgeführt werden. Der große Vorteil besteht darin, dass kein zusätzliches M erforderlich ist, um Netzwerksystemaufrufe durchzuführen. Der Netzwerkpoller verwendet einen Systemthread, der jederzeit eine aktive Ereignisschleife verarbeitet.
Diese Aufrufmethode scheint sehr kompliziert zu sein, aber die Go-Sprache verbirgt diese „Komplexität“ zur Laufzeit: Go-Entwickler müssen nicht darauf achten, ob der Socket vorhanden ist Nicht blockierend, und es ist nicht erforderlich, den Rückruf des Dateideskriptors persönlich zu registrieren. Sie müssen lediglich den Socket in der „Block-E/A“-Methode in der Goroutine behandeln, die jeder Verbindung entspricht, wodurch eine einfache Goroutine realisiert wird -Verbindungsnetzwerk-Programmiermodus (eine große Anzahl von Goroutinen bringt jedoch auch zusätzliche Probleme mit sich, wie z. B. erhöhten Stapelspeicher und erhöhte Belastung des Schedulers).
Der „Block-Socket“ in Goroutine, den die Benutzerebene sieht, wird tatsächlich vom Netpoller in der Go-Laufzeit durch den Non-Block-Socket + I/O-Multiplexing-Mechanismus „simuliert“. Die Netzbibliothek in Go ist genau so implementiert.
Szenario 3: Wenn beim Aufrufen einiger Systemmethoden die Systemmethode beim Aufruf blockiert wird, kann in diesem Fall der Netzwerkpoller (NetPoller) nicht verwendet werden und die Goroutine, die den Systemaufruf durchführt, blockiert den Strom M.
Sehen wir uns die Situation an, in der ein synchroner Systemaufruf (z. B. Datei-E/A) dazu führt, dass M blockiert: G1 führt einen synchronen Systemaufruf zum Blockieren von M1 durch.
Nachdem der Scheduler eingegriffen hat: Er erkennt, dass G1 die Blockierung von M1 verursacht hat. Zu diesem Zeitpunkt trennt der Scheduler M1 von P und nimmt auch G1 weg. Der Scheduler führt dann einen neuen M2 ein, um P zu bedienen. An diesem Punkt kann G2 aus der LRQ ausgewählt und ein Kontextwechsel auf M2 durchgeführt werden.
Nachdem der blockierende Systemaufruf abgeschlossen ist: G1 kann zurück nach LRQ verschoben und erneut von P ausgeführt werden. Sollte dies erneut passieren, wird der M1 für eine spätere Wiederverwendung reserviert.
Szenario 4: Wenn in Goroutine eine Schlafoperation ausgeführt wird, wird M blockiert.
Das Go-Programm verfügt über einen Überwachungs-Thread-Sysmon im Hintergrund, der lang laufende G-Aufgaben überwacht und dann übergreifbare Bezeichner festlegt, damit andere Goroutinen sie präventiv ausführen können.
Solange diese Goroutine das nächste Mal einen Funktionsaufruf durchführt, wird sie belegt, die Szene wird ebenfalls geschützt und dann wieder in die lokale Warteschlange von P gestellt, um auf die nächste Ausführung zu warten.
Zusammenfassung
In diesem Artikel wird hauptsächlich das G-P-M-Modell aus der Perspektive der Go-Scheduler-Architektur vorgestellt und erläutert, wie dieses Modell verwendet werden kann, um eine kleine Anzahl von Kernel-Threads zu unterstützen der gleichzeitige Betrieb einer großen Anzahl von Goroutinen. Und verwenden Sie NetPoller, Sysmon usw., um Go-Programmen dabei zu helfen, Thread-Blockierungen zu reduzieren und die vorhandenen Rechenressourcen voll auszunutzen, wodurch die Betriebseffizienz von Go-Programmen maximiert wird.
Weitere Go-Sprachkenntnisse finden Sie in der Spalte Go Language Tutorial auf der chinesischen PHP-Website.
Das obige ist der detaillierte Inhalt vonWarum ist Go so „schnell'?. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!