Los geht's Golang Parallelität, Teil 1

Überblick

Jede erfolgreiche Programmiersprache hat eine Killer-Funktion, die sie erfolgreich gemacht hat. Go's Stärke ist die gleichzeitige Programmierung. Es wurde für ein starkes theoretisches Modell (CSP) entwickelt und bietet eine Syntax auf Sprachebene in Form des Schlüsselworts "go", das eine asynchrone Task startet (ja, die Sprache ist nach dem Schlüsselwort benannt) sowie eine integrierte Methode zwischen gleichzeitigen Aufgaben kommunizieren. 

In diesem Artikel (erster Teil) werde ich das CSP-Modell vorstellen, das von Goos Parallelität implementiert, goroutines und wie der Betrieb mehrerer kooperierender goroutines synchronisiert wird. In einem zukünftigen Artikel (Teil zwei) werde ich über Gos Kanäle und über das Koordinieren von Goroutines ohne synchronisierte Datenstrukturen schreiben.

CSP

CSP steht für Kommunizieren sequentieller Prozesse. Es wurde erstmals 1978 von Tony (C.A.R.) Hoare eingeführt. CSP ist ein übergeordnetes Framework zur Beschreibung gleichzeitiger Systeme. Es ist viel einfacher, parallele Programme zu programmieren, wenn auf der CSP-Abstraktionsebene gearbeitet wird, als auf der typischen Abstraktionsebene für Threads und Sperren.

Goroutinen

Goroutinen sind ein Spiel auf Coroutinen. Sie sind jedoch nicht genau dasselbe. Eine Goroutine ist eine Funktion, die in einem separaten Thread vom ablaufenden Thread ausgeführt wird, sodass sie ihn nicht blockiert. Mehrere Goroutinen können denselben Betriebssystem-Thread gemeinsam nutzen. Im Gegensatz zu Coroutinen können Goroutinen nicht explizit die Kontrolle über eine andere Goroutine ausüben. Gos Laufzeit sorgt dafür, dass die Kontrolle implizit übertragen wird, wenn eine bestimmte Goroutine den E / A-Zugriff blockiert. 

Lassen Sie uns etwas Code sehen. Das folgende Go-Programm definiert eine Funktion, die kreativ "f" genannt wird, die bis zu einer halben Sekunde zufällig schläft und dann ihr Argument ausgibt. Das Main() Funktion ruft die f () Funktion in einer Schleife von vier Iterationen, wobei sie in jeder Iteration aufgerufen wird f () dreimal mit "1", "2" und "3" hintereinander. Wie zu erwarten, ist die Ausgabe:

--- Führen Sie nacheinander die normalen Funktionen aus. 1 2 3 1 2 3 1 2 3 1 2 3

Dann ruft main auf f () als Goroutine in einer ähnlichen Schleife. Jetzt sind die Ergebnisse anders, weil Gos Laufzeit die f goroutines gleichzeitig, und da der zufällige Schlaf zwischen den goroutines unterschiedlich ist, erfolgt das Drucken der Werte nicht in der Reihenfolge f () wurde angerufen. Hier ist die Ausgabe:

--- Laufen Sie gleichzeitig als Goroutinen 2 2 3 1 3 2 1 3 1 1 3 2 2 1 3

Das Programm selbst verwendet die Standard-Bibliothekspakete "time" und "math / rand", um das zufällige Einschlafen und Warten zu implementieren, bis alle Goroutinen abgeschlossen sind. Dies ist wichtig, da das Programm beendet ist, wenn der Haupt-Thread beendet ist, selbst wenn noch ausstehende Goroutines ausgeführt werden.

Pakethauptimport ("fmt" "time" "math / rand") var r = rand.New (rand.NewSource (time.Now (). UnixNano ())) func f (s string) // Schlaf bis halbe Sekunde Verzögerung: = time.Duration (r.Int ()% 500) * time.Millisecond time.Sleep (delay) fmt.Println (s) func main () fmt.Println ("---) Nacheinander ausführen als normale Funktionen ") für i: = 0; ich < 4; i++  f("1") f("2") f("3")  fmt.Println("--- Run concurrently as goroutines") for i := 0; i < 5; i++  go f("1") go f("2") go f("3")  // Wait for 6 more seconds to let all go routine finish time.Sleep(time.Duration(6) * time.Second) fmt.Println("--- Done.") 

Synchronisierungsgruppe

Wenn Sie eine Reihe wilder Goroutines haben, die überall herumlaufen, möchten Sie oft wissen, wann sie fertig sind. 

Es gibt verschiedene Möglichkeiten, dies zu tun, aber eine der besten Methoden ist die Verwendung von a WaitGroup. EIN WaitGroup ist ein Typ, der im "sync" -Paket definiert ist, das die Hinzufügen(), Erledigt() und Warten() Operationen. Es funktioniert wie ein Zähler, der zählt, wie viele Go-Routinen noch aktiv sind, und wartet, bis alle erledigt sind. Immer wenn Sie eine neue Goroutine starten, rufen Sie an Hinzufügen (1) (Sie können mehrere hinzufügen, wenn Sie mehrere Go-Routinen starten.) Wenn eine Goroutine fertig ist, ruft sie auf Erledigt(), was die Zahl um eins verringert, und Warten() blockiert, bis der Zähler null erreicht. 

