none
Auf Thread warten RRS feed

  • Frage

  • Hallo,

    beim start meines Programms wird ein Thread gestartet, der verschiedene Sachen macht. Ich möchte alle Buttons etc. solange Sperren, bis der Start Thread fertig ist, wie bekommt man das hin?

    Montag, 17. Oktober 2011 09:00

Antworten

  • Hallo as_1985,

    Das ist nicht ganz so kompliziert: Windows Forms-Controls sind per se nicht thread-safe. Wenn man gleichzeitig aus verschiedenen Threads auf sie zugreift und Zuweisungen vornimmt, könnte das Ergebnis sehr zu wünschen übrig lassen: Die UI könnte einfrieren (deadlock), man könnte Eigenschaften überschreiben, die bereits von anderen Threads geändert wurden und das Control in einem inkonsistenten Zustand versetzen.

    Das .NET Framework 1.0 und 1.1 warnten nicht von einem threadübergreifenden Vorgang und der Entwickler und noch viel schlimmer der Endanwender mußte mit den Konsequenzen leben. Ab .NET 2.0 wird eine InvalidOperationException geworfen, sobald man versucht aus einem Fremdthread unsynchronisiert auf ein Steuerelement zuzugreifen.

    Damit das nicht passiert, muß man Control.Invoke() aufrufen, eine der wenigen Control-Methoden die threadsicher sind. Diese Methode überprüft als erstes ob das entspr. Steuerelement auf dem aktuellen Thread erstellt wurde. Ist das der Fall, so wird einfach der Delegate, der an Invoke() übergeben wird aufgerufen. Andernfalls wird der Delegate in einer Callback-Liste gespeichert, das Handle des Controls wird ermittelt und eine Nachricht an die Fensterprozedur dieses Controls verschickt. Weil das Control die Aufgabe übernimmt den Aufruf weiterzuleiten wird es auch "marshalling control" genannt. Sobald das Control über die Nachrichtenschleife diese Nachricht empfängt befindet sich die Ausführung auf dem UI-Thread. Da über die Nachrichtenschleife eine implizite Synchronisierung stattfindet (es kann ja immer nur eine Nachricht auf einmal verarbeitet werden)  kann der Delegate aus der Callback-Liste gefahrlos aufgerufen und abgearbeitet werden.

    Und nun zu deinem Code.  Eine kleine Anmerkung vorab: Die Übergabe einer Referenz auf die Form in Thread.Start() ist nicht unbedingt erforderlich, wenn die Prozedur InBack auf "this" zugreifen kann. Willst Du nun auf Eigenschaften der Form aus diesem neuen Thread zugreifen, so kannst Du Control.Invoke() oder Control.BeginInvoke() dafür verwenden. Nehem wir an, dass Du den Titel des Formulars aus dem Hintergrundthread ändern möchtest. Es gibt dazu einen langen und einen kurzen Weg, die beide zum Ziel führen. Der lange: Du definierst eine Methode, die den Text ändert:

    private void SetFormText(string text) {
       textBox1.Text = text;
    }

    Nun brauchst Du noch einen Delegate mit der gleichen Signatur:

    public delegate void SetFormTextDelegate(string text);

    Auf dem Hintergrundthread dann:

    void InBack(object b)
    {

        //...

        SetFormTextDelegate myDelegate = new SetFormTextDelegate(SetFormText);
        MyBack.Invoke(myDelegate, new object[] {"Neuer Titel"});
    }

    Beim Aufruf von Invoke() aus dem Hintergrundthread wird der Delegate in die Callback-Liste eingetragen und an das Fenster der MyBack-Form wird eine Windowsnachricht verschickt, die nur auf dem UI-Thread verarbeitet werden kann. Sobald diese dort emfpangen wird, wird die Methode SetFormText über den Delegate aufgerufen und alles ist klar. Würde man stattdessen SetFormText direkt aus InBack aufrufen, würde man die InvalidOperationException (threadübergreifender Vorgang) erhalten.

    Der ganze vorherige Code kann über die Verwendung der Klasse Action (die auch ein Delegate ist) und eines lambda Ausdrucks mit einer anonymen Funktion verknappt werden zu:

    void InBack(object b)
    {

        //...

        MyBack.Invoke(new Action(() =>textBox1.Text = "Neuer Titel"));
    }

    Wir brauchen also nicht mehr separat einen Delgaten zu definieren und zu instanziieren und die Methode SetFormText fällt auch weg, da wir sie durch einen Inlinecodeblock (nach =>) ersetzt haben.

    Ich hoffe, mich einigermaßen verständlich ausgedrückt zu haben. Wenn nicht, frag bitte nach.

    Siehe auch: Anonyme Funktionen (C#-Programmierhandbuch)
    http://msdn.microsoft.com/de-de/library/bb882516.aspx

    Gruß
    Marcel

    • Als Antwort markiert as_1985 Dienstag, 18. Oktober 2011 06:28
    Montag, 17. Oktober 2011 14:24
    Moderator

Alle Antworten

  • Hallo as_1985,

    Wozu brauchst Du denn einen neuen Thread, wenn die UI während der Abarbeitung sowieso nicht reagieren soll? - Nun gut, Du wirst schon wissen, wozu.

    Normalerweise würde ich einen BackgroundWorker dazu verwenden, da sowohl Fehlerbehandlung als auch Fortschrittsmeldungen von Haus aus in der Komponente implementiert sind. Weil ich mir aber zum einen Tipparbeit ersparen will (es gibt ja genügend Beispiele dazu im Netz),  zum anderen ich auch andere Wege aufzeigen möchte, muss diesmal ThreadPool herhalten:

    using System;
    using System.Threading;
    using System.Windows.Forms;
    
    namespace WindowsFormsApplication1 {
        public partial class Form1 : Form {
            public Form1() { InitializeComponent(); }
    
            private void Form1_Load(object sender, EventArgs e) {
                ThreadPool.QueueUserWorkItem(DoWorkCallback);
            }
    
            private void DoWorkCallback(object state) {
                try {
                    this.Invoke(new Action(() => { foreach (Control control in this.Controls) { control.Enabled = false; } }));
                    this.Invoke(new Action(() => textBox1.Text = "Working..."));
                    Thread.Sleep(5000); // Arbeit simulieren
                    this.Invoke(new Action(() => textBox1.Text = "Done."));
                }
                catch { /* Ausnahmefehler behandeln*/ }
                finally { this.Invoke(new Action(() => { foreach (Control control in this.Controls) { control.Enabled = true; } })); }
            }
        }
    }
    
    

    Es ist wichtig, dass man nicht die ganze Form deaktiviert, sondern nur die Controls, sonst kann die Form gar nicht mehr reagieren. 

    Gruß
    Marcel

    Montag, 17. Oktober 2011 09:37
    Moderator
  • Das Problem ist, bei FormX_Shown löse ich einen Thread aus, der mir x Sachen erledigt. Wenn ich in Zwischenzeit auf einen meiner Buttonsklicke (Thread aus Form2_Shown noch nicht durchgelaufen) bekomme ich die Fehlermeldung Ungültiger threadübergreifender Vorgang.

    Ich wollte jetzt solange die Buttonssperren, aber ich glaube das ist nicht die sinnvollste Lösung.

    Montag, 17. Oktober 2011 12:34
  • Hallo as_1985,

    Das ist was ganz anderes.

    Du solltest - z.B., so wie ich's im obigen Code gemacht habe -  Control.Invoke() auf dem Hintergrundthread aufrufen. Damit wird der Aufruf auf dem Thread abgearbeitet, auf dem die Controls erstellt wurden (UI-Thread) .

    Siehe: Control.Invoke-Methode (Delegate, Object())
    http://msdn.microsoft.com/de-de/library/a1hetckb(v=VS.100).aspx

    Gruß
    Marcel

    Montag, 17. Oktober 2011 13:08
    Moderator
  • Das mit diesem Invoke verstehe ich nicht wirklich.

    Vielleicht könnt ihr/du mir helfen, ich poste mal etwas Quelltext (ohne Invoke).

    FormX

     

            public void Form_Shown(Object sender, EventArgs e)
            {
                object b = sender;
                CreateSomething nB = new CreateSomething();
                ParameterizedThreadStart pts = new ParameterizedThreadStart(nB.InBack);
                Thread thread = new Thread(pts);
                thread.Start(b);
            }
    

     

    CreateSomething Klasse

     

    public void InBack(object b)
            {       
                if (b is Form)
                {
                    Form MyBack = (Form)b;
                    MyBack.BackColor = Color.FromArgb(255, 255, 255);
                    [...]
                    // hier folgen noch ein paar Werte, denke nicht das diese relevant sind, auch for und if Abfragen.
                }
            }
    
    Die Meldung "Fehlermeldung Ungültiger threadübergreifender Vorgang..." kommt sobald ich einen Buttonklicke (der allerdings widerrum einen Thread startet) und diese InBack Funktion noch nicht komplett fertig ist.

     


    • Bearbeitet as_1985 Montag, 17. Oktober 2011 13:36
    Montag, 17. Oktober 2011 13:35
  • Hallo as_1985,

    Das ist nicht ganz so kompliziert: Windows Forms-Controls sind per se nicht thread-safe. Wenn man gleichzeitig aus verschiedenen Threads auf sie zugreift und Zuweisungen vornimmt, könnte das Ergebnis sehr zu wünschen übrig lassen: Die UI könnte einfrieren (deadlock), man könnte Eigenschaften überschreiben, die bereits von anderen Threads geändert wurden und das Control in einem inkonsistenten Zustand versetzen.

    Das .NET Framework 1.0 und 1.1 warnten nicht von einem threadübergreifenden Vorgang und der Entwickler und noch viel schlimmer der Endanwender mußte mit den Konsequenzen leben. Ab .NET 2.0 wird eine InvalidOperationException geworfen, sobald man versucht aus einem Fremdthread unsynchronisiert auf ein Steuerelement zuzugreifen.

    Damit das nicht passiert, muß man Control.Invoke() aufrufen, eine der wenigen Control-Methoden die threadsicher sind. Diese Methode überprüft als erstes ob das entspr. Steuerelement auf dem aktuellen Thread erstellt wurde. Ist das der Fall, so wird einfach der Delegate, der an Invoke() übergeben wird aufgerufen. Andernfalls wird der Delegate in einer Callback-Liste gespeichert, das Handle des Controls wird ermittelt und eine Nachricht an die Fensterprozedur dieses Controls verschickt. Weil das Control die Aufgabe übernimmt den Aufruf weiterzuleiten wird es auch "marshalling control" genannt. Sobald das Control über die Nachrichtenschleife diese Nachricht empfängt befindet sich die Ausführung auf dem UI-Thread. Da über die Nachrichtenschleife eine implizite Synchronisierung stattfindet (es kann ja immer nur eine Nachricht auf einmal verarbeitet werden)  kann der Delegate aus der Callback-Liste gefahrlos aufgerufen und abgearbeitet werden.

    Und nun zu deinem Code.  Eine kleine Anmerkung vorab: Die Übergabe einer Referenz auf die Form in Thread.Start() ist nicht unbedingt erforderlich, wenn die Prozedur InBack auf "this" zugreifen kann. Willst Du nun auf Eigenschaften der Form aus diesem neuen Thread zugreifen, so kannst Du Control.Invoke() oder Control.BeginInvoke() dafür verwenden. Nehem wir an, dass Du den Titel des Formulars aus dem Hintergrundthread ändern möchtest. Es gibt dazu einen langen und einen kurzen Weg, die beide zum Ziel führen. Der lange: Du definierst eine Methode, die den Text ändert:

    private void SetFormText(string text) {
       textBox1.Text = text;
    }

    Nun brauchst Du noch einen Delegate mit der gleichen Signatur:

    public delegate void SetFormTextDelegate(string text);

    Auf dem Hintergrundthread dann:

    void InBack(object b)
    {

        //...

        SetFormTextDelegate myDelegate = new SetFormTextDelegate(SetFormText);
        MyBack.Invoke(myDelegate, new object[] {"Neuer Titel"});
    }

    Beim Aufruf von Invoke() aus dem Hintergrundthread wird der Delegate in die Callback-Liste eingetragen und an das Fenster der MyBack-Form wird eine Windowsnachricht verschickt, die nur auf dem UI-Thread verarbeitet werden kann. Sobald diese dort emfpangen wird, wird die Methode SetFormText über den Delegate aufgerufen und alles ist klar. Würde man stattdessen SetFormText direkt aus InBack aufrufen, würde man die InvalidOperationException (threadübergreifender Vorgang) erhalten.

    Der ganze vorherige Code kann über die Verwendung der Klasse Action (die auch ein Delegate ist) und eines lambda Ausdrucks mit einer anonymen Funktion verknappt werden zu:

    void InBack(object b)
    {

        //...

        MyBack.Invoke(new Action(() =>textBox1.Text = "Neuer Titel"));
    }

    Wir brauchen also nicht mehr separat einen Delgaten zu definieren und zu instanziieren und die Methode SetFormText fällt auch weg, da wir sie durch einen Inlinecodeblock (nach =>) ersetzt haben.

    Ich hoffe, mich einigermaßen verständlich ausgedrückt zu haben. Wenn nicht, frag bitte nach.

    Siehe auch: Anonyme Funktionen (C#-Programmierhandbuch)
    http://msdn.microsoft.com/de-de/library/bb882516.aspx

    Gruß
    Marcel

    • Als Antwort markiert as_1985 Dienstag, 18. Oktober 2011 06:28
    Montag, 17. Oktober 2011 14:24
    Moderator
  • Danke für deine ausführliche Hilfestellung, echt Toll!!

    Ich muss die Referenz übergeben, da this nicht das gewünschte Ergebnis liefert. Habe jetzt deine Lösung mit new Action getestet und führte zum gewünschten Ergebnis, echt super!

    Dienstag, 18. Oktober 2011 06:27