Einstieg in Google Go
kümmern wir uns in unserem Beispiel nicht. Dieses Phänomen ist ausführlich in Artikeln des Entwickler Magazins [1], [2] behandelt worden oder in der einschlägigen Literatur nachlesbar. Wir stellen lediglich fest, dass der parallele Zugriff auf number_of_primes durch ein Synchronisationskonstrukt geschützt werden muss. Zu diesem Zweck stellen die TBB den leichtgewichtigen, generischen und atomaren Datentyp tbb::atomic<> zur Verfügung.
Ein paralleles For für Go
Wie wird eine solche Schleifenparallelität in Go realisiert? Go stellt mit den GoRoutinen ein flexibles Konzept zu Modellierung von Nebenläufigkeit zur Verfügung. Ein paralleles For -Konstrukt, wie in OpenMP oder TBB, sucht man allerdings vergeblich. Sucht man zu diesem Thema in der sehr lebendigen Go-Mailingliste (GoNuts), stößt man recht häufig auf den Hinweis, dass man in Go solch ein Konstrukt nicht benötigt, da man dieses Verhalten recht einfach mit GoRoutinen selbst implementieren kann. Machen wir uns also daran, unser eigenes paralleles For -Konstrukt zu schreiben. Zuerst brauchen wir eine geeignete Möglichkeit, den Iterationsraum in möglichst feingranulare Abschnitte zu teilen. Da GoRoutinen ähnlich leichtgewichtig sind wie Tasks und von der Go-Runtime effektiv auf einen Thread Pool verteilt werden, brauchen wir, wie bei den TBB, eine deutlich feinere Aufteilung als die Anzahl der Prozessorkerne liefert. Wie viele GoRoutinen wir benötigen, lassen wir noch offen. Es soll lediglich eine gerade positive Zahl sein. Wir definieren die Anzahl der verwendeten Tasks als Produkt aus: Anzahl der Prozessorkerne multipliziert mit einem Granularitätsfaktor:
const (
Cores = uint64(4)
GrainSize = uint64(10)
GoTasks = Cores * GrainSize
)
Danach erstellen wir eine Funktion, die als Parameter eine zu berechnende Funktion und eine definierte Schrittweite bekommt. Damit wird unser Go For etwas flexibler. Jetzt brauchen wir noch eine Iterationsfunktion für die invarianten Abschnitte des Berechnungsbereichs. Den Wert für die Anzahl der Primzahlen schützen wir mit einem Lock aus dem Paket sync (Listing 2.2).
DoSection := func(chkstart uint64, chkend uint64) {
for i := chkstart; i < chkend; i += 2 {
if isPrime(i) {
mu.Lock()
number_of_primes = number_of_primes + 1
mu.Unlock()
}
}
wg.Done()
}
Listing 2.2
Das einzige unbekannte Konstrukt ist das wg.Done() . Es bezeichnet eine so genannte WaitGroup . Es ist ähnlich einem Semaphor, also einem Zähler, der bei Erzeugung wg.Add() inkrementiert und bei Beendigung wg.Done() dekrementiert. Es ist somit ein leichtgewichtiges Synchronisationskonstrukt, um sicherzustellen, dass wir auch warten – wg.Wait() – bis die letzte GoRoutine mit ihrer Arbeit fertig ist: wg.Done() . Was noch fehlt ist unser GoRoutinen -Generator, um die nebenläufigen GoRoutinen zu erzeugen. Wir erzeugen so viel Bereiche (engl. Sections), wie wir an GoTasks definiert haben, wobei num die Gesamtzahl der durchzuführenden Iterationen ist.
wg . Add(int(GoTasks))
for section := uint64(0); section < GoTasks ; section ++ {
go doChunk((section*(num+1)/GoTasks)+1,
((section + 1)*(num)/GoTasks)+1)
}
wg . Wait()
Damit ist unsere Implementierung eines einfachen parallelen Schleifenkonstrukts fertig. Was die Laufzeit betrifft, so braucht sie den Vergleich mit den hochoptimierten Task-Schedulern von Go, PPL und TBB nicht zu scheuen. Mit 40 GoRoutinen spielen wir bereits in derselben Liga (siehe Ergebnisse) und lassen ohne zusätzliche Lastverteilungsoptimierung OpenMP weit hinter uns. Mit den GoRoutinen hat man also ein sehr einfach zu bedienendes Konstrukt zur Hand, mit dem man flexibel parallel Modelle erstellen kann, wie wir es hier für ein einfaches Go For zeigen konnten. Allerdings muss man den Granularitätsfaktor gelegentlich anpassen (z. B. mehr Kerne, andere Lastverteilung).
Idiomatischer Ansatz mit Channels
Mit der Implementierung von Go For im vorigen Abschnitt haben wir ein Beispiel gesehen, wie sich mit dem herkömmlichen „gemeinsamen Speicheransatz“ in Go arbeiten lässt. Wir haben diese Variante gezeigt, um Vergleichbarkeit zwischen Go, OpenMP und TBB zu gewährleisten. Wie aber bereits erwähnt, werden in idiomatischem Go anstatt des gemeinsamen Speichers Channels zur Kommunikation zwischen GoRoutinen verwendet. Aus diesem Grund soll an dieser Stelle der Primzahlenzähler aus dem vorigen Abschnitt mithilfe von Channels implementiert werden. Der Test selbst ist dem obigen sehr ähnlich (Listing 2.3). Anstatt nun den Wert
Weitere Kostenlose Bücher