Dojos für Entwickler 2. Stefan Lieser

Чтение книги онлайн.

Читать онлайн книгу Dojos für Entwickler 2 - Stefan Lieser страница 9

Автор:
Серия:
Издательство:
Dojos für Entwickler 2 - Stefan Lieser

Скачать книгу

1 zeigt den ersten Fall vor und nach der Parallelisierung.

      Damit die beiden Pfade auf jeweils eigenen Threads parallel ausgeführt werden, muss jeweils zu Beginn und Ende des Pfades eine zusätzliche Funktionseinheit für den Threadwechsel sorgen. Die Funktionseinheit Asynchronize sorgt dafür, dass auf einem neuen Thread weitergearbeitet wird. Am Ende des Teil-Flows sorgt Synchronize dafür, dass der Flow auf den ursprünglichen Thread zurückkehrt. Dies ist vor allem in Verbindung mit Windows Forms und WPF wichtig, weil ein Zugriff auf die Controls meist nur aus dem UI-Thread heraus gestattet ist. Ihre erste Aufgabe für diesen Monat besteht darin, die beiden Funktionseinheiten Asynchronize und Synchronize zu implementieren. Das ist mithilfe der Klassen Thread und SynchronizationContext eine überschaubare Herausforderung.

figure3a

      [Abb. 1]

       Parallele Pfade.

      In Abbildung 2 sehen Sie den zweiten Fall für eine mögliche Parallelisierung.

      Die Funktionseinheit A erhält mehrere Elemente vom Typ X und verarbeitet diese zu mehreren Elementen vom Typ Y. Der Stern zeigt an, dass statt eines einzelnen Datums eine Aufzählung von Daten fließt. Ziel ist es, die Bearbeitung der Aufzählung von mehreren parallel arbeitenden Instanzen der Funktionseinheit A ausführen zu lassen. Dazu verteilt der Scatter-Baustein die anstehenden Daten auf mehrere parallel arbeitende Funktionseinheiten. Die Ergebnisse sammelt Gather am Ende wieder ein und fasst sie zu einer Aufzählung zusammen. Um die Aufgabe nicht zu kompliziert zu machen, genügt es, Scatter und Gather mit einer fixen Anzahl von Ein- bzw. Ausgängen zu versehen. Beginnen Sie ruhig mit nur zwei Ein- bzw. Ausgängen.

      Natürlich müssen die Daten sich für eine Parallelverarbeitung eignen. Müssen die Daten etwa in einer vorgegebenen Reihenfolge bearbeitet werden, wird eine Parallelisierung schwierig. Durch die parallel laufenden Threads ist nämlich die Einhaltung der Reihenfolge nicht mehr automatisch sichergestellt. Müssen die Threads zur Einhaltung der Reihenfolge aufeinander warten, bringt die Parallelisierung aus Performancesicht ohnehin nichts. Und auch bei Daten, deren Bearbeitungsreihenfolge keine Rolle spielt, müssen Sie darauf achten, dass die Daten jeweils genau einmal bearbeitet werden. [ml]

