myCSharp.de - DIE C# und .NET Community
Willkommen auf myCSharp.de! Anmelden | kostenlos registrieren
 | Suche | FAQ

» Hauptmenü
myCSharp.de
» Startseite
» Forum
» FAQ
» Artikel
» C#-Snippets
» Jobbörse
» Suche
   » Plugin für Firefox
   » Plugin für IE7
   » Gadget für Vista
» Regeln
» Wie poste ich richtig?
» Datenschutzerklärung
» wbb-FAQ

Mitglieder
» Liste / Suche
» Stadt / Anleitung dazu
» Wer ist wo online?

Angebote
» ASP.NET Webspace
» Bücher
» Zeitschriften
   » dot.net magazin
» Accessoires

Ressourcen
» .NET-Glossar
» guide to C#
» openbook: Visual C#
» openbook: OO
» .NET BlogBook
» MSDN Webcasts
» dotnetjob.de
» Search.Net

Team
» Kontakt
» Übersicht
» Wir über uns
» Bankverbindung
» Impressum

» Unsere MiniCity
MiniCity
» myCSharp.de Diskussionsforum
Du befindest Dich hier: Community-Index » Diskussionsforum » Knowledge Base » FAQ » [FAQ] Controls von Thread aktualisieren lassen (Control.Invoke/Dispatcher.Invoke)
Letzter Beitrag | Erster ungelesener Beitrag Druckvorschau | An Freund senden | Thema zu Favoriten hinzufügen

Antwort erstellen
Zum Ende der Seite springen  

[FAQ] Controls von Thread aktualisieren lassen (Control.Invoke/Dispatcher.Invoke)

 
Autor
Beitrag « Vorheriges Thema | Nächstes Thema »
herbivore
myCSharp.de-Team (Admin)

images/avatars/avatar-2627.gif


Dabei seit: 11.01.2005
Beiträge: 47.498
Entwicklungsumgebung: csc/nmake (nothing is faster)
Herkunft: Berlin


herbivore ist offline

[FAQ] Controls von Thread aktualisieren lassen (Control.Invoke/Dispatcher.Invoke)

Beitrag: beantworten | zitieren | editieren | melden/löschen       | Top

Hallo Community,

der folgende Text schildert Problem und Lösung zunächst anhand von Windows-Forms, aber die Prinzipien gelten für WPF ganz genauso, nur die Texte der Fehlermeldungen und der konkrete Code sind bei WPF etwas anders. Deshalb sollten WPF-Programmierer, den kompletten Text lesen, inkl. des Abschnitts am Ende, der die kleinen Unterschiede zwischen Windows-Forms und WPF beschreibt. Windows-Forms-Programmierer sollten ebenfalls den kompletten Text lesen, können aber den letzten Abschnitt überspringen.


Das Problem

Alle Zugriffe auf GUI-Elemente (Controls, Form u.ä.) müssen aus dem Thread heraus erfolgen, der sie erzeugt hat. Wenn man sich nicht daran hält und aus einem extra Thread direkt auf GUI-Elemente zugreift, bekommt man unter .NET 2.0 meist folgende Meldung:

Fehlermeldung:
"Unzulässiger threadübergreifender Vorgang" bzw. "Ungültiger threadübergreifender Vorgang: Der Zugriff auf das Steuerelement 'Name des Steuerelements' erfolgte nicht von dem Thread aus, in dem das Steuerelement erstellt wurde." (*)

Und ggf. noch:

Fehlermeldung:
InvalidOperationException wurde nicht vom Benutzercode behandelt.

Aber auch in .NET 1.x und auch wenn diese Meldung nicht erscheint, sind direkte Zugriffe aus Threads unzulässig und können zu merkwürdigen Effekten und echten Fehlern führen. Deshalb würde es auch nichts nutzen, die Prüfung abzuschalten, sondern man muss auf jeden Fall die Ursache beheben.


Die Lösung

Die Lösung liegt darin, die gewünschten Zugriffe in eine neue Methode zu packen und diese Methode mit Control.Invoke oder Control.BeginInvoke aufzurufen. Das Control.Invoke/BeginInvoke bewirkt, dass die Methode nicht vom aufrufenden Thread, sondern vom GUI-Thread ausgeführt wird.


Unterschiede von Control.Invoke und Control.BeginInvoke