Lassen Sie uns das vorherige Programm in a konvertieren WaitGroup anstatt für sechs Sekunden zu schlafen, nur für den Fall. Notiere dass der f () Funktion verwendet wg.Done () verschieben anstatt zu rufen wg.Done () direkt. Dies ist nützlich um sicherzustellen wg.Done () wird immer aufgerufen, auch wenn ein Problem vorliegt und die Goroutine vorzeitig beendet wird. Andernfalls wird die Zählung niemals Null erreichen, und wg.Wait () kann für immer blockieren.

Ein weiterer kleiner Trick ist, dass ich anrufe wg.Add (3) nur einmal vor dem Aufruf f () drei Mal. Beachten Sie, dass ich anrufe wg.Add () auch beim Anrufen f () als regelmäßige Funktion. Dies ist notwendig, weil f () Anrufe wg.Done () unabhängig davon, ob es als Funktion oder als Goroutine ausgeführt wird.

Paket-Hauptimport ("fmt" "time" "math / rand" "sync") var r = rand.New (rand.NewSource (time.Now (). UnixNano ())) var wg sync.WaitGroup func f (s string) verschiebt wg.Done () // Schlaf bis zu einer halben Sekunde Verzögerung: = time.Duration (r.Int ()% 500) * time.Millisecond time.Sleep (delay) fmt.Println (s) func main () fmt.Println ("--- Nacheinander als normale Funktionen ausführen") für i: = 0; ich < 4; i++  wg.Add(3) f("1") f("2") f("3")  fmt.Println("--- Run concurrently as goroutines") for i := 0; i < 5; i++  wg.Add(3) go f("1") go f("2") go f("3")  wg.Wait() 

Synchronisierte Datenstrukturen

Die Goroutinen im 1,2,3-Programm kommunizieren nicht miteinander oder bearbeiten gemeinsame Datenstrukturen. In der realen Welt ist dies oft notwendig. Das "Sync" -Paket liefert den Mutex-Typ mit Sperren() und Freischalten() Methoden, die gegenseitigen Ausschluss bieten. Ein gutes Beispiel ist die Standard-Go-Karte. 

Es ist vom Design nicht synchronisiert. Das bedeutet, dass, wenn mehrere Goroutines gleichzeitig ohne externe Synchronisation auf dieselbe Karte zugreifen, die Ergebnisse nicht vorhersagbar sind. Wenn sich jedoch alle Goroutines bereit erklären, vor jedem Zugriff einen freigegebenen Mutex zu erwerben und ihn später freizugeben, wird der Zugriff serialisiert.

Alles zusammenfügen

Lassen Sie uns alles zusammenstellen. Die berühmte Tour of Go bietet eine Übung zum Erstellen eines Web-Crawlers. Sie bieten einen großartigen Rahmen mit einem Mock-Fetcher und Ergebnissen, mit denen Sie sich auf das anstehende Problem konzentrieren können. Ich empfehle dringend, dass Sie versuchen, es selbst zu lösen.

Ich habe eine komplette Lösung mit zwei Ansätzen geschrieben: eine synchronisierte Karte und Kanäle. Den vollständigen Quellcode finden Sie hier.

Hier sind die relevanten Teile der "Sync" -Lösung. Zuerst definieren wir eine Map mit einer Mutex-Struktur, um die abgerufenen URLs zu speichern. Beachten Sie die interessante Syntax, bei der ein anonymer Typ in einer Anweisung erstellt, initialisiert und einer Variablen zugewiesen wird.

var fetchedUrls = struct urls map [Zeichenfolge] bool m sync.Mutex urls: make (map [Zeichenfolge] bool)

Jetzt kann der Code das sperren m Mutex vor dem Zugriff auf die Map von URLs und Entsperren, wenn es fertig ist.

// Prüfen Sie, ob diese URL bereits abgerufen wurde (oder abgerufen wird) fetchedUrls.m.Lock (), wenn fetchedUrls.urls [url] fetchedUrls.m.Unlock () return // OK zurückgegeben wird. Lassen Sie uns diese URL holen fetchedUrls.urls [url] = true fetchedUrls.m.Unlock ()

Dies ist nicht völlig sicher, da jeder andere Zugriff auf das Internet hat holtUrls variabel und vergessen zu sperren oder zu entsperren. Ein robusteres Design bietet eine Datenstruktur, die sichere Operationen durch automatisches Sperren / Entsperren unterstützt.

Fazit

Go bietet hervorragende Unterstützung für Parallelität mit leichten Goroutinen. Es ist viel einfacher zu verwenden als traditionelle Threads. Wenn Sie den Zugriff auf gemeinsam genutzte Datenstrukturen synchronisieren müssen, hat Go die Verbindung zum sync.Mutex

Es gibt noch viel mehr über Gos Parallelität zu berichten. Bleib dran…