web-dev-qa-db-fra.com

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

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 lourde, telle que si j'appelle directement la méthode UserControl_Load, l'interface utilisateur cesse de répondre 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 l'opération terminée, informera l'application qu'elle a effectué son travail.
  5. Maintenant est venu un vrai problème. Toute l'interface utilisateur (forme principale et ses contrôles utilisateur) a été créée sur le thread principal principal. Dans la méthode LOAD de usercontrol, je récupère les données en fonction des valeurs de certains contrôles (tels que 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 qu'il a donné était

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

Pour en savoir plus, j'ai fait des recherches sur Google et une suggestion a été suggérée, 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 sois de retour à la case départ. L'application redevient 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 par la troisième que j'ai générée.

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

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

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 changer la valeur d'un contrôle du thread enfant. Je ne le ferai jamais à partir d'un thread enfant.

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

546
Prerak K

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

Je suppose que je n’ai pas posé la question correctement.

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 changer la valeur d'un contrôle du thread enfant. Je ne le ferai jamais à partir d'un thread enfant.

Vous devez donc uniquement accéder à 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 à ceci:

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 avant de tenter 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;
        }
    }
}
411
Jeff Hubbard

Modèle de filetage dans l'interface utilisateur

Veuillez lire le modèle de thread dans les applications d'interface utilisateur afin de comprendre les concepts de base. Le lien permet d'accéder à la page décrivant le modèle de threading WPF. Cependant, Windows Forms utilise la même idée.

Le fil de l'interface utilisateur

  • Un seul thread (thread d'interface utilisateur) est autorisé à accéder à System.Windows.Forms.Control et à ses membres de sous-classes.
  • Tenter d'accéder au membre de System.Windows.Forms.Control à partir d'un thread différent de celui de l'interface utilisateur provoquera une exception cross-thread.
  • Comme 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:

enter image description here

enter image description here

Méthodes BeginInvoke et Invoke

  • La surcharge informatique de la méthode invoquée doit être faible, de même que celle des méthodes de gestionnaire d'événements, car le thread d'interface utilisateur y est utilisé - le même responsable de la gestion des entrées de l'utilisateur. Peu importe qu'il s'agisse de System.Windows.Forms.Control.Invoke ou System.Windows.Forms.Control.BeginInvoke .
  • Pour effectuer des opérations coûteuses en informatique, utilisez toujours des threads séparés. Depuis .NET 2.0 BackgroundWorker est dédié à la réalisation d’opérations coûteuses en informatique dans Windows Forms. Cependant, dans les nouvelles solutions, vous devez utiliser le modèle async-wait tel que décrit ici .
  • Utilisez les méthodes System.Windows.Forms.Control.Invoke ou System.Windows.Forms.Control.BeginInvoke 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

Commencer à invoquer

enter image description here

Solution de code

Lire 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 here .

165
Ryszard Dżegan

Vous souhaitez uniquement utiliser Invoke ou BeginInvoke pour le travail minimal requis pour modifier l'interface utilisateur. Votre méthode "lourde" doit être exécutée sur un autre thread (par exemple, via BackgroundWorker), mais en utilisant ensuite Control.Invoke/Control.BeginInvoke pour mettre à jour l'interface utilisateur. Ainsi, votre fil d’interface utilisateur sera libre de gérer les événements d’interface utilisateur, etc.

Voir mon article de filetage pour un exemple WinForms - bien que l'article ait été écrit avant l'arrivée de BackgroundWorker, et j'ai bien peur de ne pas l'avoir mis à jour à cet égard. BackgroundWorker simplifie simplement un peu le rappel.

72
Jon Skeet

J'ai eu ce problème avec le FileSystemWatcher et j'ai constaté que le code suivant le résolvait:

fsw.SynchronizingObject = this

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

41
Peter C

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

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

Voici comment j'accède à n'importe quel contrôle de formulaire à partir d'un fil.

31
Bravo

Je trouve le code check-and-invoke qui doit être inséré dans toutes les méthodes liées aux formulaires 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 déconner - simple.

17
Rob

Les contrôles dans .NET ne sont généralement pas thread-safe. Cela signifie que vous ne devriez pas accéder à un contrôle à partir d'un thread autre que celui où il réside. Pour contourner ce problème, vous devez invoquer 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 fil principal définisse une propriété rapide ici et là.

17
Joel Coehoorn

La solution la plus propre (et appropriée) pour les problèmes de multithreading 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.

14
Igor Brejc

Un nouveau look utilisant Async/Await et les rappels. 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, tels que l'envelopper dans une instruction Try/Catch, en permettant à l'appelant de lui indiquer le type à renvoyer une fois l'opération terminée, un rappel d'exception à l'appelant:

Ajouter 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
        }
    }
10
John Peters

Vous devez regarder l'exemple de Backgroundworker:
http://msdn.Microsoft.com/en-us/library/system.componentmodel.backgroundworker.aspx Notamment comment cela interagit avec la couche d'interface utilisateur. En fonction de votre publication, cela semble répondre à vos problèmes.

8
Pat

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;
        }
    }
}
8
Vanderley Maia

J'en ai trouvé un besoin lors de la programmation d'un contrôleur d'application monotouch iOS-Phone dans un projet prototype Visual Studio Winforms en dehors de xamarin stuidio. Préférant programmer autant que possible en VS sur xamarin studio, je souhaitais que le contrôleur soit complètement découplé du framework téléphonique. De cette façon, la mise en œuvre de cette stratégie pour d'autres infrastructures telles que Androidet Windows Phone serait beaucoup plus simple pour les utilisations futures.

Je recherchais une solution permettant à l’interface graphique de réagir aux événements sans avoir à gérer le code de commutation inter-threads derrière chaque clic de bouton. En gros, laissez le contrôleur de classe s'en charger pour que le code client reste simple. Vous pouvez éventuellement avoir plusieurs événements sur l'interface graphique où, comme si vous pouviez le gérer à un endroit de la classe, ce serait plus propre. Je ne suis pas un expert en matière de theading, laissez-moi savoir si cela est imparfait.

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

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

CheckForIllegalCrossThreadCalls = false

dans Form1() constructeur.

7
Özgür

Voici un autre moyen 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 que InvokeRequired n'est pas associé.

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 comme ci-dessus, mais c'est une approche différente si vous n'avez pas d'objet avec invokerequired, mais avez accès au MainForm

6
Ashitakalax

Par exemple, pour obtenir le texte d'un thread du contrôle de l'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
5
UrsulRosu

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 du contrôle sans exception d’appel croisé.

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
Mike
this.Invoke(new MethodInvoker(delegate
            {
                //your code here;
            }));
5
Hamid Jolany

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

Deux façons:

  1. Renvoyer une valeur dans e.result et l'utiliser pour définir la valeur de votre zone de texte dans l'événement backgroundWorker_RunWorkerCompleted

  2. Déclarez une variable pour contenir ce type de valeurs dans une classe séparée (qui fonctionnera en tant que détenteur de données). Créez une instance statique de cette classe et vous pourrez 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";
    }
}
3
Saurabh

Action y; // déclaré dans la classe

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

0
Antonio Leite

Utilisez simplement ceci:

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