Threadübergreifende Operation ungültig: Steuerelement, auf das von einem anderen Thread als dem Thread zugegriffen wird, für den es erstellt wurde

584

Ich habe ein Szenario. (Windows Forms, C #, .NET)

  1. Es gibt ein Hauptformular, in dem einige Benutzersteuerelemente gehostet werden.
  2. Das Benutzersteuerelement führt einige UserControl_Loadumfangreiche Datenoperationen aus , sodass die Benutzeroberfläche für die Dauer der Ausführung der Lademethode nicht mehr reagiert, wenn ich die Methode direkt aufrufe.
  3. Um dies zu überwinden, lade ich Daten in einen anderen Thread (versuche, vorhandenen Code so wenig wie möglich zu ändern).
  4. Ich habe einen Hintergrund-Worker-Thread verwendet, der die Daten lädt und die Anwendung benachrichtigt, wenn sie fertig ist.
  5. Jetzt kam ein echtes Problem. Die gesamte Benutzeroberfläche (Hauptformular und seine untergeordneten Benutzersteuerelemente) wurde im primären Hauptthread erstellt. In der LOAD-Methode der Benutzersteuerung rufe ich Daten basierend auf den Werten eines Steuerelements (wie z. B. eines Textfelds) in userControl ab.

Der Pseudocode würde folgendermaßen aussehen:

CODE 1

UserContrl1_LoadDataMethod()
{
    if (textbox1.text == "MyName") // This gives exception
    {
        //Load data corresponding to "MyName".
        //Populate a globale variable List<string> which will be binded to grid at some later stage.
    }
}

Die Ausnahme war

Threadübergreifende Operation ungültig: Steuerelement, auf das von einem anderen Thread als dem Thread zugegriffen wird, für den es erstellt wurde.

Um mehr darüber zu erfahren, habe ich ein bisschen gegoogelt und ein Vorschlag kam wie der folgende Code

CODE 2

UserContrl1_LoadDataMethod()
{
    if (InvokeRequired) // Line #1
    {
        this.Invoke(new MethodInvoker(UserContrl1_LoadDataMethod));
        return;
    }

    if (textbox1.text == "MyName") // Now it wont give an exception
    {
    //Load data correspondin to "MyName"
        //Populate a globale variable List<string> which will be binded to grid at some later stage
    }
}

ABER ABER ABER ... es scheint, ich bin wieder auf dem ersten Platz. Die Anwendung reagiert erneut nicht mehr. Es scheint an der Ausführung von Zeile 1 zu liegen, wenn Bedingung. Die Ladeaufgabe wird wieder vom übergeordneten Thread ausgeführt und nicht vom dritten, den ich erzeugt habe.

Ich weiß nicht, ob ich das richtig oder falsch wahrgenommen habe. Ich bin neu im Threading.

Wie löse ich das und was bewirkt die Ausführung von Zeile 1, wenn blockiert?

Die Situation ist dies : Ich möchte Daten basierend auf dem Wert eines Steuerelements in eine globale Variable laden. Ich möchte den Wert eines Steuerelements im untergeordneten Thread nicht ändern. Ich werde es nie von einem Kinder-Thread aus tun.

Greifen Sie also nur auf den Wert zu, damit die entsprechenden Daten aus der Datenbank abgerufen werden können.

Prerak K.
quelle
Für meine spezielle Instanz dieses Fehlers habe ich festgestellt, dass die Problemumgehung darin besteht, einen BackgroundWorker für das Formular zu verwenden, um die datenintensiven Teile des Codes zu verarbeiten. (dh den gesamten Problemcode in die Methode backgroundWorker1_DoWork () einfügen und über backgroundWorker1.RunWorkerAsync () aufrufen) ... Diese beiden Quellen haben mich in die richtige Richtung gelenkt : stackoverflow.com/questions/4806742/… youtube.com/ watch? v = MLrrbG6V1zM
Giollia

Antworten:

433

Wie pro Prerak K-Update Kommentar (seit gelöscht):

Ich glaube, ich habe die Frage nicht richtig gestellt.

Die Situation ist folgende: Ich möchte Daten basierend auf dem Wert eines Steuerelements in eine globale Variable laden. Ich möchte den Wert eines Steuerelements im untergeordneten Thread nicht ändern. Ich werde es nie von einem Kinder-Thread aus tun.

Greifen Sie also nur auf den Wert zu, damit entsprechende Daten aus der Datenbank abgerufen werden können.

Die gewünschte Lösung sollte dann folgendermaßen aussehen:

UserContrl1_LOadDataMethod()
{
    string name = "";
    if(textbox1.InvokeRequired)
    {
        textbox1.Invoke(new MethodInvoker(delegate { name = textbox1.text; }));
    }
    if(name == "MyName")
    {
        // do whatever
    }
}

Führen Sie Ihre ernsthafte Verarbeitung im separaten Thread durch, bevor Sie versuchen, zum Thread des Steuerelements zurückzukehren. Zum Beispiel:

UserContrl1_LOadDataMethod()
{
    if(textbox1.text=="MyName") //<<======Now it wont give exception**
    {
        //Load data correspondin to "MyName"
        //Populate a globale variable List<string> which will be
        //bound to grid at some later stage
        if(InvokeRequired)
        {
            // after we've done all the processing, 
            this.Invoke(new MethodInvoker(delegate {
                // load the control with the appropriate data
            }));
            return;
        }
    }
}
Jeff Hubbard
quelle
1
Es ist schon eine Weile her, dass ich C # -Programmierung durchgeführt habe, aber basierend auf dem MSDN-Artikel und meinem lückenhaften Wissen sieht es so aus.
Jeff Hubbard
1
Der Unterschied besteht darin, dass BeginInvoke () asynchron ist, während Invoke () synchron ausgeführt wird. stackoverflow.com/questions/229554/…
frzsombor
178

Threading-Modell in der Benutzeroberfläche

Bitte lesen Sie das Threading-Modell in UI-Anwendungen, um grundlegende Konzepte zu verstehen. Der Link navigiert zu einer Seite, die das WPF-Threading-Modell beschreibt. Windows Forms verwendet jedoch dieselbe Idee.

Der UI-Thread

  • Es gibt nur einen Thread (UI-Thread), der auf System.Windows.Forms.Control und seine Unterklassenmitglieder zugreifen darf .
  • Der Versuch, von einem anderen Thread als dem UI-Thread auf ein Mitglied von System.Windows.Forms.Control zuzugreifen, führt zu einer threadübergreifenden Ausnahme.
  • Da es nur einen Thread gibt, werden alle UI-Vorgänge als Arbeitselemente in diesen Thread eingereiht:

Geben Sie hier die Bildbeschreibung ein

Geben Sie hier die Bildbeschreibung ein

BeginInvoke- und Invoke-Methoden

  • Der Rechenaufwand für die aufgerufene Methode sollte gering sein, ebenso wie der Rechenaufwand für Event-Handler-Methoden, da dort der UI-Thread verwendet wird - derselbe, der für die Verarbeitung von Benutzereingaben verantwortlich ist. Unabhängig davon, ob dies System.Windows.Forms.Control.Invoke oder System.Windows.Forms.Control.BeginInvoke ist .
  • Verwenden Sie für die Berechnung teurer Operationen immer einen separaten Thread. Seit .NET 2.0 ist BackgroundWorker darauf ausgerichtet, teure Vorgänge in Windows Forms auszuführen. Jedoch in neuen Lösungen sollten Sie die Asynchron-await Muster verwenden , wie hier .
  • Verwenden Sie die Methoden System.Windows.Forms.Control.Invoke oder System.Windows.Forms.Control.BeginInvoke nur, um eine Benutzeroberfläche zu aktualisieren. Wenn Sie sie für umfangreiche Berechnungen verwenden, blockiert Ihre Anwendung:

Geben Sie hier die Bildbeschreibung ein

Aufrufen

Geben Sie hier die Bildbeschreibung ein

BeginInvoke

Geben Sie hier die Bildbeschreibung ein

Codelösung

Antworten auf Frage lesen Wie aktualisiere ich die GUI von einem anderen Thread in C #? . Für C # 5.0 und .NET 4.5 die empfohlene Lösung ist hier .

Ryszard Dżegan
quelle
72

Sie möchten nur Invokeoder BeginInvokefür das Minimum an Arbeit verwenden, die zum Ändern der Benutzeroberfläche erforderlich ist. Ihre "schwere" Methode sollte auf einem anderen Thread ausgeführt werden (z. B. über BackgroundWorker), aber dann Control.Invoke/ Control.BeginInvokenur zum Aktualisieren der Benutzeroberfläche verwenden. Auf diese Weise kann Ihr UI-Thread UI-Ereignisse usw. verarbeiten.

In meinem Threading-Artikel finden Sie ein WinForms-Beispiel - obwohl der Artikel bereits geschrieben wurde, bevor er BackgroundWorkerin die Szene kam, und ich fürchte, ich habe ihn in dieser Hinsicht nicht aktualisiert. BackgroundWorkervereinfacht lediglich den Rückruf ein wenig.

Jon Skeet
quelle
hier in meinem Zustand. Ich ändere nicht einmal die Benutzeroberfläche. Ich greife nur über den untergeordneten Thread auf die aktuellen Werte zu. Jeder Vorschlag hw zu implementieren
Prerak K
1
Sie müssen immer noch zum UI-Thread wechseln, auch um auf Eigenschaften zuzugreifen. Wenn Ihre Methode erst fortgesetzt werden kann, wenn auf den Wert zugegriffen wird, können Sie einen Delegaten verwenden, der den Wert zurückgibt. Aber ja, gehen Sie über den UI-Thread.
Jon Skeet
Hallo Jon, ich glaube du führst mich in die richtige Richtung. Ja, ich brauche den Wert ohne ihn. Ich kann nicht weiter vorgehen. Könnten Sie bitte erläutern, ob Sie einen Delegaten verwenden, der einen Wert zurückgibt? Vielen Dank
Prerak K
1
Verwenden Sie einen Delegaten wie Func <string>: string text = textbox1.Invoke ((Func <string>) () => textbox1.Text); (Vorausgesetzt, Sie verwenden C # 3.0 - Sie könnten auch eine anonyme Methode verwenden.)
Jon Skeet
45

Ich weiß, dass es jetzt zu spät ist. Aber auch heute noch, wenn Sie Probleme beim Zugriff auf Cross-Thread-Steuerelemente haben? Dies ist die kürzeste Antwort bis heute: P.

Invoke(new Action(() =>
                {
                    label1.Text = "WooHoo!!!";
                }));

So greife ich von einem Thread aus auf ein Formularsteuerelement zu.

Bravo
quelle
1
Das gibt mir Invoke or BeginInvoke cannot be called on a control until the window handle has been created. Ich habe es hier
Rupweb
42

Ich hatte dieses Problem mit dem FileSystemWatcherund stellte fest, dass der folgende Code das Problem löste:

fsw.SynchronizingObject = this

Das Steuerelement verwendet dann das aktuelle Formularobjekt, um die Ereignisse zu verarbeiten, und befindet sich daher im selben Thread.

Peter C.
quelle
2
Das hat meinen Speck gerettet. In VB.NET habe ich.SynchronizingObject = Me
Codierung Codierung
20

Ich finde den Check-and-Invoke-Code, der in allen Methoden, die sich auf Formulare beziehen, verstreut sein muss, viel zu ausführlich und unnötig. Hier ist eine einfache Erweiterungsmethode, mit der Sie sie vollständig beseitigen können:

public static class Extensions
{
    public static void Invoke<TControlType>(this TControlType control, Action<TControlType> del) 
        where TControlType : Control
        {
            if (control.InvokeRequired)
                control.Invoke(new Action(() => del(control)));
            else
                del(control);
    }
}

Und dann können Sie einfach Folgendes tun:

textbox1.Invoke(t => t.Text = "A");

Kein Herumspielen mehr - einfach.

Rob
quelle
Was ist 't' hier
Rawat
@Rawat tin diesem Fall wird textbox1- es wird als Argument übergeben
Rob
17

Steuerelemente in .NET sind im Allgemeinen nicht threadsicher. Das bedeutet, dass Sie nicht von einem anderen Thread als dem, auf dem es sich befindet, auf ein Steuerelement zugreifen sollten. Um dies zu umgehen , müssen Sie aufrufen das Steuerelement , was in Ihrem zweiten Beispiel versucht wird.

In Ihrem Fall müssen Sie jedoch nur die lang laufende Methode an den Hauptthread zurückgeben. Natürlich ist das nicht wirklich das, was Sie tun möchten. Sie müssen dies ein wenig überdenken, damit Sie im Haupt-Thread nur hier und da eine schnelle Eigenschaft festlegen.

Joel Coehoorn
quelle
10

Ein neuer Look mit Async / Await und Rückrufen. Sie benötigen nur eine Codezeile, wenn Sie die Erweiterungsmethode in Ihrem Projekt beibehalten.

/// <summary>
/// A new way to use Tasks for Asynchronous calls
/// </summary>
public class Example
{
    /// <summary>
    /// No more delegates, background workers etc. just one line of code as shown below
    /// Note it is dependent on the XTask class shown next.
    /// </summary>
    public async void ExampleMethod()
    {
        //Still on GUI/Original Thread here
        //Do your updates before the next line of code
        await XTask.RunAsync(() =>
        {
            //Running an asynchronous task here
            //Cannot update GUI Thread here, but can do lots of work
        });
        //Can update GUI/Original thread on this line
    }
}

/// <summary>
/// A class containing extension methods for the Task class 
/// Put this file in folder named Extensions
/// Use prefix of X for the class it Extends
/// </summary>
public static class XTask
{
    /// <summary>
    /// RunAsync is an extension method that encapsulates the Task.Run using a callback
    /// </summary>
    /// <param name="Code">The caller is called back on the new Task (on a different thread)</param>
    /// <returns></returns>
    public async static Task RunAsync(Action Code)
    {
        await Task.Run(() =>
        {
            Code();
        });
        return;
    }
}

Sie können der Erweiterungsmethode andere Dinge hinzufügen, z. B. das Umschließen in eine Try / Catch-Anweisung, damit der Aufrufer ihm mitteilen kann, welcher Typ nach Abschluss zurückgegeben werden soll. Dies ist ein Ausnahmerückruf an den Aufrufer:

Hinzufügen von Try Catch, Auto Exception Logging und CallBack

    /// <summary>
    /// Run Async
    /// </summary>
    /// <typeparam name="T">The type to return</typeparam>
    /// <param name="Code">The callback to the code</param>
    /// <param name="Error">The handled and logged exception if one occurs</param>
    /// <returns>The type expected as a competed task</returns>

    public async static Task<T> RunAsync<T>(Func<string,T> Code, Action<Exception> Error)
    {
       var done =  await Task<T>.Run(() =>
        {
            T result = default(T);
            try
            {
               result = Code("Code Here");
            }
            catch (Exception ex)
            {
                Console.WriteLine("Unhandled Exception: " + ex.Message);
                Console.WriteLine(ex.StackTrace);
                Error(ex);
            }
            return result;

        });
        return done;
    }
    public async void HowToUse()
    {
       //We now inject the type we want the async routine to return!
       var result =  await RunAsync<bool>((code) => {
           //write code here, all exceptions are logged via the wrapped try catch.
           //return what is needed
           return someBoolValue;
       }, 
       error => {

          //exceptions are already handled but are sent back here for further processing
       });
        if (result)
        {
            //we can now process the result because the code above awaited for the completion before
            //moving to this statement
        }
    }
John Peters
quelle
10

Dies ist nicht die empfohlene Methode, um diesen Fehler zu beheben. Sie können ihn jedoch schnell unterdrücken. Er erledigt die Aufgabe. Ich bevorzuge dies für Prototypen oder Demos. hinzufügen

CheckForIllegalCrossThreadCalls = false

im Form1()Konstruktor.

Özgür
quelle
9

Befolgen Sie die meiner Meinung nach einfachste Methode, um Objekte aus einem anderen Thread zu ändern:

using System.Threading.Tasks;
using System.Threading;

namespace TESTE
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            Action<string> DelegateTeste_ModifyText = THREAD_MOD;
            Invoke(DelegateTeste_ModifyText, "MODIFY BY THREAD");
        }

        private void THREAD_MOD(string teste)
        {
            textBox1.Text = teste;
        }
    }
}
Vanderley Maia
quelle
Einfach! Vielen Dank .
Ali Esmaeili
7

