Einstieg in Google Go
zurückzuliefern, wird das Ergebnis des Tests in einen Channel gesteckt. Um die Primzahlen in [1,100000000] zu zählen, reicht es, die Werte, die aus dem Channel gelesen werden, wie in Listing 2.4 dargestellt aufzusummieren.
func isPrimeChan(nb uint64, c chan<- uint64) {
upperbound := uint64(math.Sqrt(float64(nb))) + 1
for i := uint64(3); i <= upperbound; i += 2 {
if nb%i == 0 {
c <- 0
return
}
}
c <- 1
}
Listing 2.3
func bruteForce(start, end uint64) (n uint64) {
result := make(chan uint64, 100)
go func() {
for i := start; i <= end; i += 2 {
go isPrimeChan(i, result)
}
}
for i := start; i <= end; i += 2 {
n += <-result
}
return
}
Listing 2.4
Diese Variante startet in einer separaten GoRoutine für jede Zahl im Intervall eine weitere GoRoutine . Darauf folgt das eigentliche Aufsummieren der Ergebnisse des Tests. Auch wenn es (mit genügend Speicher) möglich ist, 50 000 000 GoRoutinen zu starten, ist dies dennoch nicht sehr effizient. Eine bessere Variante, ähnlich dem Shared-Memory-Ansatz, kann wie in Listing 2.5 zu sehen implementiert werden.
func smart(start, end uint64) (n uint64) {
numbers := make(chan uint64, GoTasks)
result := make(chan uint64, GoTasks)
done := make(chan bool, 0)
for i := uint64(0); i < GoTasks; i++ {
go func() {
for n := range numbers {
isPrimeChan(n, result)
}
}()
}
go func() {
for i := start; i <= end; i += 2 {
n += <-result
}
done <- true
}()
for i := start; i <= end; i += 2 {
numbers <- i
}
<-done
close(numbers)
close(results)
return
}
Listing 2.5
Hier haben wir uns nun unseren eigenen GoRoutinen -Pool implementiert. In dieser Implementierung benötigen wir dazu drei Channels. In numbers und result werden die zu testenden Zahlen bzw. die Ergebnisse übertragen. Beide Channels sind asynchron mit einem Puffer von GoTasks vielen Elementen. Das bedeutet, dass ein schreibender Zugriff auf den Channel erst blockiert, wenn der Puffer voll ist. Im Gegensatz dazu ist jeder Zugriff auf den Channel done synchron. Über diesen signalisieren wir, dass wir alle Ergebnisse aus result aufsummiert haben und die Funktion verlassen können. In der ersten for -Schleife werden nun GoTask s viele Worker- GoRoutinen gestartet. Innerhalb der anonymen Funktion liest jede GoRoutine aus dem Channel numbers eine Zahl und testet sie. Nachdem alle Worker gestartet wurden, wird die GoRoutine gestartet, die alle Ergebnisse aufsummiert. Wenn das Aufsummieren beendet wurde, signalisiert die GoRoutine , dass sie fertig ist, indem sie den Wert „true“ in den done- Channel steckt. Nun ist soweit alles initialisiert. Die GoRoutinen lauschen auf die Kanäle numbers und results und warten darauf, dass diese etwas übertragen bekommen. Das geschieht dann auch tatsächlich in der letzten for -Schleife: Dort werden alle zu testenden Zahlen in den numbers- Channel gesteckt und somit die Berechnungen in Gang gesetzt. Wenn wir damit fertig sind, warten wir noch darauf, dass das Aufsummieren beendet worden ist, indem wir auf den done -Channel lauschen. Der Wert, der übertragen wird, interessiert uns eigentlich gar nicht, denn der Kanal dient lediglich der Synchronisierung. Danach werden alle Kanäle geschlossen und somit auch die Worker- GoRoutinen implizit beendet. Wenn man diese Implementierung mit den anderen vergleicht, wird man feststellen, dass die Performance deutlich geringer ist als in all den anderen Varianten. Das liegt daran, dass die Channels, auch wenn man mit ihnen elegant programmieren kann, einen gewissen Overhead mit sich bringen, der sich umso stärker zeigt, je kürzer die Berechnung und je größer die Synchronisation ist. Dies soll den Leser keines Falls entmutigen, Channels zu verwenden. Im Gegenteil: Wir haben bei der vorliegenden idiomatischen Lösung sogar eine optimale Auslastung der Worker- GoRoutinen erreicht, da jede GoRoutine einen neuen Test durchführt, sobald eine neue Zahl vorhanden ist. Außerdem entledigt man sich aller Probleme, die beim Zugriff auf gemeinsam genutzte Daten entstehen (z. B. Datenwettläufe).
Unfaire Scheduler
Schaut man sich die Ergebnisse auf einem aktuellen Zwölfkern-System an, fällt auf, dass die Task/ GoRoutinen -basierten Modelle deutlich performanter sind.
Task/GoRoutinen
Zeitaufwand (ms)
Running serial version of primes demo
153952
Running Go channel version of primes demo
50723
Running OpenMP version of primes demo
23297
Running parGO version of primes demo
19920
Running TBB version of primes demo
19766
Tabelle 2.1: Testergebnisse von Task-/Goroutinen
Weitere Kostenlose Bücher