Laden...

Eleganteste Art aus Worker-Thread auf Controls zugreifen [generell Kontrollfluss zwischen Threads]

Erstellt von moson vor 14 Jahren Letzter Beitrag vor 14 Jahren 11.683 Views
Information von herbivore vor 13 Jahren

Dies ist ein Thread, auf den aus der FAQ verwiesen wird. Bitte keine weitere Diskussion, sondern nur wichtige Ergänzungen und diese bitte knapp und präzise. Vielen Dank!

moson Themenstarter:in
151 Beiträge seit 2003
vor 14 Jahren
Eleganteste Art aus Worker-Thread auf Controls zugreifen [generell Kontrollfluss zwischen Threads]

Mahlzeit,

ich hab mal ne Frage und zwar hab ich eine Form welche ein Object o von Klasse Klasse instanziert. Dann wird bei einem Buttonklick eine Methode von o aufgerufen. In der Methode wird nun ein neuer Thread erstellt, aus diesem Thread heraus sollen Eigenschaften von Controls auf dem Form verändert werden.

In Code ausgedrückt:


public partial class Form1 : Form
{
        ...
        private void button1_Click(object sender, EventArgs e)
        {
                Klasse o = new Klasse("blablubb");
                o.DoFunkyStuff();
        }
        ...
}
class Klasse
{
        public void DoFunkyStuff()
        {
                 ...
                 Thread th = new Thread(new ThreadStart(DoOtherFunkyStuff));
                 th.Start();
                 ...
        }
        private void DoOtherFunkyStuff()
        {
                 ...
                 //Hier Zugriff auf Form1 Controls... | Feuern des Events...
                 ...
        }
}

Ich habe das jetzt erstmal über ein Event welches ich im Form abboniert habe + Form.Invoke gelöst.



public delegate void myDelegate(string a, string b, int x);

private void StatusChanged(string a, string b, int x) //Methode die beim Auslösen des Events gefeuert wird
{
       this.Invoke(new myDelegate(updateGUI), a, b, x);
}
private void updateGUI(string a, string b, int x)
{
       Control1.Text = a;
       Control2.Text = b;
       Control3.Value = x;
}

Ist die Vorgehensweise so in Ordnung oder gibts da bessere/schönere Ansätze?

1.820 Beiträge seit 2005
vor 14 Jahren

Hallo!

Man kann sich die Deklaration eines Delegaten sparen, wenn man anonyme Methoden verwendet:


control.BeginInvoke((MethodInvoker)delegate
    {
        // Do something with control
    }
);

Nobody is perfect. I'm sad, i'm not nobody 🙁

1.665 Beiträge seit 2006
vor 14 Jahren
Hinweis von herbivore vor 13 Jahren

Ursprünglich gepostet am 25.06.2009 11:01:10. Postzeit geändert, um den Beitrag an diese Stelle im Thread zu verschieben, direkt hinter den Beitrag, auf den sich die Antwort bezieht.

Was aber wiederum Edit & Continue beim Debuggen unmöglich macht 😃

49.485 Beiträge seit 2005
vor 14 Jahren

Hallo moson,

Ist die Vorgehensweise so in Ordnung oder gibts da bessere/schönere Ansätze?

ja, das ist eine sinnvolle Aufgabenteilung und insofern vollkommen in Ordnung.

Natürlich könntest du beim Auslösen des Events intern SynchronizationContext.Send oder Post verwenden, damit die EventHandler im GUI-Thread laufen, wie das auch bei den BackgroundWorker-Events passiert. Diese Aufgabenteilung ist weniger sinnvoll, aber dafür etwas bequemer, inbesondere wenn ungeübte Programmierer die Klasse benutzen wollen.

Mein Fazit ist trotzdem: besser und richtiger ist es, wie du es jetzt hast.

herbivore

6.911 Beiträge seit 2009
vor 14 Jahren

Hallo herbivore,

kannst du das bitte begründen:

Natürlich könntest du beim Auslösen des Events intern SynchronizationContext.Send oder Post verwenden, damit die EventHandler im GUI-Thread laufen, wie das auch bei den BackgroundWorker-Events passiert. Diese Aufgabenteilung ist weniger sinnvoll,

Ich finde es passender wenn der Aufrufer nichts zusätzliches implementieren muss - für mich bedeutet das auch Kapselung. Dem Aufrufer kann es ja egal sein ob die Methode die er ausführen lässt im gleichen Thread läuft oder in einem anderen. Er sollte gar nicht mitbekommen was in der Methode passiert - hauptsache es geschieht das was passieren soll.

Der Aufwand einen SynchronizationContext oder eine AsyncOperation zu implementieren ist (für mich) geringer als für jeden EventHandler ein Invoke zu schreiben.

Oder sehe ich da etwas falsch?

Vielen Dank im Voraus für die Antwort(en).

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

3.971 Beiträge seit 2006
vor 14 Jahren

Hallo moson,
Verwende bitte bei kleinen asynchronen Aufgaben ThreadPool.QueueUserWorkItem statt jedesmal aufwändig einen eigenen Thread dafür zu erstellen.