figure3b

      [Abb. 2]

       Aufzählungen parallelisiert bearbeiten.

      Lösung 3

      Parallelisierung im Flow-Design

      Erst trennen, dann vereinen

      Mit Flow-Design lassen sich auch parallele Vorgänge modellieren. Standardbausteine erleichtern die Umsetzung. Diese Übung ergänzt die neuen Bausteine Scatter und Gather.

      Inzwischen sind in der dotnetpro über 20 dojo-Lösungen erschienen. In vielen davon habe ich eine Lösung mit Flow-Design modelliert und umgesetzt. Und in zahlreichen davon stand bereits der Hinweis auf das ebclang-Projekt [1]. Wer wollte, konnte dort eine Lösung für den ersten Teil der Parallelisierungsaufgabe finden: Asynchronizer und Synchronizer sind Standardbausteine, die dort implementiert sind. Aber natürlich haben Sie Ihre übungszeit ernst genommen und beide Bausteine selbst implementiert. Oder?

      Sollten Sie das ebclang-Projekt als „Spickzettel“ benutzt haben, werden Sie gleich merken, dass ich hier die Implementation aus genau diesem Projekt als Lösung der Aufgabe vorstellen werde. Das betrifft allerdings nur den ersten Teil der übung. Der zweite Teil, Scatter/Gather, befindet sich bislang nicht im ebclang-Projekt.

      Async/Sync

      Aufgabe des Asynchronizer-Bausteins ist es, den eingehenden Datenfluss auf einen anderen Thread umzuleiten. Der Asynchronizer hat dazu eine Process-Methode als Eingang und ein Result-Event als Ausgang. Die Kernidee beim Asynchronizer ist: Beim Aufruf der Process-Methode wird der Result-Event ausgelöst, allerdings auf einem anderen Thread. Und natürlich wird der Parameter der Process-Methode an den Result-Event weitergereicht, sodass der Datenfluss durch den Asynchronizer hindurchfließt. Soll ein Baustein den Datenfluss einfach nur durchreichen, sieht die Implementation so wie in Listing 1 aus.

      Listing 1

      Den Datenfluss durchreichen.

      public class Asynchronizer<T> { public void Process(T input) { Result(input); } public event Action<T> Result; }

      Um den Event auf einem anderen Thread auszulösen, muss ein neuer Thread gestartet werden. Das kann dadurch erfolgen, dass Sie eine neue Instanz eines Threads anlegen oder einen freien Thread aus dem ThreadPool verwenden. Dabei kann eine Lambda-Expression übergeben werden, die auf dem neuen Thread ausgeführt wird. Wenn man einen neuen Thread anlegt, muss das Starten des Threads explizit durch Start erfolgen, siehe Listing 2.

      Listing 2

      Den neuen Thread explizit starten.

      public class Asynchronizer<T> { public void Process(T input) { var thread = new Thread(() => Result(input)); thread.Start(); } public event Action<T> Result; }

      Ganz einfach, oder? Die erste Frage lautet nun: Wie kann man den Asynchronizer automatisiert testen? Des Weiteren stellt sich die Frage, ob es eine gute Idee ist, jeweils einen neuen Thread zu erzeugen. Würde der Zugriff auf den ThreadPool Vorteile bringen? Doch zunächst zum Testen.

      Asynchronen Code testen

      Die Herausforderung beim automatisierten Testen von asynchronem Code liegt darin, dass der zu testende Code im Testablauf relativ schnell zum Aufrufer zurückkehrt. Denn schließlich wird die zu erledigende Aufgabe auf einen anderen Thread in den Hintergrund verschoben. Kehrt der zu testende Code jedoch in die Testmethode zurück, wird diese bis zu ihrem Ende ausgeführt und dann beendet. Damit ist dann auch der Test schon zu Ende, bevor es richtig losging. Die Herausforderung besteht also darin, in der Testmethode auf die Ausführung des Hintergrundthreads zu warten. Wie wäre es mit Thread.Sleep()? Listing 3 zeigt ein Beispiel.

      Listing 3

      Nicht optimal: Thread.Sleep.

      [Test] public void Naive_approach_to_async_ tests() { var result = 0; sut.Result += x => result = x; sut.Process(42); Thread.Sleep(500); Assert.That(result, Is.EqualTo(42)); }

      Aber wie lange soll auf den Hintergrundthread gewartet werden? Eine halbe Sekunde, sprich 500 ms wie oben gezeigt? Meist genügt das. Doch wenn der Test auf einem gut ausgelasteten Continuous Integration Server läuft, dann kann die Wartezeit zu gering sein. Also zur Sicherheit fünf Sekunden warten? Nein, natürlich ist das keine gute Idee. Der Test soll natürlich exakt so lange auf den Hintergrundthread warten, bis dieser fertig ist. Das erreichen Sie mit einem WaitHandle, Listing 4 zeigt den entsprechenden Code. Vergessen Sie Thread. Sleep für solche Fälle.

      Listing 4

      Besser : WaitHandle.

      [TestFixture] public class

Скачать книгу