In vielen Fällen kann man wählen, ob man Control.BeginInvoke oder Control.Invoke benutzt. Der Hauptunterschied ist, dass Control.Invoke wartet, bis der GUI-Thread die Aktion ausgeführt hat, wogegen Control.BeginInvoke den Arbeitsauftrag nur in die Nachrichtenschlange des GUI-Threads stellt und sofort zurückkehrt. Control.Invoke arbeitet also (in vielen Fällen unnötig) synchron. Allerdings muss man bei Control.BeginInvoke jegliche erforderliche Synchronisation beim gleichzeitigen Zugriff auf dieselben Daten selbst realisieren. Ein weiter Unterschied ist, dass man bei Control.Invoke leichter an einen evtl. Rückgabewert der Aktion kommt als bei Control.BeginInvoke.

Wenn man Control.BeginInvoke benutzt und später doch noch an den Rückgabewert der Aktion kommen möchte oder später doch noch auf die Beendigung der Aktion warten möchte, kann man Control.EndInvoke benutzen. Control.EndInvoke arbeitet synchron und kehrt erst zurück, nachdem die Aktion ausgeführt wurde. Der Aufruf von Control.EndInvoke ist optional, also nicht erforderlich.


Control.InvokeRequired und grundlegende Codebeispiele

Mit Control.InvokeRequired kann man abfragen, ob ein Aufruf von Control.Invoke/BeginInvoke nötig ist. Dadurch kann man Methoden nach folgendem Muster schreiben, ...

C#-Code:
void DoCheapGuiAccess ()
{
   if (ctrl.InvokeRequired) { // Wenn Invoke nötig ist, ...
      // dann rufen wir die Methode selbst per Invoke auf
      ctrl.Invoke (new MethodInvoker (DoCheapGuiAccess));
      return;
   }
   // eigentlicher Zugriff; läuft jetzt auf jeden Fall im GUI-Thread
   ctrl.Text = "Hello World!";
}

... die man dann nach Belieben aus dem GUI-Thread oder aus beliebigen anderen Threads heraus aufrufen kann. Es ist aber zu beachten, dass nur "billige" GUI-Zugriffe erfolgen dürfen, keine langlaufenden Aktionen. Siehe auch  [FAQ] Warum blockiert mein GUI? Abschnitt "Achtung: Die Falle".

In vielen Fällen wird man die Funktion DoCheapGuiAccess sowieso nur aus dem Worker-Thread heraus aufrufen wollen. Dann kann man sich das Control.InvokeRequired sparen, weil man weiß, dass es immer true liefert.

C#-Code:
// Implementierung der Methode
void DoCheapGuiAccess ()
{
   ctrl.Text = "Hello World!";
}

// Aufruf der Methode aus dem Worker-Thread
ctrl.Invoke (new MethodInvoker (DoCheapGuiAccess));

Man kann die Funktion DoCheapGuiAccess auch mit Parametern und einem Rückgabewert ausstatten. Dann muss man statt MethodInvoker natürlich einen passenden Delegattyp verwenden, z.B. die generischen Action- oder Func-Delegaten aus dem System-Namespace. Parameter für DoCheapGuiAccess übergibt man einfach als weitere Parameter an Control.Invoke, nicht direkt an DoCheapGuiAccess! Control.Invoke reicht diese intern an DoCheapGuiAccess weiter. Andersherum wird der Rückgabewert von DoCheapGuiAccess auch von Control.Invoke zurückgegeben, allerdings ist er beim Control.Invoke vom Typ Object, so dass man hier bei Bedarf noch in den echten Rückgabetyp casten muss.

Bei der Umsetzung der Beispiele in euren Code müsst ihr insbesondere darauf achten, dass ihr Control.Invoke/BeginInvoke benutzt (und nicht etwa z.B. Delegate.Invoke/BeginInvoke) und dass ihr die vielleicht ungewohnte Aufrufsyntax genau einhaltet. Wenn es nicht gleich klappt, also zuerst diese beiden Punkte prüfen.


Zugriffe zusammenfassen / Performance von Control.Invoke und Control.BeginInvoke

Solange alle Forms und Controls - wie empfohlen - vom selben Thread, also dem GUI-Thread erzeugt werden, ist es egal, welches Form oder Control man für das Control.Invoke verwendet, denn Control.Invoke übergibt die Steuerung ja nicht an das konkrete Form oder Control, sondern an den Thread, in dem das Form oder Control erzeugt wurde. Und das ist ja bei allen Forms und Controls dann ein und derselbe (GUI-)Thread, egal welches Form oder Control man wählt.

