Question Opération inter-thread non valide: contrôle accédé à partir d'un thread autre que le thread sur lequel il a été créé


J'ai un scénario (Windows Forms, C #, .NET)

  1. Il y a un formulaire principal qui héberge un certain contrôle d'utilisateur.
  2. Le contrôle de l'utilisateur effectue une opération lourde de données, de sorte que si j'appelle directement le UserControl_Load méthode l'interface utilisateur devient non-réponse pour la durée de l'exécution de la méthode de chargement.
  3. Pour surmonter cela, je charge des données sur différents threads (en essayant de modifier le code existant aussi peu que je peux)
  4. J'ai utilisé un thread d'arrière-plan qui chargera les données et quand cela sera fait, j'indiquerai à l'application qu'elle a fait son travail.
  5. Maintenant est venu un vrai problème. Toute l'interface utilisateur (formulaire principal et ses contrôles utilisateur) a été créée sur le thread principal. Dans la méthode LOAD du contrôle utilisateur, je récupère les données en fonction des valeurs de certains contrôles (comme textbox) sur userControl.

Le pseudocode ressemblerait à ceci:

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.
    }
}

L'exception a été donnée

Opération croisée non valide: contrôle accessible depuis un thread autre que le thread sur lequel il a été créé.

Pour en savoir plus à ce sujet, j'ai fait un peu de googling et une suggestion est apparue comme en utilisant le code suivant

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
    }
}

MAIS MAIS MAIS ... il me semble que je suis de retour à la case départ. L'application à nouveau devenir insensible. Cela semble être dû à l'exécution de la ligne # 1 si condition. La tâche de chargement est à nouveau effectuée par le thread parent et non le troisième que j'ai créé.

Je ne sais pas si j'ai perçu ce bien ou ce qui est faux. Je suis nouveau à fileter.

Comment puis-je résoudre ce problème et quel est l'effet de l'exécution de la ligne n ° 1 si elle est bloquée?

La situation est la suivante: Je souhaite charger des données dans une variable globale en fonction de la valeur d'un contrôle. Je ne veux pas modifier la valeur d'un contrôle du thread enfant. Je ne vais jamais le faire à partir d'un fil d'enfant.

Donc, accéder uniquement à la valeur pour que les données correspondantes puissent être extraites de la base de données.


471
2017-09-26 21:12


origine


Réponses:


Selon Commentaire de mise à jour de Prerak K (depuis supprimé):

Je suppose que je n'ai pas présenté la question correctement.

La situation est la suivante: Je veux charger des données dans une variable globale basée sur la valeur d'un contrôle. Je ne veux pas modifier la valeur d'un contrôle du thread enfant. Je ne vais jamais le faire à partir d'un fil d'enfant.

Donc seulement accéder à la valeur afin que les données correspondantes peuvent être récupérées à partir de la base de données.

La solution que vous souhaitez doit alors ressembler à:

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

Faites votre traitement sérieux dans le fil séparé avant vous essayez de revenir au thread du contrôle. Par exemple:

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;
        }
    }
}

366
2017-09-26 21:22



Modèle Threading dans l'interface utilisateur

Veuillez lire le Modèle de filetage dans les applications d'interface utilisateur afin de comprendre les concepts de base. Le lien accède à la page qui décrit le modèle de thread WPF. Toutefois, Windows Forms utilise la même idée.

Le fil de l'interface utilisateur

  • Il n'y a qu'un seul thread (thread UI), qui est autorisé à accéder System.Windows.Forms.Control et ses membres de sous-classes.
  • Tentative d'accès au membre de System.Windows.Forms.Control à partir de thread différent de thread UI entraînera une exception de thread-thread.
  • Comme il n'y a qu'un seul thread, toutes les opérations d'interface utilisateur sont mises en file d'attente en tant qu'éléments de travail dans ce thread:

enter image description here

enter image description here

