Windows 8, HTML5 und die Folgen für .NET?

Seite 3: Skalierbarkeit

Inhaltsverzeichnis

Wesentlich interessanter als die Performance ist jedoch die Skalierbarkeit. Klassische Desktop-Anwendungen müssen in der Regel lediglich mit einigen hundert oder eventuell tausend zeitgleich agierenden Benutzern umgehen können – für Web- und mobile Anwendungen ist jedoch schnell die Größenordnung von 10.000 und mehr Benutzern erreicht. Wie lässt sich also eine Skalierbarkeit dafür bewerkstelligen?

Zunächst gilt es, einige Ansätze zu betrachten, die sich vorgefertigt einsetzen lassen: Beispielsweise ist der Einsatz einer horizontal ausgezeichnet skalierbaren NoSQL-Datenbank denkbar, ebenso können die Architekturansätze CQRS (Command and Query Responsibility Segregation) und Eventsourcing wesentlich zur Steigerung der Leistungsfähigkeit einer Anwendung beitragen. Doch wie steht es um den eigentlichen Code? Welche Werkzeuge stehen hierfür bereit?

In den vergangenen Jahren und Jahrzehnten konnte man sich als Entwickler in der Regel darauf verlassen, dass eine neue Prozessorgeneration die Skalierbarkeit automatisch steigern würde, schließlich bedeutete eine neue Generation zugleich mehr Leistung. Doch die Grenzen des reinen Geschwindigkeitszuwachses auf Basis eines neuen Chips sind erreicht: Seit Jahren stagniert die Taktfrequenz von Prozessoren zwischen drei und vier GHz. Statt der Geschwindigkeit eines einzelnen Chips nimmt stattdessen deren Anzahl zu: Dualcore- oder auch Quadcore-Prozessoren sind heutzutage gängig. Das Motto lautet wie bei den NoSQL-Datenbanken horizontale statt vertikaler Skalierung.

Das Problem dabei ist, dass Anwendungen nicht mehr automatisch von einer neuen Prozessorgeneration profitieren. Schließlich bleibt die Geschwindigkeit pro Kern die gleiche, lediglich die Anzahl der Kerne hat sich erhöht. Weiß eine Anwendung das jedoch nicht zu nutzen, erfährt sie keinerlei Geschwindigkeitszuwachs.

Der Schlüssel zu künftiger Skalierbarkeit liegt deshalb in der Parallelisierung von Code. Microsoft hat im Rahmen von .NET 4.0 mit der Task Parallel Library (TPL) hierfür einen ersten Ansatz geboten. Dennoch ist das Schreiben asynchronen Codes nach wie vor Expertensache: Zu komplex und fehleranfällig ist die Entwicklung von Code, der mit mehreren Threads umgehen kann.

Doch C# 5.0 scheint hierfür Abhilfe zu schaffen: Ausgehend von der derzeit verfügbaren Async CTP ist naheliegend, dass die dort enthaltenen Erweiterungen auf Basis der Schlüsselwörter async und await Einzug in die nächste Version von C# halten werden.

Aus dem synchronen Code

public int SumPageSizes(IList<Uri> uris) {
int total = 0;
foreach(var uri in uris) {
this.StatusText.Text = total.ToString();
var data = new WebClient().DownloadData(uri);
total += data.Length;
}
this.StatusText.Text = total.ToString();
return total;
}

wird mit diesen Erweiterungen nur unwesentlich komplexerer Code:

public async Task<int> SumPageSizesAsync(IList<Uri> uris) {
int total = 0;
foreach(var uri in uris) {
this.StatusText.Text = total.ToString();
var data = await new WebClient().DownloadDataAsync(uri);
total += data.Length;
}
this.StatusText.Text = total.ToString();
return total;
}

Der einzige Unterschied liegt letztlich in der Verwendung der Schlüsselwörter async und await, und in der Änderung des Rückgabetyps von int zu Task<int>. Im Vergleich zur heutigen in C# 4.0 entwickelten asynchronen Lösung wird der Code damit nicht nur lesbarer, sondern vor allem auch wesentlich einfacher zu schreiben:

public void SumPageSizesAsync(IList<Uri> uris) {
this.SumPageSizesAsyncHelper(uris.GetEnumerator(), 0);
}

private void SumPageSizesAsyncHelper(
IEnumerator<Uri> enumerator, int total) {
if(enumerator.MoveNext()) {
this.StatusText.Text = total.ToString();
var client = new WebClient();
client.DownloadDataCompleted += (sender, e) => {
this.SumPageSizesAsyncHelper(
enumerator, total + e.Result.Length);
};
client.DownloadDataAsync(enumerator.Current);
} else {
this.StatusText.Text = total.ToString();
enumerator.Dispose();
}
}

Zweifelsohne gehen die Erweiterungen von C# in die richtige Richtung. Die Frage lautet jedoch, ob die Anpassungen genügen oder ob für das einfache Entwickeln parallelen Codes nicht ein Paradigmenwechsel erforderlich oder zumindest wünschenswert wäre. Ein Grundübel im Hinblick auf Parallelisierung bleibt nämlich auch mit C# 5.0: die Tatsache, dass Objekte über einen eigenen Status verfügen. Was im Rahmen serviceorientierter Architekturen für zeitgleiche und nebenläufige Zugriffe auf Services propagiert wird, nämlich Statuslosigkeit, hält in kleinerem Maßstab nun Einzug in die alltägliche Programmierung.

Typische Übel parallelisierten Codes liegen in Deadlocks und Data Races. Deren Ursachen sind jedoch häufig zeitgleiche schreibende Zugriffe auf ein statusbehaftetes Objekt. Wird nur lesend auf Objekte zugegriffen, entfallen schlagartig die meisten der üblichen Probleme mit parallelisiertem Code. Deswegen sind funktionale Sprache wie F#, bei denen jedes Objekt per se unveränderlich ist, gut für Parallelprogrammierung geeignet.

Die Frage lautet also, ob die objektorientierte Programmierung zukünftig noch den gleichen Umfang einnehmen wird wie heute. Die Zeichen dafür stehen schlecht.