In einer Methode, die man mit Control.Invoke/BeginInvoke aufruft, können mehrere Zugriffe auf mehrere Controls erfolgen. Man muss also nicht für jeden einzelnen Zugriff oder jedes einzelne Control eine einzelne Methode schreiben, sondern kann und sollte Zugriffe zusammenfassen (und dazu nötigenfalls zunächst zu sammeln und die eigentliche Aktualisierung des GUIs per Timer durchzuführen). Das ist auch deshalb zu empfehlen, weil Control.Invoke eine vergleichsweise teure (=langsame) Operation ist.

Allerdings muss man darauf achten, dass die Laufzeit der zusammengefassten Zugriffe nicht mehr als 1/10s beträgt, da sonst das GUI-blockiert; siehe  [FAQ] Warum blockiert mein GUI?

Control.BeginInvoke ist deutlich weniger teuer als Control.Invoke. Trotzdem sollte man mit Aufrufen von Control.BeginInvoke genauso sparsam sein, denn jeder Aufruf von Control.BeginInvoke stellt eine Nachricht in die Nachrichtenschlange des GUI-Threads. Wenn sich nun hintereinander soviele solcher Nachrichten in der Nachrichtenschlange angesammelt haben, dass deren Abarbeitung in der Summe länger 1/10s dauert, blockiert das GUI genauso, als wäre nur eine Nachricht eingestellt worden, deren Verarbeitung länger als 1/10s dauert.


Databinding: Zugriffe auf gebundene Daten

Nicht nur Zugriffe auf Controls selbst müssen aus dem GUI-Thread erfolgen. Auch alle Zugriffe auf andere Objekte/Daten müssen aus dem GUI-Thread erfolgen, nachdem diese mittels Databinding an Controls gebunden wurden. Das heißt, man kann durchaus eine aufwändige Liste in einem Worker-Thread füllen, aber sobald diese Liste an ein Control gebunden wurde, was natürlich im GUI-Thread erfolgen muss, müssen auch alle weiteren Zugriffe auf die Liste und/oder darin enthalte Objekte aus dem GUI-Thread erzeugen.


Parent-Child-/Owner-Owned-Beziehungen

Wenn man zwei Forms/Controls in eine z.B. Parent-Child-Beziehung zueinander setzt (z.B. durch form1.Controls.Add (textBox1)), müssen beide Forms/Controls im selben Thread erzeugt worden sein. Controls, die in Beziehung stehen, führen untereinander Zugriffe aus und die wären dann fälschlich threadübergreifend, wenn die beiden Forms/Controls in unterschiedlichen Threads laufen würden. Man darf also insbesondere nicht das Form in einem und seine Controls in einem anderen Thread erzeugen. Sinnvollerweise sollte es immer nur einen GUI-Thread geben.

Im Thread  Controls in anderem Thread erzeugen als das Form [==> auf keinen Fall] wird erklärt, warum eben dies gar nicht nötig ist.


Fehlender Fenster-Handle

Fehlermeldung:
Invoke oder BeginInvoke kann für ein Steuerelement erst aufgerufen werden, wenn das Fensterhandle erstellt wurde.

Wenn diese Fehlermeldung erscheint, dann existiert zwar schon das (.NET)Control, für das ihr Control.Invoke/BeginInvoke aufrufen wollt, aber das zugrundeliegende Win32-Control wurde noch nicht erzeugt. Und somit gibt es noch keinen Handle, mit dem das Win32-Control angesprochen werden kann. In diesem Fall reicht es rechtzeitig vorher im GUI-Thread Control.CreateControl aufzurufen. Die CreateControl-Methode erzwingt das Erstellen eines Handles für das Steuerelement (sowie der untergeordneten Steuerelemente). Wenn man Control.CreateControl benötigt, dann ruft man es am besten gleich im Konstruktor des Forms auf.

Diese Meldung - oder alternativ eine ObjectDisposedException - kann auch erscheinen, wenn das Form/Control schon wieder geschlossen/zerstört ist. Dieser Fall kann außerdem eintreten, wenn Control.Invoke aus einem (Timer-)Event heraus erfolgt und das (Timer-)Event gefeuert wird, nachdem das Form geschlossen wurde. Vor dem Schließen/Zerstören eines Forms/Controls sollte also alle EventHandler, die ein Control.Invoke auf das entsprechende Form/Control ausführen, deregistriert werden. Darüber hinaus muss (durch korrekte Thread-Synchronisation) sichergestellt werden, dass sich keiner dieser EventHandler aktuell in Ausführung befindet.

