Konzepte statt Hypes

Seite 2: Parallelisieren über Abstraktion

Inhaltsverzeichnis

Abhilfe würde ein höheres Abstraktionsniveau schaffen, das das Durchlaufen der Schleife vom eigentlichen Schleifenrumpf trennt. Da sich eine solche Abstraktion durch das Einführen einer Funktion umsetzen lässt, ist ihr jedoch der Schleifenrumpf als auszuführender Code zu übergeben. Es ist daher erforderlich, Funktionen als Parameter an andere Funktionen übergeben zu können.

Vergleicht man das vorige Beispiel mit dem folgenden Code, fällt auf, dass er nicht nur weitaus kürzer ist, sondern auch aussagekräftiger, da auf den ersten Blick klar ist, dass die map-Funktion einzelne Zahlen auf andere Zahlen abbildet:

const getSquares = function (numbers) {
return numbers.map(n => n ** 2);
};

Damit das funktioniert, müssen Funktionen "Bürger erster Klasse" in der gewählten Programmiersprache sein. Das Besondere an der im obigen Codeauszug eingeführten map-Funktion ist übrigens nicht nur, dass sie ein höheres Abstraktionsniveau bietet, sondern dass sie keine explizite Implementierung mehr ausdrückt: Ihre Verwendung gibt lediglich an, was zu erreichen ist, aber nicht wie. Daher obliegt es nun der Laufzeitumgebung, die map-Funktion gegebenenfalls zu parallelisieren. Der US-amerikanische Unternehmer Joel Spolsky hat das Beispiel in seinem äußerst lesenswerten Blogeintrag "Can Your Programming Language Do This?" aufgegriffen.

Will man herausfinden, warum sich die map-Funktion im Gegensatz zur for-Schleife problemlos parallelisieren lässt, ist das höhere Abstraktionsniveau nur ein Teil der Antwort. Der wichtigste Aspekt ist, dass map im Gegensatz zu der explizit ausformulierten Schleife keine Seiteneffekte hat. Als Seiteneffekt bezeichnet man beim Programmieren die Veränderung des Zustands einer Anwendung.

Zusammenfassend lässt sich festhalten, dass Code immer dann gut parallelisiert werden kann, wenn er keine Seiteneffekte aufweist. Das lässt sich an einem weiteren, sehr einfachen Beispiel zeigen. Gegeben sei der folgende Ausdruck:

const x = (23 * 5) + (42 * 7);

Offensichtlich haben die beiden geklammerten Terme weder Seiteneffekte noch Abhängigkeiten voneinander. Daher ist es problemlos möglich, sie unabhängig voneinander zu berechnen. Erst das Bilden der Summe verbindet die beiden Terme, weshalb dafür die Evaluation der beiden Einzelteile abgeschlossen sein muss.

Gelingt es, Code ausschließlich aus Ausdrücken aufzubauen, die keinerlei Seiteneffekte aufweisen, lässt er sich einfach parallelisieren. Dem im Weg stehen leider sämtliche Anweisungen einer Programmiersprache, deren Ziel ein Seiteneffekt ist: seien es Variablenzuweisungen oder Bildschirmausgaben.

Nimmt man das alles zusammen, lässt sich neben der fehlenden Wiederverwendbarkeit noch ein zweites Problem der Objektorientierung erkennen. Kommen Objekte aus mehreren Threads parallel zum Einsatz, kann es zu gleichzeitigen Lese- und Schreibzugriffen kommen. Das wirft eine Reihe von Synchronisationsproblemen auf. Interessanterweise verfügen viele objektorientierte Programmiersprachen über entsprechende Schlüsselwörter, um den Entwickler bei der Synchronisation zu unterstützen. So kennt Java beispielsweise synchronized, C# hingegen lock und volatile.

Die eigentliche Lösung des Problems wäre es, keine Zustandsänderungen an Objekten zuzulassen. Dazu wäre es erforderlich, jede Methode so zu schreiben, dass sie eine neue Instanz zurückgibt statt die bestehende zu verändern. Das ist möglich, in den meisten Sprachen aber aufwändig. Solche unveränderlichen Objekte bezeichnet man als "immutable". Das folgende Codebeispiel zeigt, wie sich ein Objekt zum Abbilden von Wahrscheinlichkeiten implementieren lässt, ohne dass Seiteneffekte auftreten:

const Probability = function (probability) {
if (probability < 0) {
throw new Error('Invalid probability.');
}
if (probability > 1) {
throw new Error('Invalid probability.');
}

this.probability = probability;
};

Probability.prototype.equals = function (other) {
const epsilon = 0.01;

return Math.abs(this.probability - other.probability) < epsilon;
};

Probability.prototype.inverse = function () {
return new Probability(1 - this.probability);
};

Probability.prototype.combined = function (other) {
return new Probability(this.probability * other.probability);
};

Probability.prototype.either = function (other) {
return new Probability(
(this.probability + other.probability) -
(this.probability * other.probability)
);
};