Ich habe dies beim Programmieren eines iOS-Phone-Monotouch-App-Controllers in einem Visual Studio-Winforms-Prototypprojekt außerhalb von xamarin stuidio festgestellt. Ich wollte lieber so viel wie möglich in VS über Xamarin Studio programmieren und wollte, dass der Controller vollständig vom Telefon-Framework entkoppelt wird. Auf diese Weise wäre die Implementierung für andere Frameworks wie Android und Windows Phone für zukünftige Anwendungen viel einfacher.

Ich wollte eine Lösung, bei der die GUI auf Ereignisse reagieren kann, ohne sich mit dem Cross-Threading-Schaltcode hinter jedem Tastenklick befassen zu müssen. Lassen Sie den Klassencontroller grundsätzlich damit umgehen, um den Clientcode einfach zu halten. Sie könnten möglicherweise viele Ereignisse auf der GUI haben, bei denen es sauberer wäre, wenn Sie es an einer Stelle in der Klasse handhaben könnten. Ich bin kein Multi-Theading-Experte. Lassen Sie mich wissen, wenn dies fehlerhaft ist.

public partial class Form1 : Form
{
    private ExampleController.MyController controller;

    public Form1()
    {          
        InitializeComponent();
        controller = new ExampleController.MyController((ISynchronizeInvoke) this);
        controller.Finished += controller_Finished;
    }

