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


584

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

  1. Il existe un formulaire principal qui héberge un certain contrôle utilisateur.
  2. Le contrôle utilisateur effectue une opération de données volumineuse, de sorte que si j'appelle directement la UserControl_Loadméthode, l'interface utilisateur ne répond plus pendant la durée d'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 possible)
  4. J'ai utilisé un thread de travail en arrière-plan qui chargera les données et, une fois terminé, notifiera à 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 enfants) a été créée sur le thread principal principal. Dans la méthode LOAD de usercontrol, je récupère des données en fonction des valeurs de certains contrôles (comme la zone de texte) 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 qu'il a donnée était

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

Pour en savoir plus, j'ai fait une recherche sur Google et une suggestion est venue comme utiliser 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 semble que je suis de retour à la case départ. L'Application ne répond plus. 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 par le troisième que j'ai généré.

Je ne sais pas si j'ai perçu ce bien ou ce mal. Je suis nouveau dans le filetage.

Comment puis-je résoudre ce problème et quel est également l'effet de l'exécution de la ligne # 1 en cas de blocage?

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 à partir du thread enfant. Je ne vais jamais le faire à partir d'un thread enfant.

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


Pour mon exemple particulier de cette erreur, j'ai trouvé que la solution de contournement consistait à utiliser un BackgroundWorker sur le formulaire pour gérer les parties gourmandes en données du code. (c'est-à-dire mettre tout le code du problème dans la méthode backgroundWorker1_DoWork () et l'appeler via backgroundWorker1.RunWorkerAsync ()) ... Ces deux sources m'ont indiqué la bonne direction: stackoverflow.com/questions/4806742/… youtube.com/ watch? v = MLrrbG6V1zM
Giollia

Réponses:


433

Selon le 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 à partir du thread enfant. Je ne vais jamais le faire à partir d'un thread enfant.

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

La solution que vous souhaitez alors devrait ressembler à:

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

Effectuez votre traitement sérieux dans le thread séparé avant d'essayer 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;
        }
    }
}

1
Cela fait un moment maintenant que je n'ai pas fait de programmation C #, mais sur la base de l'article MSDN et de mes connaissances fragmentaires, cela y ressemble.
Jeff Hubbard

1
La différence est que BeginInvoke () est asynchrone tandis que Invoke () s'exécute de manière synchrone. stackoverflow.com/questions/229554/…
frzsombor

179

Modèle de thread dans l'interface utilisateur

Veuillez lire le modèle de thread dans les applications d'interface utilisateur (l' ancien lien VB est ici ) afin de comprendre les concepts de base. Le lien accède à la page qui décrit le modèle de thread WPF. Cependant, Windows Forms utilise la même idée.

Le fil d'interface utilisateur

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

entrez la description de l'image ici

entrez la description de l'image ici

Méthodes BeginInvoke et Invoke

  • La surcharge de calcul de la méthode invoquée doit être faible ainsi que la surcharge de calcul des méthodes de gestionnaire d'événements car le thread d'interface utilisateur y est utilisé - le même qui est responsable de la gestion des entrées utilisateur. Peu importe s'il s'agit de System.Windows.Forms.Control.Invoke ou System.Windows.Forms.Control.BeginInvoke .
  • Pour effectuer un calcul coûteux, utilisez toujours un thread séparé. Depuis .NET 2.0 BackgroundWorker est dédié à effectuer des opérations informatiques coûteuses dans Windows Forms. Cependant, dans les nouvelles solutions, vous devez utiliser le modèle async-wait comme décrit ici .
  • Utilisez les méthodes System.Windows.Forms.Control.Invoke ou System.Windows.Forms.Control.BeginInvoke uniquement pour mettre à jour une interface utilisateur. Si vous les utilisez pour des calculs lourds, votre application bloquera:

entrez la description de l'image ici

Invoquer

entrez la description de l'image ici

BeginInvoke

entrez la description de l'image ici

Solution de code

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



72

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

Voir mon article de discussion pour un exemple WinForms - bien que l'article ait été écrit avant d' BackgroundWorkerarriver sur la scène, et je crains de ne pas l'avoir mis à jour à cet égard. BackgroundWorkersimplifie simplement un peu le rappel.


ici dans cet état à moi. je ne change même pas l'interface utilisateur. J'accède simplement à ses valeurs actuelles à partir du thread enfant. toute suggestion à mettre en œuvre
Prerak K

1
Vous devez toujours rassembler le thread d'interface utilisateur, même juste pour accéder aux propriétés. Si votre méthode ne peut pas continuer tant que la valeur n'est pas accessible, vous pouvez utiliser un délégué qui renvoie la valeur. Mais oui, passez par le thread d'interface utilisateur.
Jon Skeet

Salut Jon, je crois que tu me dirige dans la bonne direction. Oui, j'ai besoin de la valeur sans elle, je ne peux pas continuer. S'il vous plaît, pourriez-vous expliquer cela en utilisant un délégué qui retourne une valeur. Merci
Prerak K

