Operazione cross-thread non valida: controllo a cui si accede da un thread diverso dal thread su cui è stato creato

StackOverflow https://stackoverflow.com/questions/142003

Domanda

Ho uno scenario. (Windows Form, C #, .NET)

  1. Esiste un modulo principale che ospita alcuni controlli utente.
  2. Il controllo utente esegue alcune operazioni con dati pesanti, in modo tale che se io chiamo direttamente il metodo UserControl_Load l'interfaccia utente non risponde per la durata dell'esecuzione del metodo di caricamento.
  3. Per ovviare a questo, carico i dati su thread diversi (cercando di cambiare il codice esistente il meno possibile)
  4. Ho usato un thread di lavoro in background che caricherà i dati e quando fatto notificherà all'applicazione che ha fatto il suo lavoro.
  5. Ora è arrivato un vero problema. Tutta l'interfaccia utente (modulo principale e relativi controlli utente secondari) è stata creata sul thread principale principale. Nel metodo LOAD del controllo utente sto recuperando i dati in base ai valori di alcuni controlli (come la casella di testo) su userControl.

Lo pseudocodice sarebbe simile al seguente:

CODICE 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'eccezione che ha dato era

  

Operazione cross-thread non valida: controllo a cui si accede da un thread diverso dal thread su cui è stato creato.

Per saperne di più ho fatto un po 'di ricerche su Google e un suggerimento è venuto fuori come usare il seguente codice

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

MA MA MA ... sembra che io sia tornato al punto di partenza. Di nuovo l'applicazione non rispondere. Sembra essere dovuto all'esecuzione della riga n. 1 se la condizione. L'attività di caricamento viene nuovamente eseguita dal thread principale e non dal terzo che ho generato.

Non so se ho percepito questo giusto o sbagliato. Sono nuovo del threading.

Come posso risolvere questo e anche qual è l'effetto dell'esecuzione della riga n. 1 se blocco?

La situazione è questa : voglio caricare i dati in una variabile globale in base al valore di un controllo. Non voglio cambiare il valore di un controllo dal thread figlio. Non lo farò mai da un thread figlio.

Accedendo quindi solo al valore in modo che i dati corrispondenti possano essere recuperati dal database.

È stato utile?

Soluzione

Secondo Commento sull'aggiornamento di Prerak K (da quando è stato eliminato):

  

Suppongo di non aver presentato correttamente la domanda.

     

La situazione è questa: voglio caricare i dati in una variabile globale in base al valore di un controllo. Non voglio cambiare il valore di un controllo dal thread figlio. Non lo farò mai da un thread figlio.

     

Quindi accedendo solo al valore in modo che i dati corrispondenti possano essere recuperati dal database.

La soluzione che desideri allora dovrebbe apparire come:

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

Effettua la tua elaborazione seria nel thread separato prima per tentare di tornare al thread del controllo. Ad esempio:

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

Altri suggerimenti

Modello di threading nell'interfaccia utente

Leggi Threading Model nelle applicazioni dell'interfaccia utente per comprendere i concetti di base. Il collegamento passa alla pagina che descrive il modello di threading WPF. Tuttavia, Windows Form utilizza la stessa idea.

Il thread dell'interfaccia utente

  • Esiste un solo thread (thread dell'interfaccia utente) a cui è consentito accedere a System.Windows.Forms.Control e i suoi membri delle sottoclassi.
  • Tentativo di accedere al membro di System.Windows.Forms .Control da thread diversi rispetto al thread dell'interfaccia utente causerà un'eccezione cross-thread.
  • Poiché esiste un solo thread, tutte le operazioni dell'interfaccia utente vengono messe in coda come elementi di lavoro in quel thread:

inserisci qui la descrizione dell'immagine

inserisci qui la descrizione dell'immagine

metodi BeginInvoke e Invoke

inserisci qui la descrizione dell'immagine

Invoke

inserisci qui la descrizione dell'immagine

BeginInvoke

inserisci qui la descrizione dell'immagine

Soluzione di codice

Leggi le risposte alla domanda Come aggiornare la GUI da un altro thread in C #? . Per C # 5.0 e .NET 4.5 la soluzione consigliata è qui .

Si desidera utilizzare solo Invoke o BeginInvoke per il minimo lavoro richiesto per modificare l'interfaccia utente. Il tuo "pesante" Il metodo dovrebbe essere eseguito su un altro thread (ad esempio tramite BackgroundWorker) ma utilizzando Control.Invoke / Control.BeginInvoke solo per aggiornare l'interfaccia utente. In questo modo il tuo thread dell'interfaccia utente sarà libero di gestire eventi dell'interfaccia utente ecc.

Vedi il mio articolo di discussione per un esempio di WinForms - sebbene l'articolo sia stato scritto prima che BackgroundWorker arrivasse sulla scena e temo di non averlo aggiornato in quel rispetto. BackgroundWorker semplifica un po 'il callback.

Ho riscontrato questo problema con FileSystemWatcher e ho scoperto che il codice seguente ha risolto il problema:

fsw.SynchronizingObject = this

Il controllo utilizza quindi l'oggetto modulo corrente per gestire gli eventi e sarà quindi sullo stesso thread.

Ora so che è troppo tardi. Tuttavia, anche oggi se riscontri problemi nell'accedere ai controlli tra thread? Questa è la risposta più breve fino alla data: P

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

Questo è il modo in cui accedo a qualsiasi controllo del modulo da un thread.

I controlli in .NET non sono generalmente thread-safe. Ciò significa che non dovresti accedere a un controllo da un thread diverso da quello in cui vive. Per ovviare a questo, devi invocare il controllo, che è quello che sta tentando il tuo secondo campione.

Tuttavia, nel tuo caso tutto ciò che hai fatto è passare il metodo di lunga durata al thread principale. Certo, non è proprio quello che vuoi fare. Devi ripensarci un po 'in modo che tutto quello che stai facendo sul thread principale sia impostare una proprietà rapida qua e là.

Trovo che il codice check-and-invoke che deve essere disseminato in tutti i metodi relativi ai moduli sia troppo dettagliato e non necessario. Ecco un semplice metodo di estensione che ti consente di eliminarlo completamente:

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

E poi puoi semplicemente farlo:

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

Niente più scherzi - semplice.

La soluzione più pulita (e corretta) per i problemi di cross-threading dell'interfaccia utente è utilizzare SynchronizationContext, vedere Sincronizzazione delle chiamate all'interfaccia utente in un'applicazione multi-thread , lo spiega molto bene.

Un nuovo look con Async / Await e callbacks. Hai solo una riga di codice se mantieni il metodo di estensione nel tuo progetto.

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

È possibile aggiungere altre cose al metodo Extension come includerlo in un'istruzione Try / Catch, consentendo al chiamante di dirgli quale tipo restituire dopo il completamento, un'eccezione callback per il chiamante:

Aggiunta di Try Catch, Registrazione eccezioni automatica e 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
        }
    }

