Auf dem heutigen TechTalk in Köln zum Thema Parallel Computing ist eine rege Diskussion zum Thema Lambdas in C# entstanden. Beim Vorstellen der .NET Klasse Parallel aus dem System.Threading Namespace in .NET 4.0 habe ich neben den einfachen Parallel.Invoke Beispiel auch eine Monte Carlo PI Simulation gezeigt. Dabei wurde die Funktion Parallel.For<T> benutzt.
Mit dieser Funktion lassen sich Schleifen parallelisieren und entsprechend die Ergebnisse der einzelnen Tasks zusammenfassen. Zum Beispiel kann man diese For Schleife
1: int total = 0;
2: for (int i = 0; i < 10; i++)
3: {
4: total += 1;
5: }
folgendermaßen mittels der Klasse Parallel auf mehrere Prozessorkerne verteilen:
1: Parallel.For<int>(0, 10, () => 0, (i, pls, subtotal) =>
2: {
3: subtotal += 1;
4: return subtotal;
5: },
6: (x) => { Interlocked.Add(ref total, x); });
(Hinweis: Das ist nur ein Beispiel und ist den Aufwand nicht wert parallel verarbeitet zu werden!)
Nun ist der parallelisierte Code nicht so leicht lesbar. Was für hitzige Diskussionen sorgte. Ich habe auch ein wenig provoziert und auch darauf hingewiesen das der Code nicht unbedingt gleich zu verstehen ist. Was passiert da eigentlich?
Die Parallel.For<T> Methode so wie ich sie oben benutze bekommt 3 Lambda Ausdrücke mit. Einmal die Initialisierung des einzelnen Threads mittels der Funktion () => 0. Diese Lambda Schreibweise ist die Abkürzung für Action ohne Parameter. Der zweite Lambda Ausdruck hat gleich 3 Parameter, einmal die Laufvariable i, dann eine Variable mit dem Namen pls und schliesslich die letzte Variable subtotal. Interessant ist, das man nicht anhand des Codes erkennen kann was diese einzelnen Variablen darstellen. Vielmehr muss man mittels Intellisense oder Dokumentation nachschauen um welche Typen es sich hier genau handelt. i ist ein int, pls ist ParallelLoopState und subtotal wieder ein int und ein Rückgabewert! Letztlich mapped diese Lambda auf die Deklaration Func<int,ParallelLoopState,out int>. Ist schon gewöhnungsbedürftig. Die dritte Lambda ist schliesslich für die Reduktion zuständig, das Zusammenführen der einzelnen Ausführungspfade zur Gesamtvariable total. Die Variable x an dieser Stelle ist der Rückgabewert der zweiten Lamdba subtotal. Eigentlich recht einfach wenn man weiss wie Paralle.For<T> implementiert ist.
Genau hier liegt das Problem. So mächtig Lambda Ausdrücke auch sein mögen, Sie bergen auch viele Risiken. Wenn Klassen Funktionen implementieren die vor lauter Action und Func Parameter nur so strotzen, so darf man sicher sein dass Lambdas an dieser Stelle eingesetzt werden. Das wiederrum macht den Code nicht gleich intuitiv lesbar. Beschäftigt man sich gerade mit der API so ist es ein leichtes zu verstehen was die Implementierung bedeutet. Aber wie sieht es aus wenn man sich eine Weile nicht mit dieser API beschäftigt und dann nach 2-3 Monaten mal wieder drauf schaut? Ich wette man muss dann kurz inne halten und sich vielleicht sogar die Dokumentation nochmals lesen.
Insofern ist der Einwand der zwei Teilnehmer heute irgendwie schon berechtigt, auf der anderen Seite ist eine anderen Lösung womöglich gar nicht so trivial. Natürlich könnte man das ganze auch in einem Fluent Interface Ansatz als API zur Verfügung stellen, vielleicht so,
1: FluentParallel.For
2: .From(0)
3: .To(10)
4: .ExecuteLoopBody(
5: (subtotal) =>
6: {
7: subtotal += 1; return subtotal;
8: })
9: .Reduce(
10: (subtotal) =>
11: {
12: Interlocked.Add(ref total, subtotal);
13: })
14: .Wait();
doch wirklich etwas gewonnen hat man hier nicht unbedingt. Die API könnte ein paar allgemeine Annahmen einfach implementieren, trotzdem wird beim Einsatz der Lambdas die Problematik mit den Übergabeparametern nicht gelöst. Man erkennt den Typen nach wie vor nicht und bei komplexeren Loop Bodies wäre damit auch dieser Ansatz nicht unbedingt lesbarer.
Lambdas bergen Risiken was die Lesbarkeit des Codes angeht und damit Hand in Hand auch dessen Wartbarkeit. Wie seht Ihr den Einsatz von Lambdas?