Channels in Go: Bequem parallel
Seite 3: Zahl der verfügbaren CPUs zur Laufzeit
Go kann die Zahl der verfügbaren CPUs zur Laufzeit ermitteln, hier per numcpu
. Bei I/O-intensiven Go-Routinen und ähnlichen Situationen kann die Anwendung den Wert auch mit einem Faktor versehen. Sie startet dann numcpu
-Instanzen, denen es über den Channel fnchan
den Dateinamen übergibt. Welche Go-Routine das nächste Datum aus nchan
erhält, entscheidet die Go-Runtime. Sind alle Go-Routinen beschäftigt, wartet der Aufrufer. Die Go-Routinen senden Werte – oder Fehlermeldungen – an eine Empfänger-Routine. main()
kann sich derweil anderen Dingen widmen.
Mit dem Schließen von fnchan
enden auch die Go-Routinen. Mit einem Trick lässt sich die WaitGroup
umgehen: scaler()
sendet vor dem Return einen leeren String, und der Empfänger zählt, bis von numcpu
leere Strings empfangen wurden. Der leere String ist nur ein Beispiel – eine Struktur mit einem Boolean tut es ebenso wie bei Pointern der Wert nil
.
Auf eine Falle ist hier zu achten: Die Schleife im Empfänger wird zwar durch close(msgchan)
verlassen, aber vielleicht geschieht das nicht sofort. Um danach auf die Ergebnisse zuzugreifen, wartet main()
auf die Fertigmeldung über den Bool-Channel done
. Erst wenn diese Meldung eintrifft, sind sowohl die Go-Routinen scale
als auch die Empfänger-Go-Routine beendet.
Diese Umsetzung reicht in den meisten Fällen aus. Sollte es im Channel msgchan
häufig Stau geben, kann das Programm diesen Kanal auch mit einem Puffer für mehrere Objekte generieren. Dabei können mehrere Empfängerroutinen parallel laufen, sofern dabei keine Konflikte auftreten können. Zumindest der Empfang von Fehlermeldungen dürfte in der Regel über eine gesonderte Go-Routine laufen, wenn das Paket log
nicht ausreicht (Logging ist Thread-sicher). Wer das mit Mutexen realisieren möchte, muss mit deutlich mehr Schwierigkeiten rechnen.
Mengenbeschränkung für Ungeduldige
Der Code aus dem vorherigen Listing taugt für die meisten Fälle, aber nicht für alle. Beispielsweise dauerte das rekursive Bearbeiten eines großen Dateibaums zu lange. Eine bei jedem gefundenen Verzeichnis neu gestartete Go-Routine würde ähnlich wie im vorigen Abschnitt funktionieren, doch existiert ein einfacherer Weg, der in diesem Listing zu finden ist:
import (
"io/fs"
"os"
"runtime"
)
var gocnt chan bool
func main() {
gocnt = make(chan bool, 4*runtime.NumCPU())
parseDir(".")
}
func parseDir(dir string) {
defer func() {
<-gocnt
}()
gocnt <- true
flist, _ := os.ReadDir(dir)
for _, entry := range flist {
if entry.Type() & fs.ModeType == 0 {
// entry ist eine Datei
do_something_heavy_with(entry)
return
}
if entry.IsDir() {
parseDir(entry.Name())
}
}
}
Dieser Code zeigt die einfache Beschränkung der Zahl der Go-Routinen.
Dort erzeugt das Programm einen Channel mit Puffer – in diesem Fall kommt die vierfache Anzahl der CPU-Kerne zum Einsatz, weil es um I/O-intensive Operationen geht und weniger gerechnet wird. Ruft das Programm parseDir()
auf, wird ein true
hineingeschrieben. Wenn der Channel aber gefüllt ist und zu viele Go-Routinen aktiv sind, wartet die Routine an dieser Stelle, noch bevor weitere Aktionen folgen oder Ressourcen beansprucht werden. Beim Verlassen der Routine entfernt sie den Wert per defer()
aus dem Channel. So können höchstens 4*numcpu Goroutinen
gleichzeitig aktiv sein. Dieses Vorgehen brachte einen deutlichen Performancezuwachs beim Parsen und sogar beim parallelen Schreiben auf eine SSD-Platte dank intelligentem Controller hervor. Ein elegantes Beispiel zeigt folgendes Listing, das von Eli Bendersky stammt.
func limitNumClients(handle http.HandlerFunc, maxClients int) http.HandlerFunc {
sema := make(chan bool, maxClients)
return func(w http.ResponseWriter, req *http.Request) {
sema <- true
handle(w, req)
<-sema
}
}
// Limit to max 10 connections for this handler.
http.HandleFunc("/inc", limitNumClients(inchandler, 10))
Das Programm zeigt eine Möglichkeit zur eleganten Beschränkung der HTTP-Rufe im Webserver.
Dieses Programm gibt eine unbenannte Funktion zurück, die auf den Channel sema
(der hier wie ein Semaphore agiert) zurückgreifen kann, da er in ihrem Kontext definiert ist. So kann man die gleichzeitige Nutzerzahl einer Webseite einfach beschränken. Channels können ihrerseits Channels oder sogar Funktionen mit einheitlicher Signatur übermitteln. Da Go typsicher ist, stellt dieses Vorgehen aber dank Go-Interfaces kein Problem dar. Programmierer und Programmiererinnen können bei Bedarf variable Aufgaben an eine Menge von "Arbeitsroutinen" (Workern) verteilen. In jedem Fall müssen sie nur einen Ablauf entwerfen und sich nicht mit unübersichtlicher Mutex-Logik plagen.