Devi guardare l'esempio di Backgroundworker:
http://msdn.microsoft.com/en-us/library /system.componentmodel.backgroundworker.aspx Soprattutto come interagisce con il livello dell'interfaccia utente. Sulla base dei tuoi post, questo sembra rispondere ai tuoi problemi.

Segui il modo più semplice (secondo me) di modificare gli oggetti da un altro 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;
        }
    }
}

Ne ho trovato la necessità durante la programmazione di un controller di app monotouch per iOS-Phone in un progetto prototipo di visual studio winforms al di fuori di Xamarin Stuidio. Preferendo programmare il più possibile su VS su Xamarin Studio, volevo che il controller fosse completamente disaccoppiato dal framework del telefono. In questo modo implementarlo per altri framework come Android e Windows Phone sarebbe molto più semplice per usi futuri.

Volevo una soluzione in cui la GUI potesse rispondere agli eventi senza l'onere di gestire il codice di commutazione cross-threading dietro ogni clic di un pulsante. Fondamentalmente lascia che il controller di classe lo gestisca per mantenere semplice il codice client. Potresti avere molti eventi sulla GUI in cui, come se potessi gestirlo in un posto nella classe, sarebbe più pulito. Non sono un esperto multi-theading, fammi sapere se questo è difettoso.

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

Il modulo della GUI non è a conoscenza del fatto che il controller stia eseguendo attività asincrone.

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

Questo non è il modo consigliato per risolvere questo errore ma puoi eliminarlo rapidamente, farà il lavoro. Preferisco questo per prototipi o demo. aggiungi

CheckForIllegalCrossThreadCalls = false

nel costruttore Form1 () .

Ecco un modo alternativo se l'oggetto con cui stai lavorando non ha

(InvokeRequired)

Questo è utile se stai lavorando con il modulo principale in una classe diversa dal modulo principale con un oggetto che si trova nel modulo principale, ma non ha 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;
}

Funziona come sopra, ma è un approccio diverso se non hai un oggetto con invokerequired, ma hai accesso al MainForm

Sulla stessa linea delle risposte precedenti, ma un'aggiunta molto breve che consente di utilizzare tutte le proprietà di controllo senza l'eccezione di invocazione tra thread.

Metodo di supporto

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

Esempio di utilizzo

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

Ad esempio per ottenere il testo da un controllo del thread dell'interfaccia utente:

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

Stessa domanda: how-to-update- la-gui-da-un-filo-in-c

Due modi:

  1. Restituisce il valore in e.result e lo usa per impostare il valore della casella di testo in backgroundWorker_RunWorkerCompleted event

  2. Dichiara alcune variabili per contenere questo tipo di valori in una classe separata (che funzionerà come titolare dei dati). Crea un'istanza statica di questa classe e puoi accedervi da qualsiasi thread.

Esempio:

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

Azione y; // dichiarato all'interno della classe

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

Usa semplicemente questo:

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

Esistono due opzioni per le operazioni di thread incrociato.

Control.InvokeRequired Property 

e il secondo è quello di usare

SynchronizationContext Post Method

Control.InvokeRequired è utile solo quando si utilizzano i controlli ereditati dalla classe Control mentre SynchronizationContext può essere utilizzato ovunque. Alcune informazioni utili sono i seguenti collegamenti

Interfaccia utente aggiornamento thread incrociato | .Net

Interfaccia utente di aggiornamento discussione incrociata utilizzando SynchronizationContext | .Net

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top