Dojos für Entwickler 2. Stefan Lieser
Чтение книги онлайн.
Читать онлайн книгу Dojos für Entwickler 2 - Stefan Lieser страница 10
Das WaitHandle wird verwendet, um nach Aufruf der Process-Methode darauf zu warten, dass der Result-Event auf dem neu gestarteten Thread seine Arbeit verrichtet hat. Das WaitHandle befindet sich anfangs im Zustand „nicht gesetzt“. Wird waitHandle.WaitOne aufgerufen, wird der aktuelle Thread so lange angehalten, bis das WaitHandle gesetzt ist. Daher muss das WaitHandle im Result-Event mit Set gesetzt werden. Auf diese Weise wacht der Thread, auf dem der Test läuft, dann wieder auf und kann sein Assert ausführen.
Damit der Test bei einem Fehler nicht unendlich lange wartet, habe ich bei WaitOne einen Timeout von 500 ms definiert. Dadurch wird das WaitHandle entweder durch Set innerhalb von 500 ms gesetzt oder durch einen Timeout. Der Timeout tritt automatisch ein, wenn das WaitHandle nicht nach der hinterlegten Zeit mit Set gesetzt wird, in diesem Fall nach 500 ms. Der Timeout wird dadurch signalisiert, dass WaitOne false zurückliefert.
Um den Timeout im Test zu erkennen, habe ich ein Assert ergänzt, welches prüft, ob WaitOne mit true beendet wurde. Wenn das der Fall ist, ist das WaitHandle ordnungsgemäß mit Set gesetzt worden, sprich, der Result-Event wurde ausgelöst. Liefert WaitOne jedoch false zurück, ist im Test etwas schiefgelaufen, weil das WaitHandle durch einen Timeout ausgelöst wurde.
Hier ist es nun tatsächlich angemessen, darüber nachzudenken, ob 500 ms als Timeout ausreichend sind. Auf einem Continuous Integration Server kann das schon mal knapp bemessen sein. Da die Timeout-Zeitspanne nur dann in Anspruch genommen wird, wenn etwas schiefläuft, können Sie den Timeout nun ohne Not auf mehrere Sekunden hochsetzen. Solange alles in Ordnung ist, und das sollte der Normalfall sein, läuft der Test dann durch, so schnell es eben geht. Nur für den hoffentlich seltenen Fall, dass jemand den Code „kaputt gemacht hat“, tritt die Timeout-Wartezeit in Kraft.
ThreadPool
Alternativ zum Erzeugen eines neuen Threads kann auch ein Thread aus dem ThreadPool verwendet werden. Dies hat Vorteile bei kurzlaufenden Operationen, da der ThreadPool weniger Overhead benötigt. Die Threads sind bereits erzeugt und warten nur auf einen neuen Auftrag.
Der wichtigste Unterschied liegt jedoch darin, dass Threads aus dem ThreadPool als Hintergrundthreads laufen, während neu erzeugte Threads standardmäßig Vordergrundthreads sind. Hintergrundthreads werden automatisch beendet, sobald alle Vordergrundthreads beendet sind. In der Regel hat ein Programm lediglich einen einzigen Vordergrundthread, der die UI übernimmt. Beendet man das Programm, werden automatisch alle Hintergrundthreads mitbeendet. Die selbst erzeugten Vordergrundthreads laufen jedoch weiter, sodass das Programm in diesem Fall nicht einfach dadurch beendet wird, dass das UI geschlossen wird.
Man kann natürlich auch selbst erzeugte Threads als Hintergrundthread starten, indem vor dem Start die Eigenschaft IsBackground auf true gesetzt wird. Was man im Einzelfall benötigt, hängt vom konkreten Anwendungsfall ab.
Und wieder synchron
Mithilfe des Asynchronizer-Bausteins kann die Ausführung des im Fluss nachfolgenden Bausteins auf einen anderen Thread verlagert werden. Doch was tun, wenn die Ergebnisse aus dem Hintergrundthread in das UI übernommen werden sollen? Dann ergibt sich bei Windows Forms, WPF und Silverlight das Problem, dass die Controls nur aus dem UI-Thread heraus angesprochen werden dürfen. Die Windows-Forms-Controls haben dafür einen Mechanismus: InvokeRequired und Invoke, siehe Listing 5.
Listing 5
InvokeRequired nutzen.
if (listBox1.InvokeRequired) { listBox1.Invoke(new MethodInvoker(() => listBox1.Items.Add(t))); } else { listBox1.Items.Add(t); }
Bevor in diesem Beispiel ein weiterer Eintrag in die ListBox eingefügt wird, wird durch Abfrage von InvokeRequired geprüft, ob der Aufruf mit Invoke erfolgen muss. Ist das der Fall, wird dem Invoke-Aufruf eine Lambda-Expression übergeben, welche den Eintrag in die ListBox schreibt. Invoke sorgt dafür, dass die Lambda-Expression auf dem UI-Thread ausgeführt wird.
Das kann man so machen, doch man sollte es nicht so machen. Denn auf diese Weise wird der UI-Code mit Aspekten der Parallelisierung kontaminiert. Außerdem müsste ein UI, das zunächst synchron und erst später asynchron verwendet wird, modifiziert werden. Das ist unschön. Zum einen verstößt diese Vorgehensweise gegen das Open Closed Principle (OCP), welches besagt, dass eine Klasse offen für Erweiterungen, aber geschlossen gegenüber Modifikationen sein sollte. Noch schwerer wiegt das Argument, dass auf diese Weise das Prinzip die Separation of Concerns (SoC) verletzt würde. Der Aspekt der Parallelisierung würde mit der eigentlichen Aufgabe des UIs vermischt.
Abhilfe schafft die Verwendung des SynchronizationContext bei Windows Forms bzw. des Dispatchers bei WPF und Silverlight. Beides sind Infrastrukturelemente, mit denen die Synchronisation auf einen Zielthread erfolgen kann. Nutzt man diese Infrastruktur, kann man auf einfache Weise einen Synchronizer-Baustein realisieren, der den Result-Event auf dem gewünschten Zielthread ausführt.
Der Synchronizer erhält dieselbe Schnittstelle wie der Asynchronizer: eine Process- Methode als Eingang sowie einen Result- Event als Ausgang. Auch hier sorgt der Baustein wieder dafür, dass der Parameter durchgereicht wird, der Datenfluss also einfach hindurchfließt. Damit der Baustein im Sinne eines Standardbausteins vielseitig einsetzbar ist, erhält er einen generischen Typparameter, der den Datentyp von Ein- und Ausgang bestimmt. Hier zahlt es sich aus, dass beim Flow-Design immer nur maximal ein Parameter verwendet wird. Auf diese Weise ist es nämlich leicht möglich, Standardbausteine zu erstellen und Flows neu zusammenzustöpseln. Zwar könnte man auch Varianten der Standardbausteine anlegen, die mehrere Parameter unterstützen, aber dadurch würde die ganze Sache doch etwas komplizierter.
Listing 6 zeigt die Implementation des Synchronizers für Windows Forms.
Listing 6
Synchronizer für Windows Forms.
public class Synchronizer<T> { private readonly SynchronizationContext synchronizationContext; public Synchronizer() { synchronizationContext = SynchronizationContext.Current ?? new SynchronizationContext(); } public void Process(T input) { synchronizationContext.Send(_ => Result(input), null); } public event Action<T>Result; }
Der SynchronizationContext wird im Konstruktor des Bausteins ermittelt und in einem Feld des Synchronizers abgelegt. Auf diese Weise steht er in der Process-Methode zur Verfügung, um mittels Send eine Lambda-Expression an den Zielthread zu übergeben. Die Lambda-Expression ruft wieder den Result-Event auf, wie das beim Asynchronizer schon der Fall war. Damit der Synchronizer funktioniert, muss sein Konstruktor auf dem Thread aufgerufen werden, auf den später synchronisiert werden soll. Normalerweise ist das der Hauptthread, auf dem das Programm initialisiert wird. Aber Obacht!
Der SynchronizationContext funktioniert nicht mit beliebigen Threads. Windows Forms treibt hinter den Kulissen etwas Magie. Es realisiert nämlich eine Message-Loop, die dafür sorgt, dass Aufrufe an den UI-Thread übergeben werden können. Zwischen beliebigen Threads zu synchronisieren