• Zuhause
  • Artikel
  • Was sind Goroutinen? Und wie funktionieren sie eigentlich?
Veröffentlicht am 12-03-2019

Was sind Goroutinen? Und wie funktionieren sie eigentlich?

Einer der Hauptgründe, warum Go Language in den letzten Jahren eine unglaubliche Popularität erlangt hat, ist die Einfachheit, die es mit seinen leichten Goroutinen und Kanälen für Parallelität gibt.

Nebenläufigkeit ist nicht unbedingt etwas Neues, sie existiert schon seit langer Zeit in Form von Threads, die heute in fast allen Anwendungen weit verbreitet sind.

Bevor wir jedoch wirklich verstehen, was Goroutines sind, und nein, es handelt sich nicht um leichtgewichtige Threads (obwohl Goroutines auf Thread angewiesen sind, um ausgeführt zu werden), werden wir uns zunächst mit der Funktionsweise von Threads im Betriebssystem beschäftigen.

Was sind Threads?

Ein Thread ist die kleinste Verarbeitungseinheit, die in einem Betriebssystem ausgeführt werden kann. In den meisten modernen Betriebssystemen ist ein Thread in einem Prozess vorhanden, dh ein einzelner Prozess kann mehrere Threads enthalten.

Ein gutes Beispiel ist ein Webserver.

Ein Webserver ist normalerweise für die gleichzeitige Bearbeitung mehrerer Anfragen ausgelegt. Und diese Anforderungen sind normalerweise unabhängig voneinander.

So kann ein Thread erstellt oder einem Thread-Pool entnommen und Anforderungen delegiert werden, um Parallelität zu erzielen. Aber erinnern Sie sich an das berühmte Gespräch von Rob Pike: "Parallelität ist kein Parallelismus".

Aber ist ein Faden leichter als ein Prozess? Mal schauen.

Kommt drauf an, wie Sie es betrachten.

Theoretisch teilt ein Thread den Speicher mit einem anderen Thread und muss bei der Erstellung keinen neuen virtuellen Speicherplatz erstellen, ohne dass ein Kontextwechsel der MMU (Memory Management Unit) erforderlich ist. Außerdem ist die Kommunikation einfacher als Prozesse, vor allem, weil sie einen gemeinsamen Speicher haben können, während Prozesse verschiedene Arten von IPC (Inter-Process Communications) wie Semaphore, Message Queues, Pipes usw. erfordern.

Macht das Threads also immer leistungsfähiger als Prozesse? Nicht in dieser Multiprozessor-Welt, in der wir leben.

z.B. Linux unterscheidet nicht zwischen Threads und Prozessen. Beide werden Tasks genannt. Jede Aufgabe kann beim Klonen eine minimale bis maximale Freigabeebene aufweisen.

Wenn Sie Fork () aufrufen, wird eine neue Aufgabe ohne gemeinsam genutzte Dateideskriptoren, PIDs und Speicherplatz erstellt. Wenn Sie pthread_create () aufrufen, wird eine neue Aufgabe erstellt, in der alle oben genannten gemeinsam genutzt werden.

Das Synchronisieren von Daten sowohl im gemeinsam genutzten Speicher als auch im L1-Cache von Tasks, die auf mehreren Kernen ausgeführt werden, erfordert mehr Aufwand als das Ausführen verschiedener Prozesse im isolierten Speicher.

Linux-Entwickler haben versucht, die Kosten zwischen dem Task-Switch zu minimieren und haben es geschafft. Das Erstellen einer neuen Aufgabe ist immer noch ein größerer Aufwand als bei einem neuen Thread, das Wechseln jedoch nicht.

Wo können Threads verbessert werden?