Der Fehler mit dem fehlenden Fensterhandle tritt auch auf, wenn man sowas probiert:

C#-Code (FALSCH):
new Form1 ().Invoke (new MethodInvoker (DoCheapGuiAccess)); // falsch

Dieser Code würde selbst dann nichts nützen, wenn man vor dem Control.Invoke den FensterHandle noch explizit erzeugen würde, denn ein Form oder Control, das man in dem Thread erzeugt hat, der Control.Invoke aufruft, macht natürlich keinen Sinn. Man braucht immer ein Control, das im und vom GUI-Thread erzeugt wurde, damit Control.Invoke die Kontrolle an den GUI-Thread übergibt.


Spezielle Probleme

Wenn man in dem Thread gar kein Control zur Verfügung hat, für das man Control.Invoke oder Control.BeginInvoke aufrufen kann, muss man nicht verzweifeln, denn gibt es ab .NET 2.0 SynchronizationContext.Post/Send. Allerdings wird in  Eleganteste Art aus Worker-Thread auf Controls zugreifen [generell Kontrollfluss zwischen Threads] beschrieben, warum das eher eine Notlösung ist. Für .NET 1.1 hat Programmierhans in  Komponenten mit mehreren Threads quasi einen SynchronizationContext nachgebaut, den man aber nur verwendet werden sollte, wenn man tatsächlich noch .NET 1.1 benutzt.

Wenn ihr Control.InvokeRequired benutzt, kann es in sehr seltenen Fällen zu einem heimtückischen Problem kommen. Details in  Threadübergreifender Vorgang trotz InvokeRequired.

Wenn ihr den Fehler threadübergreifender Vorgang im EventHandler eines Timers bekommt, dann habt wahrscheinlich den falschen Timer. Nehmt System.Windows.Forms.Timer.

Bei der Verwendung von Control.Invoke kann man sich Deadlocks einhandeln, weil der extra Thread darauf wartet, bis das GUI die Aktion durchgeführt hat. Wenn das GUI nun so programmiert ist, dass es gerade auf den extra Thread wartet, warten die beiden ewig aufeinander. Das GUI sollte *nie* auf die Threads warten (es sollte grundsätzlich nie auf irgendwas warten). Das ist auch nie nötigt. Stattdessen sollen Thread nötigenfalls eigene Events feuern, die das GUI abonnieren kann. Hinweis: Die EventHandler laufen ohne weiteres Zutun im Worker-Thread, weshalb man beim Zugriff auf Controls und gebundene Daten das Control.Invoke nicht vergessen darf.

Ob so eine Deadlock-Situation vorliegt, kann man leicht testen, in dem man Control.Invoke durch Control.BeginInvoke ersetzt. Funktioniert es dann, hatte man vorher wohl einen solchen Deadlock. *Aber* einfach Control.BeginInvoke zu lassen, ist keine Lösung! Man sollte dann die Ursache suchen und beheben. Meist geht das so, wie im vorigen Absatz beschrieben.

Wenn man im FormClosing das Beenden des Worker-Threads anstoßen will und gleichzeitig möchte, dass sich das Form erst schließt, nachdem der Thread beendet ist, sollte man auf keinen Fall Thread.Join oder while(IsAlive) verwenden (das GUI sollte grundsätzlich nie auf irgendwas warten), sondern stattdessen sollte man das FormClosing abbrechen (e.Cancel = true), solange der Thread noch lebt. Als letzte Aktion kann der Thread einen eigenes Event feuern, der vom GUI abonniert wird und das dann per Control.Invoke Form.Close aufrufen kann.

Eine Konstruktion mit einem Worker-Thread per anonymen Delegaten mit Parameterübergabe, bei der auf Controls zugegriffen wird, also z.B.

C#-Code (FALSCH):
new Thread(() => { DoSomethingExpensive (myControl1.Text, myControl2.Text) }).Start ();

kann und wird zu einem unzulässigem threadübergreifendem Zugriff führen, weil der Zugriff auf myControl1.Text und myControl2.Text erst aus dem neu gestarteten Thread heraus erfolgt. Wenn man so eine Konstruktion verwenden will, muss man die Werte aus den Controls noch im GUI-Thread abrufen, diese z.B. in lokalen Variablen zwischenspeichern und mit diesen DoSomethingExpensive aufrufen, also z.B.

