Einstieg in Google Go
Umgebungsvariable Gomaxoprocs über die Anzahl der zu erzeugenden Threads.
Go parallel?
In diesem Abschnitt wenden wir uns den Möglichkeiten zu, die Go zur Parallelisierung bereithält. Dazu vergleichen wir Go mit den verschiedenen Modellen, die einem nativen C/C++-Programmierer zu Verfügung stehen. Als Beispiel dient uns ein naiver Primzahlenalgorithmus, mit dem wir über eine Schleife alle Primzahlen in einem vorgegebenen Intervall von natürlichen Zahlen berechnen (Listing 2.1).
func isPrime(nb uint64) bool {
upperbound := uint64(math.Sqrt(float64(nb))) + 1
for i := uint64(3); i <= upperbound; i += 2 {
if nb%i == 0 {
return false
}
}
return true
}
Listing 2.1
In einer Schleife werden von einem Startwert start bis zu einem Endwert end alle Primzahlen ermittelt und in der globalen Variablen number_of_primes gespeichert. Es geht hier nicht darum, einen besonders cleveren Algorithmus zur Primzahlenbestimmung zu ermitteln, sondern in der Hauptsache, Last für die einzelnen Schleifeniterationen zu erzeugen. Wir beginnen mit start=3 , deshalb fehlt uns z. B. die Zwei als Primzahl. Außerdem muss der Startwert in unserem Beispiel eine ungerade Zahl sein, damit der Algorithmus funktioniert. Für das Intervall im Zahlenraum von 1 bis 100 000 000 (Schreibweise: [1,100000000]) liefert ein serieller Test (C++) eine Laufzeit von 153 952 ms. In dieser Zeit werden 5 761 455 Primzahlen gefunden:
for (i = start; i <= end; i += 2) {
if (is_prime(i)) {
number_of_primes++;
}
}
Die Parallelisierung von Schleifen wird in der Regel über die Verteilung des Iterationsraums auf die verschiedenen Prozessorkerne durch die Laufzeitumgebung realisiert. Das erste Modell zur Parallelisierung, das wir betrachten, verwendet OpenMP. OpenMP definiert, wie eingangs erwähnt, spezielle Compiler-Direktiven, die die zu parallelisierenden Programmteile auf ein Team von Threads verteilen. Schleifen werden in OpenMP einfach dadurch parallelisiert, dass ein Pragma vor die Schleife eingefügt wird. Die OpenMP-Laufzeitumgebung legt fest, wie viele Threads erzeugt (z. B. Anzahl der Prozessorkerne), synchronisiert und nach Beendigung des Konstrukts beendet werden.
#pragma omp parallel for
for (i = start; i <= end; i += 2) {
if (is_prime(i)) {
number_of_primes++;
}
}
Der Einsatz von OpenMP hat zu einer drastischen Reduzierung der Laufzeit (23 297 ms) geführt. Modernere Parallelisierungsansätze wie die Threading Building Blocks (TBB) verwenden im Gegensatz zu OpenMP ein Task-Modell. Während OpenMP noch den Iterationsraum durch die Anzahl der vorhandenen Prozessorkerne teilt und somit die Anzahl der zu erzeugenden Threads ermittelt, versuchen Task-Modelle, die zu verarbeitende Aufgabe in viele kleinere Teilaufgaben, so genannte Tasks , zu zerlegen. Diese Tasks können dann parallel und unabhängig voneinander verarbeitet werden. Dabei wird die zu verteilende Arbeit deutlich feingranularer zerteilt und damit eine bessere Lastverteilung erreicht. Bei Betrachtung der verschiedenen Laufzeiten sieht man den Unterschied: Bei OpenMP bekommt der erste Thread die einfachen Primzahlen, der letzte Thread hingegen den oberen Teil des Iterationsraums und somit die großen Primzahlen zum Test, was natürlich länger dauert. Das Ergebnis ist eine ungleichmäßige Verteilung der Rechenlast. Bei den Task-basierten Ansätzen ist die Verteilung der Last um einiges gleichmäßiger, da der Iterationsraum viel häufiger geteilt wird und so eine gleichmäßige Auslastung des Thread Pools gewährleistet ist. Er soll hier nicht verschwiegen werden, dass es auch bei OpenMP Möglichkeiten gibt, den Iterationsraum geschickter zu verteilen:
// C++ - Intel Threading Building Blocks (TBB)
tbb::atomic number_of_primes;
tbb::parallel_for(start, end, (unsigned long int)2,
[&number_of_primes] (unsigned long int i) {
if (is_prime(i)){
number_of_primes++;
}
});
Schaut man sich aktuellen parallelen C/C++- oder C#-Code an, fällt auf, dass man sich sehr gerne der Lambda-Funktionen bedient, um die Parallelisierung auszudrücken. Da ein Lambda-Ausdruck nichts anderes ist als ein Funktionsobjekt, kann dieser Teil des Algorithmus sehr elegant über eine anonyme Funktion abgebildet werden. Auf diese Weise kann man die parallelen Berechnungen sehr schön kapseln. Während bei C# und Go Lambda-Funktionen aktueller Bestandteil der Sprache sind, müssen C++ Programmierer einen Compiler wählen, der diesen Teil des C++-11-Standards schon unterstützt. Um Datenwettläufe
Weitere Kostenlose Bücher