    void controller_Finished(string returnValue)
    {
        label1.Text = returnValue; 
    }

    private void button1_Click(object sender, EventArgs e)
    {
        controller.SubmitTask("Do It");
    }
}

Dem GUI-Formular ist nicht bekannt, dass der Controller asynchrone Aufgaben ausführt.

public delegate void FinishedTasksHandler(string returnValue);

public class MyController
{
    private ISynchronizeInvoke _syn; 
    public MyController(ISynchronizeInvoke syn) {  _syn = syn; } 
    public event FinishedTasksHandler Finished; 

    public void SubmitTask(string someValue)
    {
        System.Threading.ThreadPool.QueueUserWorkItem(state => submitTask(someValue));
    }

    private void submitTask(string someValue)
    {
        someValue = someValue + " " + DateTime.Now.ToString();
        System.Threading.Thread.Sleep(5000);
//Finished(someValue); This causes cross threading error if called like this.

        if (Finished != null)
        {
            if (_syn.InvokeRequired)
            {
                _syn.Invoke(Finished, new object[] { someValue });
            }
            else
            {
                Finished(someValue);
            }
        }
    }
}
RandallTo
quelle
6

Hier ist eine alternative Möglichkeit, wenn das Objekt, mit dem Sie arbeiten, nicht vorhanden ist

