Multithreading lernen Gehen Sie mit Bildern programmieren


Höchstwahrscheinlich haben Sie bereits von der Programmiersprache Go gehört, deren Beliebtheit ständig zunimmt, was durchaus vernünftig ist. Diese Sprache ist einfach, schnell und basiert auf einer großartigen Community. Einer der merkwürdigsten Aspekte der Sprache ist das Multithread-Programmiermodell. Mit den zugrunde liegenden Grundelementen können Sie einfach und einfach Multithread-Programme erstellen. Dieser Artikel richtet sich an diejenigen, die diese Grundelemente lernen möchten: Goroutinen und Kanäle. Und anhand der Abbildungen werde ich zeigen, wie man mit ihnen arbeitet. Ich hoffe, dass dies eine gute Hilfe für Sie in Ihrem weiteren Studium sein wird.

Einzel- und Multithread-Programme


Sie haben höchstwahrscheinlich bereits Single-Thread-Programme geschrieben. Normalerweise sieht es so aus: Es gibt eine Reihe von Funktionen zum Ausführen verschiedener Aufgaben. Jede Funktion wird nur aufgerufen, wenn die vorherige Daten darauf vorbereitet hat. Somit läuft das Programm nacheinander.

Das wird unser erstes Beispiel sein - das Erzabbauprogramm. Unsere Funktionen werden Erz suchen, abbauen und verarbeiten. Das Erz in der Mine in unserem Beispiel wird durch Listen von Strings dargestellt, Funktionen nehmen sie als Parameter und geben eine Liste von „verarbeiteten“ Strings zurück. Für ein Single-Thread-Programm wird unsere Anwendung wie folgt gestaltet:



In diesem Beispiel wird die gesamte Arbeit von einem Thread (Garys Gopher) ausgeführt. Drei Hauptfunktionen: Suche, Produktion und Verarbeitung werden nacheinander ausgeführt.

func main() { theMine := [5]string{"rock", "ore", "ore", "rock", "ore"} foundOre := finder(theMine) minedOre := miner(foundOre) smelter(minedOre) } 

Wenn wir das Ergebnis jeder Funktion drucken, erhalten wir Folgendes:

 From Finder: [ore ore ore] From Miner: [minedOre minedOre minedOre] From Smelter: [smeltedOre smeltedOre smeltedOre] 

Einfaches Design und einfache Implementierung sind ein Plus eines Single-Threaded-Ansatzes. Was aber, wenn Sie Funktionen unabhängig voneinander ausführen und ausführen möchten? Hier hilft Ihnen die Multithread-Programmierung.


Dieser Ansatz für den Erzabbau ist viel effizienter. Jetzt arbeiten mehrere Threads (Gophers) unabhängig voneinander, und Gary macht nur einen Teil der Arbeit. Ein Gopher sucht nach Erz, der andere produziert und der dritte schmilzt, und all dies ist möglicherweise gleichzeitig. Um diesen Ansatz zu implementieren, benötigen wir zwei Dinge im Code: Gopher-Prozessoren unabhängig voneinander zu erstellen und Erz zwischen ihnen zu übertragen. Go hat dafür Goroutinen und Kanäle.

Gorutins


