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, ++:::
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
yield return ExecuteIn.GuiThread
daß der darauf folgende Code im GuiThread ausgeführt wird
Und mit
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.
Eine besondere Feinheit ist die als :::{style="color: green;"}'##Optimierung){green}
gekennzeichnete Verknüpfung der Ladevorgänge: Der 2. Vorgang verwendet den Workerthread des ersten einfach weiter.
using System;
using System.Data;
using System.Drawing;
using System.Windows.Forms;
using System.Threading;
using System.Collections.Generic;
namespace YieldThreadTest {
public partial class uclYieldThread : UserControl {
public uclYieldThread() {
InitializeComponent();
SetupDB();
/* .FirstSection(ExecuteIn.GuiThread) gibt an, daß in LoadNames() der Code-Abschnitt bis zum
* ersten yield return im GuiThread erfolgen soll.
* Die Thread-Bestimmung der weiteren Abschnitte erfolgt den jeweiligen yield Rückgabewert. */
btLoadNames.Click += (s, e) => LoadNames().FirstSection(ExecuteIn.GuiThread);
btLoadDates.Click += (s, e) => LoadBirthDates().FirstSection(ExecuteIn.GuiThread);
}
IEnumerable<ExecuteIn> LoadNames() {
SetGuiViewToAsync("Loading Names"); // Beginn der Aktion melden
personDts.Person.Clear();
yield return ExecuteIn.WorkerThread; // in WorkerThread schalten
foreach (var rw in dataBase.Person) {
// zeitaufwändig Daten holen
Thread.Sleep(300);
var name = rw.Name;
// den im Gui-Thread auszuführenden Code-Bereich in entsprechende yield-returns einschließen.
yield return ExecuteIn.GuiThread; // in GuiThread schalten
personDts.Person.AddPersonRow(name, default(DateTime)); // aufgrund von DataBinding ist dieses
// dem GuiThread zuzuordnen
toolStripProgressBar1.PerformStep();
yield return ExecuteIn.WorkerThread; // in WorkerThread zurück-schalten
}
yield return ExecuteIn.GuiThread; //für Abschluß-Meldungen wieder in GuiThread schalten
ResetGuiView(); // Abschluß der Aktion melden
if (MessageBox.Show(this,
"Wanna load BirthDates too?", "Wanna load BirthDates too?",
MessageBoxButtons.YesNo) == DialogResult.Yes) {
// ##Optimierung: in den WorkerThread schalten und dann die YieldThread-Ausführung aufrufen
// So wird statt eines neuen NebenThreads der bisherige weiter verwendet.
yield return ExecuteIn.WorkerThread;
LoadBirthDates().FirstSection(ExecuteIn.GuiThread);
}
}
IEnumerable<ExecuteIn> LoadBirthDates() {
SetGuiViewToAsync("Loading Birthdates");
personDts.Person.ForEach(rw => rw.BornAt = default(DateTime)); //alte Datumse rauslöschen
yield return ExecuteIn.WorkerThread; // in WorkerThread schalten
foreach (var rw in personDts.Person) {
// zeitaufwändig Daten holen
Thread.Sleep(300);
var bornAt = dataBase.Person.FindByName(rw.Name).BornAt;
// den im Gui-Thread auszuführenden Code-Bereich in entsprechende yield-returns einschließen.
yield return ExecuteIn.GuiThread;
rw.BornAt = bornAt; // aufgrund von DataBinding ist dieses
// dem GuiThread zuzuordnen
toolStripProgressBar1.PerformStep();
yield return ExecuteIn.WorkerThread;
}
yield return ExecuteIn.GuiThread;
ResetGuiView();
}
private void SetupDB() {
dataBase.Person.AddPersonRow("Homberg", DateTime.Parse("11.9.1962"));
dataBase.Person.AddPersonRow("Hempel", DateTime.Parse("12.9.1962"));
dataBase.Person.AddPersonRow("Meier", DateTime.Parse("13.9.1962"));
dataBase.Person.AddPersonRow("Lüdenscheidt", DateTime.Parse("14.9.1962"));
dataBase.Person.AddPersonRow("Gauß", DateTime.Parse("11.4.1962"));
dataBase.Person.AddPersonRow("Euler", DateTime.Parse("15.9.1962"));
dataBase.Person.AddPersonRow("Gates", DateTime.Parse("18.9.1962"));
dataBase.Person.AddPersonRow("Merkel", DateTime.Parse("11.9.1962"));
dataBase.Person.AddPersonRow("Hinz", DateTime.Parse("10.9.1962"));
dataBase.Person.AddPersonRow("Kunz", DateTime.Parse("6.9.1962"));
}
private void SetGuiViewToAsync(string status) {
toolStripStatusLabel1.Text = status;
toolStripProgressBar1.Value = 0;
toolStripProgressBar1.Visible = true;
}
private void ResetGuiView() {
toolStripStatusLabel1.Text = "Ready";
toolStripProgressBar1.Visible = false;
}
}
}
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"
Auch zu beachten
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.
Der frühe Apfel fängt den Wurm.
Nachdem ich den "AsyncExecutor" jetzt seit fast 2 Jahren verwende dachte ich mir es wird Zeit ihn zusammen mit einem Beispiel bei codeplex einzustellen:
http://asyncexecutor.codeplex.com/
Grüße,
Ralf
Hallo zusammen,
zur Info: mit C# 5.0 wurden die Schlüsselworte async und await eingeführt, wobei das await im wesentlichen dem ExecuteIn.GuiThread entspricht.
herbivore