(InvokeRequired)

Dies ist nützlich, wenn Sie mit dem Hauptformular in einer anderen Klasse als dem Hauptformular mit einem Objekt arbeiten, das sich im Hauptformular befindet, aber nicht über InvokeRequired verfügt

delegate void updateMainFormObject(FormObjectType objectWithoutInvoke, string text);

private void updateFormObjectType(FormObjectType objectWithoutInvoke, string text)
{
    MainForm.Invoke(new updateMainFormObject(UpdateObject), objectWithoutInvoke, text);
}

public void UpdateObject(ToolStripStatusLabel objectWithoutInvoke, string text)
{
    objectWithoutInvoke.Text = text;
}

Es funktioniert genauso wie oben, aber es ist ein anderer Ansatz, wenn Sie kein Objekt mit invokerequired haben, aber Zugriff auf die MainForm haben

Ashitakalax
quelle
5

In Anlehnung an die vorherigen Antworten, jedoch eine sehr kurze Ergänzung, mit der alle Control-Eigenschaften verwendet werden können, ohne dass eine Ausnahme für den Aufruf eines Cross-Threads vorliegt.

Hilfsmethode

/// <summary>
/// Helper method to determin if invoke required, if so will rerun method on correct thread.
/// if not do nothing.
/// </summary>
/// <param name="c">Control that might require invoking</param>
/// <param name="a">action to preform on control thread if so.</param>
/// <returns>true if invoke required</returns>
public bool ControlInvokeRequired(Control c, Action a)
{
    if (c.InvokeRequired) c.Invoke(new MethodInvoker(delegate
    {
        a();
    }));
    else return false;

    return true;
}