Méthodes BeginInvoke et Invoke

  • La charge de calcul de la méthode invoquée doit être faible, tout comme le surcoût des méthodes du gestionnaire d'événements, car le thread d'interface utilisateur y est utilisé, le même que celui qui gère les entrées utilisateur. Peu importe si c'est System.Windows.Forms.Control.Invoke ou System.Windows.Forms.Control.BeginInvoke.
  • Pour effectuer une opération coûteuse, utilisez toujours un thread séparé. Depuis .NET 2.0 BackgroundWorker est dédié à la réalisation d'opérations coûteuses dans Windows Forms. Cependant, dans les nouvelles solutions, vous devez utiliser le modèle async-await comme décrit ici.
  • Utilisation System.Windows.Forms.Control.Invoke ou System.Windows.Forms.Control.BeginInvoke méthodes uniquement pour mettre à jour une interface utilisateur. Si vous les utilisez pour des calculs lourds, votre application bloquera:

enter image description here

Invoquer

enter image description here

BeginInvoke

enter image description here

Solution de code

Lire les réponses à la question Comment mettre à jour l'interface graphique d'un autre thread en C #?. Pour C # 5.0 et .NET 4.5, la solution recommandée est ici.


141
2017-09-26 08:34



Vous ne souhaitez utiliser Invoke ou BeginInvoke que pour le travail minimum requis pour modifier l'interface utilisateur. Votre méthode "lourde" doit s'exécuter sur un autre thread (par exemple, via BackgroundWorker), mais en utilisant Control.Invoke / Control.BeginInvoke juste pour mettre à jour l'interface utilisateur. De cette façon, votre thread d'interface utilisateur sera libre de gérer les événements de l'interface utilisateur, etc.

Voir mon article de filetage pour un Exemple de WinForms - bien que l'article ait été écrit avant que BackgroundWorker n'arrive sur les lieux, et je crains de ne pas l'avoir mis à jour à cet égard. BackgroundWorker simplifie simplement le rappel un peu.


66
2017-09-26 21:21



J'ai eu ce problème avec le FileSystemWatcher et trouvé que le code suivant a résolu le problème:

fsw.SynchronizingObject = this

Le contrôle utilise ensuite l'objet de formulaire en cours pour gérer les événements et sera donc sur le même thread.


36
2018-01-13 02:13



Les contrôles dans .NET ne sont généralement pas sécurisés pour les threads. Cela signifie que vous ne devez pas accéder à un contrôle d'un thread autre que celui où il réside. Pour contourner cela, vous devez invoquer le contrôle, qui est ce que votre 2ème échantillon tente.

Cependant, dans votre cas, tout ce que vous avez fait est de passer la méthode de longue durée au thread principal. Bien sûr, ce n'est pas vraiment ce que vous voulez faire. Vous devez le repenser un peu afin que tout ce que vous faites sur le thread principal soit de définir une propriété rapide ici et là.


13
2017-09-26 21:19



Je trouve que le code check-and-invoke qui doit être jeté dans toutes les méthodes liées aux formulaires est beaucoup trop bavard et inutile. Voici une méthode d'extension simple qui vous permet de la supprimer complètement:

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);
    }
}

Et puis vous pouvez simplement faire ceci:

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

Pas plus de déconner - simple.


12
2018-05-02 13:42



La solution la plus propre (et appropriée) pour les problèmes d'interfonctionnement de l'interface utilisateur consiste à utiliser SynchronizationContext, voir Synchronisation des appels vers l'interface utilisateur dans une application multithread article, il l'explique très bien.


10
2018-04-27 05:18



Je sais qu'il est trop tard maintenant. Cependant, même aujourd'hui, si vous rencontrez des difficultés pour accéder aux contrôles de threads? C'est la réponse la plus courte à ce jour: P

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

Voici comment j'accède à tout contrôle de formulaire à partir d'un thread.


8
2017-10-17 11:55



Suivez la méthode la plus simple (à mon avis) pour modifier les objets d'un autre thread:

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;
        }
    }
}

6
2018-05-06 16:49



Un nouveau look utilisant Async / Await et callbacks. Vous n'avez besoin que d'une ligne de code si vous conservez la méthode d'extension dans votre projet.

/// <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;
    }
}

Vous pouvez ajouter d'autres éléments à la méthode Extension, par exemple en l'enveloppant dans une instruction Try / Catch, ce qui permet à l'appelant de lui indiquer le type à renvoyer après l'achèvement, un rappel d'exception à l'appelant:

Ajout de Try Catch, Auto Exception Logging et 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
        }
    }

6
2018-05-16 01:55