Laden...

[ExtensionMethods] GuiThread und WorkerThread einfach umschalten

Erstellt von ErfinderDesRades vor 15 Jahren Letzter Beitrag vor 12 Jahren 7.509 Views
ErfinderDesRades Themenstarter:in
5.299 Beiträge seit 2008
vor 15 Jahren
[ExtensionMethods] GuiThread und WorkerThread einfach umschalten

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.

C
1 Beiträge seit 2011
vor 12 Jahren
AsyncExecutor - jetzt auf Codeplex

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

49.485 Beiträge seit 2005
vor 12 Jahren

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