Beispielnutzung

// usage on textbox
public void UpdateTextBox1(String text)
{
    //Check if invoke requied if so return - as i will be recalled in correct thread
    if (ControlInvokeRequired(textBox1, () => UpdateTextBox1(text))) return;
    textBox1.Text = ellapsed;
}

//Or any control
public void UpdateControl(Color c, String s)
{
    //Check if invoke requied if so return - as i will be recalled in correct thread
    if (ControlInvokeRequired(myControl, () => UpdateControl(c, s))) return;
    myControl.Text = s;
    myControl.BackColor = c;
}
Mike
quelle
5
this.Invoke(new MethodInvoker(delegate
            {
                //your code here;
            }));
Hamid Jolany
quelle
5

Zum Beispiel, um den Text von einem Steuerelement des UI-Threads abzurufen:

Private Delegate Function GetControlTextInvoker(ByVal ctl As Control) As String

Private Function GetControlText(ByVal ctl As Control) As String
    Dim text As String

    If ctl.InvokeRequired Then
        text = CStr(ctl.Invoke(
            New GetControlTextInvoker(AddressOf GetControlText), ctl))
    Else
        text = ctl.Text
    End If

    Return text
End Function
UrsulRosu
quelle
3

Gleiche Frage: Wie-aktualisiere ich-die-GUI-von-einem-anderen-Thread-in-c

