der folgende Text schildert Problem und Lösung zunächst anhand von Windows-Forms, doch 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 Abschnitt über WPF überspringen.
Es gibt verschiedene Möglichkeiten, wie man die Ausführung von GUI-Zugriffen an den GUI-Thread delegieren kann. Welche man davon am besten verwendet hängt hauptsächlich davon ab, welches nebenläufige Konstrukt (z.B. Thread, ThreadPool, BackgroundWorker, Tasks, async) man verwendet hat. Das zugrundeliegende Prinzip ist immer das gleiche, nämlich das am Beispiel Control.Invoke beschriebene. Die verschiedenen anderen Möglichkeiten (Dispatcher.Invoke, SynchronizationContext.Send, BackgroundWorker.ReportProgress, Task.ContinueWith, await) werden anschließend entsprechend kurz beschrieben. Deshalb sollte man den kompletten Text von oben nach unten lesen.
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 (oder einem beliebigen anderen echten nebenläufigem Konstrukt, z.B. BackgroundWorker oder Task) direkt auf GUI-Elemente zugreift, bekommt man ab .NET 2.0 bei eingeschalteter Prüfung meist folgende Meldung:
Fehler |
"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:
Fehler |
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.
In .NET 2.0 ist die Prüfung automatisch eingeschaltet. In höheren .NET Versionen ist das leider nicht mehr der Fall. Um das Debugging zu erleichtern, sollte Control.CheckForIllegalCrossThreadCalls stets auf true gesetzt und dann die Ursache aller auftretenden Exceptions behoben werden, selbst wenn das Programm bisher scheinbar korrekt lief.
Die automatische Prüfung ist eine unschätzbare Hilfe bei der Lokalisierung des Problems. Leider gibt es aufgrund von Race Conditions, Umgebungseinstellungen und weiteren Gründen keine vollständige Garantie, dass alle denkbaren Programmierfehler in diesem Bereich per "IllegalCrossThreadCall"-Exception gemeldet werden. Deshalb sollte man zusätzlich den von Threads ausgeführten Code gründlich durchsehen, um verbliebene direkte GUI-Zugriffe zu finden.
Die Lösung
Die Lösung liegt darin, die gewünschten GUI-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, ...
bool DoCheapGuiAccess (int percent)
{
if (this.InvokeRequired) { // Wenn Invoke nötig ist, ...
// dann rufen wir die Methode selbst per Invoke auf
return (bool)this.Invoke ((Func<int,bool>)DoCheapGuiAccess, percent);
// hier ist immer ein return (oder alternativ ein else) erforderlich.
// Es verhindert, dass der folgende Code im Worker-Thread ausgeführt wird.
}
// eigentliche Zugriffe; laufen jetzt auf jeden Fall im GUI-Thread
progressBar.Value = percent; // schreibender Zugriff
return cancelCheckBox.Checked; // lesender Zugriff
}
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.
// Implementierung der Methode
bool DoCheapGuiAccess (int percent)
{
progressBar.Value = percent;
return cancelCheckBox.Checked;
}
// Aufruf der Methode aus dem Worker-Thread
cancel = (bool)this.Invoke ((Func<int,bool>)DoCheapGuiAccess, percent);
Zu beachten ist, dass man die Parameter für DoCheapGuiAccess als weitere Parameter an Control.Invoke übergibt, 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 immer vom Typ Object, so dass man in den echten Rückgabetyp casten muss.
Der Rückgabetyp wird immer als letzter Typ-Parameter von Func<> angegeben. Falls nicht benötigt, kann man die Parameter und/oder den Rückgabewert auch weglassen. Lässt man den Rückgabewert weg, kann man statt Func<> die generischen Action<>-Delegaten (ebenfalls aus dem System-Namespace) verwenden. Lässt man Parameter und Rückgabewert weg, kann man den nicht generischen Action-Delegaten oder den MethodInvoker-Delegaten verwenden.
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). Das this in den Codebeispielen ist vom Typ Form. Außerdem müsst ihr die vielleicht ungewohnte Aufrufsyntax genau einhalten. 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 erfolgen.
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
Fehler |
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:
new Form1 ().Invoke ((Action)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 und Lösungen
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.Send/Post. 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.
new Thread(() => { DoSomethingExpensive (myControl1.Text, myControl2.Text) }).Start ();
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. Ersteres wird durch BackgroundWorker.ReportProgress ausgelöst, letzteres nach dem Ende der Verarbeitung im BackgroundWorker. 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.
Tasks/TPL/async/await
Wenn man die Nebenläufigkeit direkt oder indirekt durch Tasks erzeugt hat, kann man statt Control.Invoke für den GUI-Zugriff auch Continuations verwenden, die im GUI-Thread laufen, oder await im GUI-Thread verwenden. Wie das geht, hat gfoidl weiter unten beschrieben.
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:
Fehler |
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:
bool DoCheapGuiAccess (int percent)
{
if (!this.Dispatcher.CheckAccess ()) { // Wenn Invoke nötig ist, ...
// dann rufen wir die Methode selbst per Invoke auf
return (bool)this.Dispatcher.Invoke ((Func<int,bool>)DoCheapGuiAccess, percent);
// hier ist immer ein return (oder alternativ ein else) erforderlich.
// Es verhindert, dass der folgende Code im Worker-Thread ausgeführt wird.
}
// eigentliche Zugriffe; laufen jetzt auf jeden Fall im GUI-Thread
progressBar.Value = percent; // schreibender Zugriff
return cancelCheckBox.Checked; // lesender Zugriff
}
Weitere Infos für WPF: Threading-Modell
Siehe auch
Gewusst wie: Threadsicheres Aufrufen von Windows Forms-Steuerelementen
Was soll ich tun, wenn mir alle diese (und die verlinkten) Infos noch nicht geholfen haben?
Bitte mach keinen neuen Thread auf, denn:
Die FAQ ist mittlerweile so gereift, dass alle denkbaren Normal-, Spezial- und Fehlerfälle vollständig abgedeckt sind. Wir könnten in einem neuen Thread also auch nichts anderes oder neues schreiben! Lies dir also bitte alles nochmal (mehrfach) gründlich durch.
Wenn es mit deiner bisherigen Herangehensweise nicht funktioniert, kann das auch daran liegen, dass deine Herangehensweise ungünstig oder ungeeignet ist. In diesem Fall solltest du dich besser auf einen der beschriebenen Ansätze einlassen. Und die allgemeine Beschreibung auf dein konkretes Problem zu übertragen, ist in unseren Augen eben genau deine Aufgabe.
Wenn der Code trotzdem nicht tut, was er deiner Meinung nach tun sollte, stelle alle deine Annahmen, die du getroffen hast, infrage. Oft genug kommt man gerade deshalb nicht weiter, weil man annimmt, dass der Fehler an einer bestimmten Stelle oder an einer bestimmten Ursache auf keinen Fall liegen kann, aber gerade genau dort liegt. Überlege, welche Codestellen und Fehlerquellen du gedanklich ausgeschlossen hast, und prüfe, wenn du nicht weiterkommst, genau diese (erneut und) unvoreingenommen.
Wenn alle Stricke reißen, probiere, ob du mit [Tutorial] Vertrackte Fehler durch Vergleich von echtem Projekt mit minimalem Testprojekt finden weiterkommst.
Und wenn das alles noch nicht hilft, liegt es vermutlich daran, dass dir noch ein paar Grundlagen der objektorientierten oder der Windows-Programmierung fehlen. Diese Grundlagen eignest du dir am besten an, indem du ein gutes Buch durcharbeitest (siehe auch [FAQ] Wie finde ich den Einstieg in C#? und Buchempfehlungen). Ein Forum kann keine Einführung in die (Windows-)Programmierung ersetzen und auch nicht leisten.
In diesem Sinne viel Erfolg!
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.
Fehler |
|