Es gibt 3 Arten von Menschen, die die bis 3 zählen können und die, die es nicht können...

49.485 Beiträge seit 2005
vor 14 Jahren
Hinweis von herbivore vor 12 Jahren

In diesem Beitrag geht es, ausgehend von einem Szenario, in dem die Ausführung des Code in EventHandlern an den GUI-Thread delegieren werden muss, um die Frage, ob das dafür nötige Control.Invoke, schon beim Auslösen eines Events aufrufen soll, so dass alle registrieren EventHandler automatisch im GUI-Thread laufen, oder ob Control.Invoke erst in den EventHandlern aufgerufen werden soll.

Um die Frage zu beantworten, werden einige sehr prinzipielle Überlegungen über Zuständigkeiten angestellt. Die kurze Antwort ist, dass erst die EventHandler wissen, was zu tun ist und daher auch erst diese bei Bedarf das Control.Invoke aufrufen sollten.

Bezogen auf auf die Frage, was man tun soll, wenn man in einem Worker-Thread kein Control zur Verfügung hat, um Control.Invoke aufrufen zu können, bedeutet das, dass man in dem Thread ein [FAQ] eigenen Event definieren und feuern sollte, der vom GUI abonniert wird. Im Ergebnis liegt der EventHandler in einer GUI-Klasse und dort ist dann automatisch ein passendes Control für das Control.Invoke verfügbar.

Natürlich kann man gedanklich noch einen Schritt weiter gehen: Wenn sich die Notwendigkeit für einen Worker-Thread aus der Anforderung ergibt, dass das GUI nicht blockieren soll, dann ist der Thread selbst Sache des GUIs (auch wenn aus dem Thread heraus Methoden der Modellklassen aufgerufen werden). Im Ergebnis gehört die ThreadStart-Methode in eine GUI-Klasse und dort ist dann automatisch ein passendes Control verfügbar. Events sind in diesem Fall gar nicht erforderlich.

Für WPF gilt alles vollkommen analog, nur dass Dispatcher.Invoke statt Control.Invoke verwendet wird.

Hallo gfoidl,

Ich finde es passender wenn der Aufrufer nichts zusätzliches implementieren muss - für mich bedeutet das auch Kapselung.

nicht jeder Aufrufer muss ja was zusätzliches implementieren, sondern nur Aufrufer, die besondere Anforderungen haben.

Dem Aufrufer kann es ja egal sein ob die Methode die er ausführen lässt im gleichen Thread läuft oder in einem anderen.

Dem GUI ist das aber - im hier besprochenen Fall - eben nicht egal. Es ist eben eine Anforderung, die das GUI stellt. Und genau deshalb sollte es auch in der Zuständigkeit des GUIs liegen, dafür zu sorgen, dass alle GUI-Zugriffe aus dem GUI-Thread erfolgen. Deine Argumentation umgedreht: Es kann und sollte der aufgerufenen Klasse doch egal sein, dass der Aufrufer besondere Anforderungen hat. Ansonsten müsstest du ja die Klasse ändern, wenn ein weiterer Aufrufer kommt, der noch andere Anforderungen hat.

Er sollte gar nicht mitbekommen was in der Methode passiert - hauptsache es geschieht das was passieren soll.

Richtig!

Der Aufwand einen SynchronizationContext oder eine AsyncOperation zu implementieren ist (für mich) geringer als für jeden EventHandler ein Invoke zu schreiben.

Aus Bequemlichkeitsgründen kann man das auch machen. Das habe ich ja gesagt. Und da SynchronizationContext die Spezialitäten der typischen Arten von Aufrufern abhandelt, ist das auch nicht wirklich verwerflich, wenn man es benutzt, aber richtig sauber ist es eben auch nicht.

Stell dir vor, du hast einen Worker-Thread, der seine Arbeitsaufträge über eine synchronisierte Queue bekommt (SyncQueue <T> - Eine praktische Job-Queue). Und stell dir weiter vor, dieser Thread benutzt ein (COM-)Objekt, auf das alle Zugriffe von dem Thread erfolgen müssen, der das Objekt erzeugt hat. Dann müsste nach deiner Logik die aufgerufene Klasse die EventHandler, die solche Zugriffe ausführen, an diesen Thread delegieren. Wie man Zugriffe an diesen Thread delegiert, weiß aber die SynchronizationContext-Klasse nicht und schon funktioniert die heile und bequeme Welt nicht mehr. Spätestens dann muss das Delegieren doch vom EventHandler vorgenommen werden(*), der ja weiß wie das geht. Und damit zeigt sich, es liegt in der Zuständigkeit des Aufrufers dafür zu sorgen, dass seine speziellen Anforderungen berücksichtigt werden.

