ErfinderDesRades
Neulich stieß ich auf eine erstaunliche Möglichkeit, innerhalb derselben Methode nach Belieben umzuschalten, zw. Gui-Thread und Worker-Thread. Damit hat man den glaub einfachsten Lösungsansatz der viel gefragten
[FAQ] Warum blockiert mein GUI? , und der auf dem Fuße folgenden
[FAQ] Controls von Thread aktualisieren lassen (Control.Invoke)
Statt also für Zwischenmeldungen ans Gui, und für die Abschlußmeldung je spezifische Methoden zu implementieren, kann man die im WorkerThread laufende Methode einfach zeitweilig auf "GuiThread" umschalten, Meldungen absetzen, und fortfahren. Insbesondere der Transfer von Variablen von einem Thread in den anderen vereinfacht sich sehr (nämlich indem er entfällt
) da ja alles in derselben Methode stattfindet.
Der Trick besteht darin, einen Iterator-Block zu schreiben.
Ein Iterator-Block ist eine Methode, die als IEnumerable deklariert ist, und mittels yield return imstande, mehrere (!) Werte nacheinander (!) an ihren Aufrufer zu liefern.
Im einzelnen läuft im Iterator-Block dabei der Code immer von einem yield return Statement zum nächsten, yieldet den Wert, und verharrt an dieser Stelle, bis die aufrufende Schleife den nächsten yield-Wert anfordert.
(An dieser Stelle hätte ich empfohlen, sich in der MSDN schlau zumachen - Stichwort "Iterator". Leider fehlt in der Express-Edition der (gute) Beitrag.)
Eine "YieldThreading"-Methode jedenfalls ist ein solcher Iterator-Block, und er kann ExecuteIn.WorkerThread und ExecuteIn.GuiThread yielden, beliebig viele, und zwar nacheinander, wie gesagt.
(ExecuteIn ist eine hierfür geschaffene Enumeration).
Die YieldThreading-Methode wird nun von aussen enumeriert, von einer Methode, die im WorkerThread läuft.
Yieldet sie nun ExecuteIn.GuiThread an den Aufrufer, so sendet dieser das Anfordern des nächsten yield-Wertes über einen SynchronisationContext an den GuiThread (statt den nächsten Wert direkt anzufordern).
Das bewirkt, daß nun auch der Code im GuiThread läuft, mit dem die YieldThreading-Methode zum nächsten yield return Statement eilt.
Hmm - ob einer diese Erklärung versteht?
Inne Praxis jedenfalls bewirkt
daß der darauf folgende Code im GuiThread ausgeführt wird
Und mit
kann man in den WorkerThread zurück-switchen.
Ein Beispiel mit etwas gehobenen Ansprüchen
Gesetzt seien 2 zeitaufwändige-Lade-Vorgänge, nämlich die Personen-Namen, und die Personen-Geburtsdaten. Die Ladevorgänge können wahlweise einzeln gestartet werden, aber auch zusammen, in einem gemeinsamen Worker-Job.
Jeder Ladevorgang umfasst
Ein Minuspunkt:
Das Yield-Threading verwendet SynchronisationContext.Send() statt .Post(). ( entspricht Control.Invoke() statt .BeginInvoke()).
Der Nachteil besteht darin, daß .Send() / .Invoke() auf die Fertig-Ausführung im Gui-Thread warten. Der WorkerThread bremst sich also selbst ein bischen aus.
Und das ist leider nicht zu umgehen:
Würde der WorkerThread nicht warten, so würde er dem Gui-Thread den für diesen bestimmten yield return quasi "wegschnappen" - die bekannte CrossthreadCall-Exception wäre direkte Folge von das.
.
Aber man betrachte die Relationen: Eine Meldung ans Gui sollte eh schnell vonstatten gehen, und man sollte die Threads auch nicht häufiger als ca. 3 mal pro Sekunde wechseln, weil
Threading wird dadurch nicht automatisch zum Kinderspiel.
Z.B. besteht die Gefahr, daß yield returns vergessen werden, zu setzen, oder den ungeeigneten Thread angeben. Oder daß beim Umherschieben von Zeilen Threading-Code in Gui-Code-Bereiche gerät.
Besonders bedenkenswert scheint mir, daß ein yield return versehentlich übersprungen wird, wenn aus einer Block-Struktur, etwa mit break; heraussgesprungen wird.
Nichtmal mit einem try{ }finally{ }-Konstrukt ist diesen Problemen zu begegnen, denn yield return darf nicht in finally-Blöcken stehen.
Auch kann man nicht innerhalb von weiter-verzweigten Methoden den Thread umschalten, jedenfalls nicht direkt.
In dem Fall muß man die Unterfunktion auch als Yield-Thread-Methode anlegen, und wie im Beispiel als Optimierung gezeigt, der Yield-Threading-Extension übergeben.
Danksagung:
Diese fabelhafte Idee ist leider nicht von mir. Ich habe sie aus dem
Material zum Kurzvortrag am 17.2.09, den Ralf Hoffmann gehalten hat, der Erfinder dieses Ansatzes.
[FAQ] Warum blockiert mein GUI? , und der auf dem Fuße folgenden
[FAQ] Controls von Thread aktualisieren lassen (Control.Invoke) Statt also für Zwischenmeldungen ans Gui, und für die Abschlußmeldung je spezifische Methoden zu implementieren, kann man die im WorkerThread laufende Methode einfach zeitweilig auf "GuiThread" umschalten, Meldungen absetzen, und fortfahren. Insbesondere der Transfer von Variablen von einem Thread in den anderen vereinfacht sich sehr (nämlich indem er entfällt
) da ja alles in derselben Methode stattfindet.Der Trick besteht darin, einen Iterator-Block zu schreiben.
Ein Iterator-Block ist eine Methode, die als IEnumerable deklariert ist, und mittels yield return imstande, mehrere (!) Werte nacheinander (!) an ihren Aufrufer zu liefern.
Im einzelnen läuft im Iterator-Block dabei der Code immer von einem yield return Statement zum nächsten, yieldet den Wert, und verharrt an dieser Stelle, bis die aufrufende Schleife den nächsten yield-Wert anfordert.
(An dieser Stelle hätte ich empfohlen, sich in der MSDN schlau zumachen - Stichwort "Iterator". Leider fehlt in der Express-Edition der (gute) Beitrag.)
Eine "YieldThreading"-Methode jedenfalls ist ein solcher Iterator-Block, und er kann ExecuteIn.WorkerThread und ExecuteIn.GuiThread yielden, beliebig viele, und zwar nacheinander, wie gesagt.
(ExecuteIn ist eine hierfür geschaffene Enumeration).
Die YieldThreading-Methode wird nun von aussen enumeriert, von einer Methode, die im WorkerThread läuft.
Yieldet sie nun ExecuteIn.GuiThread an den Aufrufer, so sendet dieser das Anfordern des nächsten yield-Wertes über einen SynchronisationContext an den GuiThread (statt den nächsten Wert direkt anzufordern).
Das bewirkt, daß nun auch der Code im GuiThread läuft, mit dem die YieldThreading-Methode zum nächsten yield return Statement eilt.
Hmm - ob einer diese Erklärung versteht?
Inne Praxis jedenfalls bewirkt
C#-Code: |
yield return ExecuteIn.GuiThread
|
daß der darauf folgende Code im GuiThread ausgeführt wird
Und mit
C#-Code: |
yield return ExecuteIn.WorkerThread
|
kann man in den WorkerThread zurück-switchen.
Ein Beispiel mit etwas gehobenen Ansprüchen
Gesetzt seien 2 zeitaufwändige-Lade-Vorgänge, nämlich die Personen-Namen, und die Personen-Geburtsdaten. Die Ladevorgänge können wahlweise einzeln gestartet werden, aber auch zusammen, in einem gemeinsamen Worker-Job.
Jeder Ladevorgang umfasst
- [GuiThread] ---- Löschung der alten Daten, dann Status-Meldung "Beginne Laden xy", dann Rücksetzen und Anzeigen der Progressbar.
- [WorkerThread] Laden der Daten.
- [GuiThread] ---- Dazwischen eingestreut Meldungen über den Fortschritt des Vorgangs
- [WorkerThread] Weiter-Laden der Daten
- [GuiThread] ---- Zum Abschluß die Progressbar wieder verstecken, und die Status-Meldung "Ready" ausgeben.
C#-Code: |
using System;
|
Ein Minuspunkt:
Das Yield-Threading verwendet SynchronisationContext.Send() statt .Post(). ( entspricht Control.Invoke() statt .BeginInvoke()).
Der Nachteil besteht darin, daß .Send() / .Invoke() auf die Fertig-Ausführung im Gui-Thread warten. Der WorkerThread bremst sich also selbst ein bischen aus.
Und das ist leider nicht zu umgehen:
Würde der WorkerThread nicht warten, so würde er dem Gui-Thread den für diesen bestimmten yield return quasi "wegschnappen" - die bekannte CrossthreadCall-Exception wäre direkte Folge von das.
.Aber man betrachte die Relationen: Eine Meldung ans Gui sollte eh schnell vonstatten gehen, und man sollte die Threads auch nicht häufiger als ca. 3 mal pro Sekunde wechseln, weil
- so schnell kann keiner gucken
- ein Thread-Wechsel ist in jedem Fall "teuer"
Threading wird dadurch nicht automatisch zum Kinderspiel.
Z.B. besteht die Gefahr, daß yield returns vergessen werden, zu setzen, oder den ungeeigneten Thread angeben. Oder daß beim Umherschieben von Zeilen Threading-Code in Gui-Code-Bereiche gerät.
Besonders bedenkenswert scheint mir, daß ein yield return versehentlich übersprungen wird, wenn aus einer Block-Struktur, etwa mit break; heraussgesprungen wird.
Nichtmal mit einem try{ }finally{ }-Konstrukt ist diesen Problemen zu begegnen, denn yield return darf nicht in finally-Blöcken stehen.
Auch kann man nicht innerhalb von weiter-verzweigten Methoden den Thread umschalten, jedenfalls nicht direkt.
In dem Fall muß man die Unterfunktion auch als Yield-Thread-Methode anlegen, und wie im Beispiel als Optimierung gezeigt, der Yield-Threading-Extension übergeben.
Danksagung:
Diese fabelhafte Idee ist leider nicht von mir. Ich habe sie aus dem
Material zum Kurzvortrag am 17.2.09, den Ralf Hoffmann gehalten hat, der Erfinder dieses Ansatzes.