Zwei Wege:

  1. Geben Sie den Wert in e.result zurück und verwenden Sie ihn, um Ihren Textfeldwert im Ereignis backgroundWorker_RunWorkerCompleted festzulegen

  2. Deklarieren Sie eine Variable, um diese Art von Werten in einer separaten Klasse zu speichern (die als Datenhalter fungiert). Erstellen Sie eine statische Instanz dieser Klasse, und Sie können über einen beliebigen Thread darauf zugreifen.

Beispiel:

public  class data_holder_for_controls
{
    //it will hold value for your label
    public  string status = string.Empty;
}

class Demo
{
    public static  data_holder_for_controls d1 = new data_holder_for_controls();
    static void Main(string[] args)
    {
        ThreadStart ts = new ThreadStart(perform_logic);
        Thread t1 = new Thread(ts);
        t1.Start();
        t1.Join();
        //your_label.Text=d1.status; --- can access it from any thread 
    }

    public static void perform_logic()
    {
        //put some code here in this function
        for (int i = 0; i < 10; i++)
        {
            //statements here
        }
        //set result in status variable
        d1.status = "Task done";
    }
}
Saurabh
quelle
2

Verwenden Sie einfach dies:

this.Invoke((MethodInvoker)delegate
            {
                YourControl.Property= value; // runs thread safe
            });
Hasan Shouman
quelle
0

Aktion y; // innerhalb der Klasse deklariert

label1.Invoke (y = () => label1.Text = "text");

Antonio Leite
quelle
0

Einfache und wiederverwendbare Möglichkeit, dieses Problem zu umgehen.

Erweiterungsmethode

public static class FormExts
{
    public static void LoadOnUI(this Form frm, Action action)
    {
        if (frm.InvokeRequired) frm.Invoke(action);
        else action.Invoke();
    }
}

Beispielnutzung

private void OnAnyEvent(object sender, EventArgs args)
{
    this.LoadOnUI(() =>
    {
        label1.Text = "";
        button1.Text = "";
    });
}
Timothy Macharia
quelle
-3

Es gibt zwei Optionen für Cross-Thread-Operationen.

Control.InvokeRequired Property 

und zweitens ist zu verwenden

SynchronizationContext Post Method

Control.InvokeRequired ist nur nützlich, wenn Steuerelemente von der Control-Klasse geerbt werden, während SynchronizationContext überall verwendet werden kann. Einige nützliche Informationen sind folgende Links

Cross Thread Update UI | .Netz

Threadübergreifende Update-Benutzeroberfläche mit SynchronizationContext | .Netz

Nasir Mahmood
quelle