Es gibt drei Dinge, die Threads langsam machen:

  1. Threads haben eine große Stapelgröße (≥ 1 MB) und verbrauchen daher viel Speicher. Stellen Sie sich vor, Sie sollten 1000s Thread erstellen, wenn Sie bereits 1 GB Speicher benötigen. Das ist viel!
    1. Threads müssen viele Register wiederherstellen, darunter AVX (erweiterte Vektorerweiterung), SSE (Streaming SIMD Ext.), Gleitpunktregister, Programmzähler (PC) und Stack Pointer (SP), die die Anwendungsleistung beeinträchtigen.
    2. Das Einrichten und Abbauen von Threads erfordert einen Aufruf des Betriebssystems für Ressourcen (z. B. Speicher), was langsam ist. NICHT GUT!
    3. Was ist mit Goroutinen?

      Goroutinen sind die Möglichkeit, gleichzeitig Aufgaben in Golang zu erledigen. Sie sind nur im virtuellen Raum der Go-Laufzeitumgebung und nicht im Betriebssystem vorhanden. Daher wird der Go-Laufzeitzeitplaner zur Verwaltung der Lebenszyklen benötigt. Beachten Sie, dass das Betriebssystem nur einen Prozess auf Benutzerebene sieht, der mehrere Threads anfordert und ausführt. Die Goroutinen selbst werden vom Go Runtime Scheduler verwaltet.

      Diagramm der Beziehung zwischen Laufzeit, Betriebssystem und Go-Code für Entwickler

      Go Runtime verwaltet zu diesem Zweck drei C-Strukturen: (https://golang.org/src/runtime/runtime2.go)

      1. The G Struct: Stellt eine einzelne Goroutine dar und enthält die Felder, die erforderlich sind, um den Stack und den aktuellen Status zu verfolgen. Es enthält auch Verweise auf den Code, für den es verantwortlich ist.
        1. The M Struct: Stellt einen OS-Thread dar. Es enthält auch Zeiger auf Felder wie die globale Warteschlange der ausführbaren Goroutinen, die aktuell ausgeführte Goroutine, ihren eigenen Cache und die Referenz zum Scheduler
        2. Die Sched-Struktur: Es handelt sich um eine einzelne, globale Struktur, die die verschiedenen Warteschlangen von Goroutines und Ms sowie einige andere Informationen aufzeichnet, die der Scheduler zum Ausführen benötigt, z.
        3. Es gibt zwei Warteschlangen mit G-Strukturen, eine in der ausführbaren Warteschlange, in der M (Threads) mehr Arbeit finden können, und die andere ist eine freie Liste von Goroutinen. Es gibt nur eine Warteschlange für Ms (Threads), die der Scheduler verwaltet. Und um diese Warteschlangen zu ändern, muss die globale Zeitplansperre gehalten werden.

          Beim Start startet go runtime eine Reihe von Goroutinen für GC, Scheduler und Benutzercode. Ein OS-Thread wird erstellt, um diese Goroutinen zu behandeln. Diese Threads können höchstens mit GOMAXPROCS übereinstimmen (Dies ist standardmäßig auf 1 gesetzt, aber für die beste Leistung wird normalerweise die Anzahl der Prozessoren auf Ihrem Computer festgelegt).

          Hier ist der Haken! (OK)

          Um die Stapel klein zu machen, verwendet die Laufzeit von Go skalierbare, begrenzte Stapel mit anfänglich nur 2 KB / Goroutine. Eine neu geprägte Goroutine erhält einige Kilobyte, was fast immer ausreicht. Wenn dies nicht der Fall ist, vergrößert (und verkleinert) die Laufzeit den Speicher für das automatische Speichern des Stapels, sodass viele Goroutines in einem bescheidenen Speicherbereich leben können. Der CPU-Aufwand beträgt durchschnittlich drei billige Anweisungen pro Funktionsaufruf. Es ist praktisch, Hunderttausende von Goroutinen in demselben Adressraum zu erstellen. Wenn Goroutinen nur Threads wären, würden die Systemressourcen mit einer viel geringeren Anzahl ausgehen.

          Threads sind der Kojote und die Goroutinen der Roadrunner. : P

          Blockierung? Kein Problem!

          Wenn eine Coroutine einen Blockierungsaufruf ausführt, z. B. durch Aufrufen eines blockierenden Systemaufrufs, muss der laufende Thread blockiert werden, und die Laufzeitumgebung verschiebt automatisch andere Coroutinen auf demselben Betriebssystemthread in einen anderen ausführbaren Thread aus der Warteschlange von Scheduler (die Sched Struct), damit sie nicht blockiert werden. Daher sollte von der Laufzeit mindestens ein weiterer Thread erstellt werden, um die Ausführung anderer Goroutines fortzusetzen, die keine Aufrufe blockieren. Der Programmierer sieht nichts davon, worauf es ankommt. Das Ergebnis, das wir als Goroutines bezeichnen, kann sehr billig sein: Sie haben wenig zusätzlichen Aufwand für den Stack, der nur wenige Kilobyte beträgt.

          Go-Routinen lassen sich daher gut skalieren.

          Wenn Sie jedoch Kanäle zur Kommunikation verwenden, die in Go nur im virtuellen Raum vorhanden sind, blockiert das Betriebssystem den Thread nicht. Diese Goroutinen gehen einfach in den Wartezustand und eine andere lauffähige Goroutine (von der M struct) ist an ihrem Ort geplant.

          Runtime Scheduler aufrufen

          Der Go-Laufzeitplaner verfolgt jede Goroutine und plant, dass sie nacheinander in einem Pool von Threads ausgeführt werden, die zu einem Prozess gehören.

          Der Go-Runtime-Scheduler führt eine kooperative Planung durch, was bedeutet, dass eine weitere Goroutine nur dann geplant wird, wenn die aktuelle Version blockiert oder ausgeführt wird. Dies kann leicht über Code erfolgen. Hier sind einige Beispiele:

          • Blockieren von Systemaufrufen wie Datei- und Netzwerkoperationen.
            • Nachdem der Müllabfuhrzyklus gestoppt wurde.
            • Dies ist besser als die vorbeugende Planung, bei der ein neuer Thread mit rechtzeitigen Systemunterbrechungen (z. B. alle 10 ms) blockiert und geplant wird, was dazu führen kann, dass eine Task länger als zum Abschluss benötigt, wenn die Anzahl der Threads steigt oder wenn Tasks mit höherer Priorität benötigt werden geplant werden, während eine Task mit niedrigerer Priorität ausgeführt wird.

              Ein weiterer Vorteil ist, dass der Code implizit aufgerufen wird, z. Während des Sleep- oder Channel-Wartens muss das Compile nur die Register sichern / wiederherstellen, die an diesen Punkten aktiv sind. In Go bedeutet dies, dass nur 3 Register, d. H. PC, SP und DX (Datenregister), während der Kontextumschaltung anstelle aller Register (z. B. AVX, Floating Point, MMX) aktualisiert werden.

              Wenn Sie mehr über die Parallelität von go erfahren möchten, können Sie auf die folgenden Links verweisen:

              • Parallelität ist kein Parallelismus von Rob Pike (Muss für jeden Go-Entwickler aufpassen)
                • Analyse des Go-Laufzeitplaners
                • Wie der Beitrag? Also lasst uns CLAP IT!

Siehe auch

Das Dilemma des Android-BenutzersInklusive Einstellung und bessere JobbeschreibungenNau Mai Haere Mai Ki Dev Academy, Newmarket.Treffen Sie den Einstellungsmanager - Emma PorteusWie man als Niemand am Arbeitsplatz eskaliertWORBLI für Austrades Dubai Blockchain Mission ausgewählt