C#-Code (richtig):
String text1 = myControl1.Text;
String text2 = myControl2.Text;
new Thread(() => { DoSomethingExpensive (text1, text2) }).Start ();


Weitere Codebeispiele


Weitere Codebeispiele finden sich in  Controls von Thread aktualisieren lassen (Invoke-/TreeView-Beispiel) und  invoke und eventproblem sowie über die Forumsuche. Diese Codebeispiele solltet ihr euch unbedingt ansehen.


Weiterführende Infos

Hier noch ein Verweis auf den guten Artikel  Sicheres und einfaches Multithreading in Windows Forms von den Microsoft-Seiten.


BackgroundWorker

Statt mit extra Threads und Control.Invoke, kann man auch mit der BackgroundWorker-Klasse ungültige threadübergreifende Vorgänge vermeiden. Dazu müssen bei BackgroundWorker alle Zugriffe auf das GUI einfach aus den ProgressChanged- oder RunWorkerCompleted-EventHandlern durchgeführt werden. Wenn man sich daran hält, ist explizites Control.Invoke bei BackgroundWorker nicht erforderlich, denn die genannten EventHandler werden hinter den Kulissen automatisch per Control.BeginInvoke aufgerufen.

Alles vorausgesetzt, der BackgroundWorker wurde korrekt initialisiert, so dass er insbesondere einen passenden SynchronizationContext verwendet. Wenn der BGW unter Verwendung von VS im GUI-/Main-Thread erzeugt wurde, sollte das automatisch der Fall sein.


Wie geht es in WPF?

Da ich (noch) kein WPF-Kenner bin, hat mir bei dem folgenden Text und Code michlG geholfen. Vielen Dank!

Im Vergleich zu Windows-Forms hat sich bei WPF nicht viel geändert. In Windows.Forms hat man direkt Control.Invoke verwendet, in WPF ist jedes Control von DispatcherObject abgeleitet und besitzt daher einen Dispatcher. Statt Control.Invoke verwendet man jetzt Control.Dispatcher.Invoke. Statt der InvokeRequired-Property von Windows-Forms gibt es in WPF nun die CheckAccess-Methode, welches true zurückliefert, wenn man im GUI-Thread ist und false, wenn man in einem anderen Thread ist. Der Rückgabewert ist also gegenüber Control.InvokeRequired genau negiert. Die Fehlermeldung lautet bei WPF:

Fehlermeldung:
Der aufrufende Thread kann nicht auf dieses Objekt zugreifen, da sich das Objekt im Besitz eines anderen Threads befindet.

Hier das Codebeispiel von oben abgeändert für WPF:

C#-Code:
void DoCheapGuiAccess ()
{
   if (!ctrl.Dispatcher.CheckAccess ()) { // Wenn Invoke nötig ist, ...
      // dann rufen wir die Methode selbst per Invoke auf
      ctrl.Dispatcher.Invoke (new Action (DoCheapGuiAccess));
      return;
   }
   // eigentlicher Zugriff; läuft jetzt auf jeden Fall im GUI-Thread
   ctrl.Text = "Hello World!";
}

Da der Delegattyp MethodInvoker in System.Windows.Forms definiert ist, wurde er durch den Delegattyp Action aus System ersetzt.

Weitere Infos für WPF:  Threading-Modell


Siehe auch

 Gewusst wie: Threadsicheres Aufrufen von Windows Forms-Steuerelementen


herbivore

(*) Alternative Fehlermeldungen in anderen Sprachen bzw. aus anderen .NET-Versionen sowie andere Fehlermeldungen, die ebenfalls daraufhinweisen, dass der verursachende Code fälschlich nicht im GUI-Thread läuft.

Fehlermeldung:
  • Control.Invoke must be used to interact with controls created on a separate thread.
  • SendKeys kann nicht innerhalb der Anwendung ausgeführt werden, da diese Anwendung keine Windows-Meldungen verarbeitet. Ändern Sie die Anwendung so, dass sie Meldungen behandelt, oder verwenden Sie die SendKeys.SendWait-Methode.
02.03.2007 11:05 E-Mail | Beiträge des Benutzers | zu Buddylist hinzufügen
Baumstruktur | Brettstruktur       | Top 
myCSharp.de | Forum Der Startbeitrag ist älter als 6 Jahre.
Der letzte Beitrag ist älter als 6 Jahre.
Antwort erstellen


© Copyright 2003-2013 myCSharp.de-Team. Alle Rechte vorbehalten. 25.05.2013 18:58