1
Utilisez un délégué tel que Func <string>: string text = textbox1.Invoke ((Func <string>) () => textbox1.Text); (Cela suppose que vous utilisez C # 3.0 - vous pourriez utiliser une méthode anonyme sinon.)
Jon Skeet

45

Je sais que c'est trop tard maintenant. Cependant, même aujourd'hui, si vous rencontrez des problèmes pour accéder aux contrôles croisés? Ceci est la réponse la plus courte jusqu'à la date: P

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

C'est ainsi que j'accède à n'importe quel contrôle de formulaire à partir d'un fil.


1
Cela me donne Invoke or BeginInvoke cannot be called on a control until the window handle has been created. Je l'ai résolu ici
rupweb

42

J'ai eu ce problème avec le FileSystemWatcheret j'ai constaté que le code suivant a résolu le problème:

fsw.SynchronizingObject = this

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


2
Cela a sauvé mon bacon. Dans VB.NET, j'ai utilisé.SynchronizingObject = Me
codingcoding

20

Je trouve que le code check-and-invoke qui doit être jonché dans toutes les méthodes liées aux formulaires est beaucoup trop verbeux et inutile. Voici une méthode d'extension simple qui vous permet de vous en débarrasser 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");

Plus besoin de déconner - simple.


ce qui n'est pas ici
Rawat

@Rawat tdans ce cas sera textbox1- il est passé en argument
Rob

17

Les contrôles dans .NET ne sont généralement pas thread-safe. Cela signifie que vous ne devez pas accéder à un contrôle à partir d'un thread autre que celui où il se trouve. Pour contourner ce problème, vous devez appeler le contrôle, ce que tente votre deuxième échantillon.

Cependant, dans votre cas, tout ce que vous avez fait est de renvoyer 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 repenser cela un peu pour que tout ce que vous faites sur le thread principal soit de définir une propriété rapide ici et là.



10

Un nouveau look avec Async / Await et les rappels. Vous n'avez besoin que d'une seule 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 choses à la méthode Extension, comme l'encapsuler dans une instruction Try / Catch, permettant à l'appelant de lui dire quel type renvoyer une fois terminé, un rappel d'exception à l'appelant:

Ajout de Try Catch, de la journalisation des exceptions automatiques et de 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
        }
    }

10

Ce n'est pas la méthode recommandée pour résoudre cette erreur mais vous pouvez la supprimer rapidement, elle fera l'affaire. Je préfère cela pour les prototypes ou les démos. ajouter

CheckForIllegalCrossThreadCalls = false

dans Form1()constructeur.


9

Suivez la manière la plus simple (à mon avis) de modifier des 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;
        }
    }
}

Facile ! Merci .
Ali Esmaeili


7

J'ai trouvé un besoin pour cela lors de la programmation d'un contrôleur d'application monotouch iOS-Phone dans un projet prototype Winforms de Visual Studio en dehors de Xamarin Stuidio. Préférant programmer autant que possible dans VS sur xamarin studio, je souhaitais que le contrôleur soit complètement découplé de la structure du téléphone. De cette façon, l'implémentation pour d'autres cadres comme Android et Windows Phone serait beaucoup plus facile pour les utilisations futures.

Je voulais une solution où l'interface graphique pourrait répondre aux événements sans avoir à gérer le code de commutation croisé derrière chaque clic de bouton. Fondamentalement, laissez le contrôleur de classe gérer cela pour garder le code client simple. Vous pouvez éventuellement avoir de nombreux événements sur l'interface graphique où comme si vous pouviez le gérer en un seul endroit dans la classe serait plus propre. Je ne suis pas un expert multi-theading, faites-moi savoir si cela est défectueux.

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

Le formulaire GUI ignore que le contrôleur exécute des tâches asynchrones.

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

6

Voici une alternative si l'objet avec lequel vous travaillez n'a pas

(InvokeRequired)

Ceci est utile si vous travaillez avec le formulaire principal dans une classe autre que le formulaire principal avec un objet qui se trouve dans le formulaire principal, mais qui n'a pas InvokeRequired

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

Cela fonctionne de la même manière que ci-dessus, mais c'est une approche différente si vous n'avez pas d'objet avec invokerequired, mais avez accès au MainForm


5

Dans le même sens que les réponses précédentes, mais un ajout très court qui permet d'utiliser toutes les propriétés de contrôle sans avoir d'exception d'invocation croisée.

Méthode d'assistance

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

Exemple d'utilisation

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


5

Par exemple, pour obtenir le texte d'un contrôle du thread d'interface utilisateur:

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

3

Même question: comment-mettre-à-jour-le-gui-d'un-autre-thread-en-c

Deux façons:

  1. Renvoyez la valeur dans e.result et utilisez-la pour définir la valeur de votre zone de texte dans l'événement backgroundWorker_RunWorkerCompleted

  2. Déclarez une variable pour conserver ce type de valeurs dans une classe distincte (qui fonctionnera comme détenteur de données). Créez une instance statique de cette classe et vous pouvez y accéder via n'importe quel thread.

Exemple:

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

2

Utilisez simplement ceci:

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

0

Action y; // déclaré à l'intérieur de la classe

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


0

Un moyen simple et réutilisable de contourner ce problème.

Méthode d'extension

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

Exemple d'utilisation

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

-3

Il existe deux options pour les opérations entre fils.

Control.InvokeRequired Property 

et le second est d'utiliser

SynchronizationContext Post Method

Control.InvokeRequired n'est utile que lorsque les contrôles de travail hérités de la classe Control alors que SynchronizationContext peut être utilisé n'importe où. Certaines informations utiles sont les liens suivants

Interface utilisateur de mise à jour croisée | .Net

Interface de mise à jour croisée des threads à l'aide de SynchronizationContext | .Net

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.