Überhaupt kann man die Ausführung nur an Threads delegieren, die kooperativ sind, also extra so programmiert sind, dass man ihnen Arbeitsaufträge übergeben kann. Wenn ein Thread von Anfang bis Ende eine sequentielle Verarbeitung durchführt, dann tut er eben stur genau das und nicht das, was andere Threads gerne von ihm hätten. So kann man z.B. an den Main-Thread eines Konsolenprogramms üblicherweise nichts delegieren. Spätestens hier scheitert also selbst die SynchronizationContext-Klasse.

Zurück zum Ausgangsfall mit dem GUI: Wenn andersherum im EventHandler gar keine Zugriffe auf Controls o.ä. erfolgen, sondern einfach nur z.B. geloggt werden soll, dann würde bei deinem Vorschlag trotzdem und dann unnötigerweise an den GUI-Thread delegiert werden. Das zeigt nochmal: Nur der Aufrufer weiß, ob das nötig ist. Also soll auch der Aufrufer und nicht die aufgerufene Klasse entscheiden.

... als für jeden EventHandler ein Invoke zu schreiben

Appropos mehrere EventHandler. Jeder Aufrufer kann seine eigenen Anforderungen haben. Man würde also gar nicht mit mit einem SynchronizationContext auskommen, sondern bräuchte für jeden registrieren EventHandler einen eigenen. Das wäre zwar nicht vollkommen undenkbar, würde aber zumindest die Komplexität der Klasse weiter unnötig steigern. Da ist es doch wesentlich einfacher, wenn der jeweilige EventHandler das jeweils nötige veranlasst.

Ein weiterer Punkt ist, dass gerade Control.Invoke eine relativ teure Operation ist (und auch SynchronizationContext.Send macht im Fall von Windows Forms intern auch nichts anders als Control.Invoke). Wird das Event (möglicherweise) häufig ausgeführt, kann es durchaus zu Problemen kommen (selbst bei Control.BeginInvoke/SynchronizationContext.Post). Ein EventHandler kann dagegen durchaus erstmal mehrere Anforderungen sammeln und für sie zusammen in einem Rutsch Control.Invoke aufrufen. Es ist eh unsinnig das GUI mehr als ca. 10 mal pro Sekunde zu aktualisieren. Wenn der Event das Delegieren übernehmen würde, handelt man sich also möglicherweise echte Probleme ein und ist gleichzeitig weniger flexibel.

Nicht vergessen darf man auch, dass der eigentliche Zweck von Events Entkoppelung ist. Schon alleine deshalb wäre es also geradezu widersinnig, wenn ein Event versuchen würde, spezielle Anforderungen seiner Abonnenten zu berücksichtigen.

Oder sehe ich da etwas falsch?

Zumindest sehe ich das klar anders. 😃 Ich hoffe, ich konnte dich überzeugen.

herbivore

(*) Genaugenommen muss man natürlich nicht die aufgerufene Klasse ändern. Es würde reichen, wenn der Aufrufer der Klasse einen eigenen, passenden SynchronizationContext mitgibt, der weiß, wie man korrekt delegieren muss und kann. Das wäre eine saubere Alternative. Aber eben auch nur, weil es dann wieder in der Zuständigkeit des Aufrufers liegt zu bestimmen, was seine besonderen Anforderungen sind und wie diese zu erfüllen sind.

Suchhilfe: 1000 Worte, Event, Events, EventHandler, zuständig, zuständige, zuständiger, Zuständigkeit, Zuständigkeiten, verantwortlich, verantwortliche, verantwortlicher, Verantwortlichkeit, Verantwortlichkeiten

6.911 Beiträge seit 2009
vor 14 Jahren

...nur Aufrufer, die besondere Anforderungen haben....Es kann und sollte der aufgerufenen Klasse doch egal sein, dass der Aufrufer besondere Anforderungen hat....es liegt in der Zuständigkeit des Aufrufers dafür zu sorgen, dass seine speziellen Anforderungen berücksichtigt werden....Nur der Aufrufer weiß, ob das nötig ist. Also soll auch der Aufrufer und nicht die aufgerufene Klasse entscheiden.

Mit diesen Argumenten hast du mich überzeugt. Danke für die gute Erklärung.

Wie stehst du dann zum BackgroundWorker?
Dieser funktioniert ja genau nach dem Prinzip des SynchronizationContext. Ist es bei diesem OK da sein Aufgabenbereich auf Arbeiten im Thread und Events an GUI eingeschränkt ist?

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

49.485 Beiträge seit 2005
vor 14 Jahren

Hallo gfoidl,

der BackgroundWorker ist aus meiner Sicht eine reine Komfortklasse, die einen Thread als Component kapselt, damit man ihn direkt im Designer verwenden kann. Er erzielt darauf ab, es dem Aufrufer so einfach wie möglich zu machen. Er ist also genau die oben angesprochene Bequemlichkeitslösung. Und da die SynchronizationContext-Klasse für die gängigen Anwendungs- bzw. Thread-Typen funktioniert, ist das praktisch gesehen auch ok.

herbivore

6.911 Beiträge seit 2009
vor 14 Jahren

Danke. 👍

Jetzt sehe ich das auch so wie du.

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"