Goroutinen können als "leichte Threads" betrachtet werden. Um Goroutinen zu erstellen, müssen Sie nur das Schlüsselwort go vor den Funktionsaufrufcode setzen. Um zu demonstrieren, wie einfach es ist, erstellen wir zwei Suchfunktionen, rufen sie mit dem Schlüsselwort go auf und drucken jedes Mal eine Nachricht, wenn sie das „Erz“ in ihrer Mine finden.


 func main() { theMine := [5]string{"rock", "ore", "ore", "rock", "ore"} go finder1(theMine) go finder2(theMine) <-time.After(time.Second * 5) //       } 

Die Ausgabe unseres Programms wird wie folgt sein:

 Finder 1 found ore! Finder 2 found ore! Finder 1 found ore! Finder 1 found ore! Finder 2 found ore! Finder 2 found ore! 

Wie Sie sehen können, gibt es keine Reihenfolge, in der die Funktion zuerst Erz findet. Suchfunktionen arbeiten gleichzeitig. Wenn Sie das Beispiel mehrmals ausführen, ist die Reihenfolge anders. Jetzt können wir Multithread-Programme (Multi-Sphere-Programme) ausführen, und dies ist ein schwerwiegender Fortschritt. Aber was tun, wenn wir eine Verbindung zwischen unabhängigen Goroutinen herstellen müssen? Die Zeit für die Magie der Kanäle kommt.

Kanäle



Über Kanäle können Goroutinen Daten austauschen. Dies ist eine Art Rohr, durch das Goroutins Informationen von anderen Goroutinen senden und empfangen können.

Das Lesen und Schreiben in den Kanal erfolgt mit dem Pfeiloperator (<-), der die Richtung der Datenbewegung angibt.

 myFirstChannel := make(chan string) myFirstChannel <- "hello" //    myVariable := <- myFirstChannel //    

Jetzt muss unser Gopher-Scout kein Erz mehr ansammeln, er kann es sofort über Kanäle weiter übertragen.

Ich habe das Beispiel aktualisiert, jetzt ist der Code des Erzfinders und Bergmanns anonyme Funktionen. Machen Sie sich nicht zu viele Sorgen, wenn Sie ihnen noch nicht begegnet sind. Denken Sie jedoch daran, dass jeder von ihnen mit dem Schlüsselwort go aufgerufen wird. Daher wird er in seiner eigenen Goroutine ausgeführt. Das Wichtigste dabei ist, dass Goroutinen Daten über den oreChan- Kanal untereinander übertragen. Und wir werden uns näher am Ende mit anonymen Funktionen befassen.

 func main() { theMine := [5]string{“ore1”, “ore2”, “ore3”} oreChan := make(chan string) //   go func(mine [5]string) { for _, item := range mine { oreChan <- item // } }(theMine) //   go func() { for i := 0; i < 3; i++ { foundOre := <-oreChan // fmt.Println(“Miner: Received “ + foundOre + “ from finder”) } }() <-time.After(time.Second * 5) //     } 

Die folgende Schlussfolgerung zeigt deutlich, dass unser Bergmann dreimal eine Portion nach der anderen vom Kanal erhält.

 Miner: Received ore1 from finder Miner: Received ore2 from finder Miner: Received ore3 from finder 

Jetzt können wir Daten zwischen verschiedenen Goroutinen (Gophern) übertragen. Bevor wir jedoch mit dem Schreiben eines komplexen Programms beginnen, wollen wir uns einige wichtige Eigenschaften von Kanälen ansehen.

Schlösser


In einigen Situationen kann Goroutin beim Arbeiten mit Kanälen blockiert sein. Dies ist notwendig, damit die Goroutinen miteinander synchronisiert werden können, bevor sie beginnen oder weiterarbeiten.

Schreibsperre




Wenn Goroutine (Gopher) Daten an einen Kanal sendet, werden diese blockiert, bis eine andere Goroutine Daten aus dem Kanal liest.

Sperre lesen




Ähnlich wie beim Sperren beim Schreiben in einen Kanal kann Goroutin beim Lesen von einem Kanal gesperrt werden, bis nichts mehr darauf geschrieben wird.
Wenn Ihnen die Schlösser auf den ersten Blick kompliziert erscheinen, können Sie sie sich als „Geldtransfer“ zwischen zwei Goroutinen (Gophers) vorstellen. Wenn ein Gopher Geld überweisen oder erhalten möchte, muss er auf den zweiten Teilnehmer der Transaktion warten.

Nachdem wir uns mit Goroutine-Sperren für Kanäle befasst haben, wollen wir zwei verschiedene Arten von Kanälen diskutieren: gepuffert und ungepuffert. Wenn wir diesen oder jenen Typ wählen, bestimmen wir weitgehend das Verhalten des Programms.

Ungepufferte Kanäle




In allen vorherigen Beispielen haben wir nur solche Kanäle verwendet. Auf solchen Kanälen kann jeweils nur ein Datenelement übertragen werden (mit Blockierung, wie oben beschrieben).

Gepufferte Kanäle




Streams in einem Programm können nicht immer perfekt synchronisiert werden. Angenommen, in unserem Beispiel hat ein Gopher-Scout drei Teile Erz gefunden, und ein Gopher-Bergmann hat es geschafft, nur einen Teil der gefundenen Reserven gleichzeitig zu gewinnen. Damit die Gopher-Aufklärung nicht die meiste Zeit damit verbringt, darauf zu warten, dass der Bergmann seine Arbeit beendet, werden wir gepufferte Kanäle verwenden. Beginnen wir mit der Erstellung eines Kanals mit einer Kapazität von 3.

 bufferedChan := make(chan string, 3) 

Wir können mehrere Daten an den gepufferten Kanal senden, ohne sie mit einer anderen Goroutine lesen zu müssen. Dies ist der Hauptunterschied zu ungepufferten Kanälen.


 bufferedChan := make(chan string, 3) go func() { bufferedChan <- "first" fmt.Println("Sent 1st") bufferedChan <- "second" fmt.Println("Sent 2nd") bufferedChan <- "third" fmt.Println("Sent 3rd") }() <-time.After(time.Second * 1) go func() { firstRead := <- bufferedChan fmt.Println("Receiving..") fmt.Println(firstRead) secondRead := <- bufferedChan fmt.Println(secondRead) thirdRead := <- bufferedChan fmt.Println(thirdRead) }() 

Die Ausgabereihenfolge in einem solchen Programm ist wie folgt:

 Sent 1st Sent 2nd Sent 3rd Receiving.. first second third 

Um unnötige Komplikationen zu vermeiden, werden wir in unserem Programm keine gepufferten Kanäle verwenden. Es ist jedoch wichtig zu bedenken, dass diese Kanaltypen auch zur Verwendung verfügbar sind.
Es ist auch wichtig zu beachten, dass gepufferte Kanäle Sie nicht immer vor dem Blockieren bewahren. Wenn beispielsweise ein Gopher-Scout zehnmal schneller als ein Gopher-Miner ist und über einen gepufferten Kanal mit einer Kapazität von 2 verbunden ist, wird der Gopher-Scout bei jedem Senden blockiert, wenn sich bereits zwei Daten im Kanal befinden.

Alles zusammenfügen


Mit Goroutinen und Kanälen können wir also ein Programm schreiben, das alle Vorteile der Multithread-Programmierung in Go nutzt.


 theMine := [5]string{"rock", "ore", "ore", "rock", "ore"} oreChannel := make(chan string) minedOreChan := make(chan string) //  go func(mine [5]string) { for _, item := range mine { if item == "ore" { oreChannel <- item //   oreChannel } } }(theMine) //  go func() { for i := 0; i < 3; i++ { foundOre := <-oreChannel //   oreChannel fmt.Println("From Finder: ", foundOre) minedOreChan <- "minedOre" //   minedOreChan } }() //  go func() { for i := 0; i < 3; i++ { minedOre := <-minedOreChan //   minedOreChan fmt.Println("From Miner: ", minedOre) fmt.Println("From Smelter: Ore is smelted") } }() <-time.After(time.Second * 5) //     

Ein solches Programm gibt Folgendes aus:

 From Finder: ore From Finder: ore From Miner: minedOre From Smelter: Ore is smelted From Miner: minedOre From Smelter: Ore is smelted From Finder: ore From Miner: minedOre From Smelter: Ore is smelted 

Im Vergleich zu unserem ersten Beispiel ist dies eine wesentliche Verbesserung. Jetzt werden alle Funktionen unabhängig voneinander ausgeführt, jede in ihrer eigenen Goroutine. Außerdem haben wir einen Förderer aus Kanälen bekommen, durch den das Erz unmittelbar nach der Verarbeitung transportiert wird. Um mich weiterhin auf ein grundlegendes Verständnis der Funktionsweise von Kanälen und Goroutinen zu konzentrieren, habe ich einige Punkte weggelassen, die zu Schwierigkeiten beim Start des Programms führen können. Abschließend möchte ich auf diese Merkmale der Sprache eingehen, da sie bei der Arbeit mit Goroutinen und Kanälen helfen.

Anonyme Gorutinen




So wie wir eine reguläre Funktion in goroutine ausführen, können wir eine anonyme Funktion unmittelbar nach dem Schlüsselwort go deklarieren und sie mit der folgenden Syntax aufrufen:

 //   go func() { fmt.Println("I'm running in my own go routine") }() 

Wenn wir also eine Funktion nur an einer Stelle aufrufen müssen, können wir sie in einer separaten Goroutine ausführen, ohne uns vorher um ihre Deklaration kümmern zu müssen.

Die Hauptfunktion ist Goroutine.




Ja, die Hauptfunktion funktioniert in einer eigenen Goroutine. Und was noch wichtiger ist, nach seiner Fertigstellung enden auch alle anderen Goroutinen. Aus diesem Grund haben wir am Ende unserer Hauptfunktion einen Timer-Aufruf getätigt. Dieser Aufruf erstellt einen Kanal und sendet nach 5 Sekunden Daten an diesen.

 <-time.After(time.Second * 5) //       

Denken Sie daran, dass die Goroutine beim Lesen aus dem Kanal blockiert wird, bis etwas an ihn gesendet wird? Dies ist genau das, was passiert, wenn der angegebene Code hinzugefügt wird. Die Hauptgoroutine wird blockiert, so dass die anderen Goroutien 5 Sekunden Zeit zum Arbeiten haben. Diese Methode funktioniert gut, aber normalerweise wird ein anderer Ansatz verwendet, um zu überprüfen, ob alle Goroutinen ihre Arbeit abgeschlossen haben. Um ein Signal über den Abschluss der Arbeit zu senden, wird ein spezieller Kanal erstellt, die Hauptgoroutine wird daran gehindert, daraus zu lesen, und sobald die Tochtergoroutine ihre Arbeit beendet hat, schreibt sie in diesen Kanal. Die Hauptgoroutine ist entsperrt und das Programm endet.



 func main() { doneChan := make(chan string) go func() { //  -  doneChan <- “I'm all done!” }() <-doneChan //        } 

Lesen Sie aus einer Pipe in einer For-Range-Schleife


In unserem Beispiel haben wir in der Funktion des Goffer-Getters die for- Schleife verwendet, um drei Elemente aus dem Kanal auszuwählen. Aber was tun, wenn nicht im Voraus bekannt ist, wie viele Daten sich im Kanal befinden können? In solchen Fällen können Sie den Kanal wie bei Sammlungen als Argument für die for-range- Schleife verwenden. Die aktualisierte Funktion sieht möglicherweise folgendermaßen aus:

  //  go func() { for foundOre := range oreChan { fmt.Println(“Miner: Received “ + foundOre + “ from finder”) } }() 

Auf diese Weise liest der Erzbergmann alles, was der Scout ihm sendet, und die Verwendung des Kanals im Zyklus garantiert dies. Bitte beachten Sie, dass der Zyklus nach dem Verarbeiten aller Daten vom Kanal beim Lesen gesperrt wird. Um ein Blockieren zu vermeiden, müssen Sie den Kanal schließen, indem Sie close (channel) aufrufen.

Nicht blockierendes Kanallesen


Mit dem Select-Case- Konstrukt kann das Blockieren von Lesevorgängen aus der Pipe vermieden werden. Das folgende Beispiel zeigt die Verwendung dieser Konstruktion: goroutine liest Daten aus dem Kanal, wenn sie nur vorhanden sind, andernfalls wird der Standardblock ausgeführt:

 myChan := make(chan string) go func(){ myChan <- “Message!” }() select { case msg := <- myChan: fmt.Println(msg) default: fmt.Println(“No Msg”) } <-time.After(time.Second * 1) select { case msg := <- myChan: fmt.Println(msg) default: fmt.Println(“No Msg”) } 

Nach dem Start gibt dieser Code Folgendes aus:

 No Msg Message! 

Nicht blockierende Kanalaufzeichnung


Sperren beim Schreiben in einen Kanal können vermieden werden, indem dasselbe Select-Case- Konstrukt verwendet wird. Nehmen wir eine kleine Änderung am vorherigen Beispiel vor:

 select { case myChan <- “message”: fmt.Println(“sent the message”) default: fmt.Println(“no message sent”) } 

Was weiter zu studieren




Es gibt eine große Anzahl von Artikeln und Berichten, die die Arbeit mit Kanälen und Goroutinen viel detaillierter behandeln. Mit dem Code haben Sie eine klare Vorstellung davon, warum und wie diese Tools verwendet werden. So können Sie die folgenden Materialien optimal nutzen:



Vielen Dank, dass Sie sich die Zeit zum Lesen genommen haben. Ich hoffe, ich habe Ihnen geholfen, die Kanäle, Goroutinen und die Vorteile von Multithread-Programmen zu verstehen.

Source: https://habr.com/ru/post/de412715/


All Articles