Laden...

Threading in Fenstern: korrekte Vorgehensweise beim Schließen der Form

Erstellt von Fabian E. vor 13 Jahren Letzter Beitrag vor 13 Jahren 4.121 Views
F
Fabian E. Themenstarter:in
258 Beiträge seit 2008
vor 13 Jahren
Threading in Fenstern: korrekte Vorgehensweise beim Schließen der Form

Hallo zusammen,

ich habe eine Frage zum Threading in Fenster und zum Thread übergreifenden Zugriff auf meine Controls.

Bitte jetzt keine Links zu den bekannten Beiträgen posten, ich glaube das hier ist etwas komplizierter...

Ich habe folgende Situation: Mein erster Thread ist natürlich der GUI-Thread. Der Zweite ist ein Thread der seriellen Schnisstelle.
Und zwar scheint ein Thread im Hintergrund zu laufen, der das Event

SerialPort.DataReceived

schmeißt.

Nun muss ich aus diesem Thread-Kontext auf meine Controls zugreifen. (Das Event wird noch ein bisschen weiter gereicht, der Kontext ändert sich aber nicht)

Der Code sieht wie folgt aus:


        void RP6PortWrapper_DataAvailable(string obj) //Weitergeleitetes Event von SerialPort.DataReceived
        {
            if (!tbxDataReceived.IsDisposed && tbxDataReceived.IsHandleCreated)
            {
                if (tbxDataReceived.InvokeRequired)
                {
                    tbxDataReceived.Invoke(new Action(() => AppendNewText(obj))); //Hier kommt der Fehler
                }
                else
                {
                    AppendNewText(obj);
                }
            }
        }

        private void AppendNewText(string text)
        {
            if (tbxDataReceived.Text.Length > 5000)
            {
                string tmp = tbxDataReceived.Text.ToString();
                tbxDataReceived.Clear();
                tbxDataReceived.AppendText(tmp.Substring(2500));
            }
            tbxDataReceived.AppendText(text);
        }

Das klappt soweit auch schon ziemlich gut, keine Fehler oder sonstiges.
Die Probleme treten erst beim Schließen der Form auf.
Dann wird des Öfteren (allerdings nicht reproduzierbar) folgende Exception geworfen:

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

InnerException:

StackTrace: bei System.Windows.Forms.Control.WaitForWaitHandle(WaitHandle waitHandle)
bei System.Windows.Forms.Control.MarshaledInvoke(Control caller, Delegate method, Object[] args, Boolean synchronous)
bei System.Windows.Forms.Control.Invoke(Delegate method, Object[] args)
bei System.Windows.Forms.Control.Invoke(Delegate method)
bei RP6Remotrol.Forms.GeneralForms.TerminalForm.RP6PortWrapper_DataAvailable(String obj) in H:\RP6\RP6 Remotrol\C#\RP6Remotrol\Forms\GeneralForms\TerminalForm.cs:Zeile 29.
bei System.Action`1.Invoke(T obj)
bei RP6Remotrol.RP6Library.RP6PortWrapper.Port_DataReceived() in H:\RP6\RP6 Remotrol\C#\RP6Remotrol\RP6Library\RP6PortWrapper.cs:Zeile 166.
bei RP6Remotrol.RP6Library.RP6SerialPort.port_DataReceived(Object sender, SerialDataReceivedEventArgs e) in H:\RP6\RP6 Remotrol\C#\RP6Remotrol\RP6Library\RP6Port.cs:Zeile 32.
bei System.IO.Ports.SerialPort.CatchReceivedEvents(Object src, SerialDataReceivedEventArgs e)
bei System.IO.Ports.SerialStream.EventLoopRunner.CallReceiveEvents(Object state)
bei System.Threading.QueueUserWorkItemCallback.WaitCallback_Context(Object state)
bei System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean ignoreSyncCtx)
bei System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem()
bei System.Threading.ThreadPoolWorkQueue.Dispatch()
bei System.Threading._ThreadPoolWaitCallback.PerformWaitCallback()

Methode: Void WaitForWaitHandle(System.Threading.WaitHandle) (Das ist die Ausgabe meines Logs)

Wie man in meinem Code sieht, habe ich schon versucht, das Problem mit .IsHandleCreated zu umschiffen, leider ohne Erfolg...
Zusätzlich habe ich versucht das Event beim Schließen abzumelden:


        private void TerminalForm_FormClosing(object sender,  FormClosingEventArgs e)
        {
            RP6PortWrapper.DataAvailable -= RP6PortWrapper_DataAvailable;
        }

Auch das führt nicht zum gewünschten Ergebnis...
Ich bin jetzt etwas ratlos...

Das Event dürfte nach dem Dispose der Textbox nicht mehr aufgerufen werden,
und selbst wenn, dann dürfte das Invoke nicht mehr aufgerufen werden...
Trotzdem passiert all das...

Kann mir da irgendjemand helfen?

795 Beiträge seit 2006
vor 13 Jahren

Hi!

Probier mal, ob es etwas hilft, hier if (!tbxDataReceived.IsDisposed && tbxDataReceived.IsHandleCreated) noch ein !this.IsDispose && this.IsHandleCreated (also für das Handle der Form) einzubauen.

Gruß, Christian.

`There are 10 types of people in the world: Those, who think they understand the binary system Those who don't even have heard about it And those who understand "Every base is base 10"`
F
Fabian E. Themenstarter:in
258 Beiträge seit 2008
vor 13 Jahren

Nein, leider keinerlei Veränderung 😦

Gelöschter Account
vor 13 Jahren

offensichtlich bist du immer noch angemeldet. Prüfe das mal nach, warum...

F
Fabian E. Themenstarter:in
258 Beiträge seit 2008
vor 13 Jahren

Also ich verstehe es wirklich nicht...

Wenn ich mir den Fehler mal von der IDE auffangen lasse und mir dann die Werte von IsDisposed und IsHandleCreated ansehe, dann passen die. Disposed -ja Handle - nein.
Daher dann natürlich auch der Fehler...
Das ignoriert mein Programm aber einfach gnadenlos...
Wie geht das?
Im Anhang mal ein Bild davon...
Link entfernt

Hinweis von michlG vor 13 Jahren

Bitte die Bilder als Dateianhang hinzufügen und nicht per imageshack oder Ähnlichem...

Gelöschter Account
vor 13 Jahren

setze in den debugoptionen, das alle threads angehalten werden, wenn du in ein break läufst, setze ein Breakpoint direkt nach der ersten if mit einer Condition auf IsDisposed und wiederhole den test.

F
Fabian E. Themenstarter:in
258 Beiträge seit 2008
vor 13 Jahren

Okay, das ist interessant...
Ergebnis ist genau das Selbe, der Fehler tritt manchmal auf...
Der Breakpoint wird aber niemals aktiviert.
Das bedeutet also der GUI-Thread disposed manchmal direkt nach der Abfrage auf IsDisposed.
Daher auch der Yeti-Effekt, ist halt eine Timing-Sache.

Nun die Preisfrage, was kann ich dagegen tun?
Ich hatte es schonmal mit einem lock probiert, das half aber auch nichts...
Daher war ich eigentlich auch wieder weg von der Timing Geschichte...

Im Moment habe ich ein leeres try catch Konstrukt drum rum.
Ist aber natürlich langsam und schön ist auch anders...

5.299 Beiträge seit 2008
vor 13 Jahren

vlt kannst du eine volatile Variable setzen, die threadübergreifend aussagt, ob Daten noch verarbeitet werden können.

Der frühe Apfel fängt den Wurm.

4.221 Beiträge seit 2005
vor 13 Jahren

Der Invoke im void RP6PortWrapper_DataAvailable(string obj) kommt nicht in den UI-Thread rein da das Form gerade geschlossen wird.

Probier mal folgendes:

Als erstes im void RP6PortWrapper_DataAvailable(string obj) eine volatile bool setzen um zu signalisieren dass Du schon im DataAvailable drin bist.... dieses Flag nach dem Invoke wieder clearen.

Im TerminalForm_FormClosing den Event abhängen und dann das volatile-Flag abfragen. Falls es gesetzt ist machst du ein e.Cancel=true (damit der DataAvailalble noch verarbeitet werden kann) und dann ein BeginInvoke auf this.Close.

Müsste gemäss meinem Verständnis das Problem lösen.

Gruss
Programmierhans

Früher war ich unentschlossen, heute bin ich mir da nicht mehr so sicher...

F
Fabian E. Themenstarter:in
258 Beiträge seit 2008
vor 13 Jahren

vlt kannst du eine volatile Variable setzen, die threadübergreifend aussagt, ob Daten noch verarbeitet werden können.

Als erstes im void RP6PortWrapper_DataAvailable(string obj) eine volatile bool setzen um zu signalisieren dass Du schon im DataAvailable drin bist.... dieses Flag nach dem Invoke wieder clearen.

Im TerminalForm_FormClosing den Event abhängen und dann das volatile-Flag abfragen. Falls es gesetzt ist machst du ein e.Cancel=true (damit der DataAvailalble noch verarbeitet werden kann) und dann ein BeginInvoke auf this.Close.

Ihr seid meine Helden! Genauso funktioniert es! =)
Zumindest ist der Fehler bis jetzt nicht mehr aufgetreten und sonst eigentlich mindestens jedes zweite Mal!
Vielen Dank! =)

49.485 Beiträge seit 2005
vor 13 Jahren

Hallo Programmierhans,

Müsste gemäss meinem Verständnis das Problem lösen.

nein, leider nicht in allen Fällen, nämlich dann nicht, wenn der Thread-Wechsel genau in dem Moment erfolgt, wo der EventHander schon aufgerufen, aber die Variable noch nicht gesetzt wurde. Das kann selbst dann passieren, wenn das Setzen der Variable die erste Anweisung in dem EventHandler ist. Ist zwar recht unwahrscheinlich, aber dennoch möglich und wenn ein solcher Fehler auftritt, ist er gerade aufgrund seiner schlechten Reproduzierbarkeit extrem schwer zu finden.

Ich vermute, man braucht zwei volatile boolsche Variablen und zusätzlich mit lock gesperrte Code-Abschnitte sowohl im EventHandler als auch um das Close herum.

herbivore

4.221 Beiträge seit 2005
vor 13 Jahren

@herbivore

Hmm den würde ich per Try-Catch abfangen... dies, da die Wahrscheinlichkeit doch sehr klein ist (ein volatile-bool ist atomar).... aber Grundsätzlich hast Du natürlich recht.

Gruss
Programmierhans

Früher war ich unentschlossen, heute bin ich mir da nicht mehr so sicher...

U
1.688 Beiträge seit 2007
vor 13 Jahren

Hallo,

ich habe ein ähnliches Problem gelöst, indem ich im Form_Closing Handler die serielle Schnittstelle mit Dispose freigegeben habe. Dieser Aufruf blockiert, solange der DataReceived-Handler "unterwegs" ist.
Dies würde dann ggf. zu einem Deadlock führen, wenn DataReceived mit Invoke auf den GUI-Thread wechseln will - also sollte man BeginInvoke nehmen (was natürlich für den Lesethread der seriellen Schnittstelle sowieso besser ist) oder SynchronizationContext.Post.
Wird das entsprechende Ereignis doch noch ausgelöst, kann man im GUI-Thread zuverlässig IsDisposed verwenden.