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


584

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 chiamo direttamente il UserControl_Loadmetodo 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 modificare il codice esistente il meno possibile)
  4. Ho usato un thread di lavoro in background che caricherà i dati e quando fatto notifica 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 è stata

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. L'applicazione diventa nuovamente non rispondente. 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 in caso di 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.

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


Per la mia particolare istanza di questo errore, ho trovato la soluzione alternativa per utilizzare un BackgroundWorker nel modulo per gestire le parti del codice ad alta intensità di dati. (vale a dire mettere tutto il codice problema nel metodo backgroundWorker1_DoWork () e chiamarlo tramite backgroundWorker1.RunWorkerAsync ()) ... Queste due fonti mi hanno indicato nella direzione corretta: stackoverflow.com/questions/4806742/… youtube.com/ guarda? v = MLrrbG6V1zM
Giollia,

Risposte:


433

Secondo il commento di aggiornamento di Prerak K (poiché eliminato):

Immagino 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 solo l'accesso al valore in modo che i dati corrispondenti possano essere recuperati dal database.

La soluzione che vuoi quindi dovrebbe apparire come:

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

Esegui la tua elaborazione seria nel thread separato prima di provare a tornare al thread del controllo. Per 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;
        }
    }
}

1
È passato un po 'di tempo da quando ho fatto la programmazione in C #, ma sulla base dell'articolo MSDN e delle mie conoscenze frammentarie, sembra che sia così.
Jeff Hubbard,

1
La differenza è che BeginInvoke () è asincrono mentre Invoke () viene eseguito in modo sincrono. stackoverflow.com/questions/229554/...
frzsombor

179

Threading Model in UI

Leggere il modello di threading nelle applicazioni dell'interfaccia utente ( qui è presente il vecchio collegamento VB ) 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 ai suoi membri delle sottoclassi.
  • Il tentativo di accedere al membro di System.Windows.Forms.Control da un thread diverso da quello 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

  • L'overhead di calcolo del metodo invocato dovrebbe essere piccolo così come l'overhead di calcolo dei metodi di gestione degli eventi perché qui viene utilizzato il thread dell'interfaccia utente, lo stesso responsabile della gestione dell'input dell'utente. Indipendentemente se si tratta di System.Windows.Forms.Control.Invoke o System.Windows.Forms.Control.BeginInvoke .
  • Per eseguire operazioni di calcolo costose, utilizzare sempre thread separati. Dal momento che .NET 2.0 BackgroundWorker è dedicato all'esecuzione di costose operazioni di calcolo in Windows Form. Tuttavia, nelle nuove soluzioni è necessario utilizzare il modello async-waitit come descritto qui .
  • Utilizzare i metodi System.Windows.Forms.Control.Invoke o System.Windows.Forms.Control.BeginInvoke solo per aggiornare un'interfaccia utente. Se li usi per calcoli pesanti, la tua applicazione bloccherà:

inserisci qui la descrizione dell'immagine

Invocare

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 .



72

Si desidera utilizzare Invokeo solo BeginInvokeper il minimo lavoro necessario per modificare l'interfaccia utente. Il tuo metodo "pesante" dovrebbe essere eseguito su un altro thread (ad es. Via BackgroundWorker) ma usando Control.Invoke/ Control.BeginInvokesolo 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 - anche se l'articolo è stato scritto prima che BackgroundWorkerarrivasse sulla scena, e temo di non averlo aggiornato a tale riguardo. BackgroundWorkersemplifica semplicemente un po 'il callback.


qui in queste mie condizioni. non sto nemmeno cambiando l'interfaccia utente. Sto solo accedendo ai suoi valori attuali dal thread figlio. qualsiasi suggerimento hw da attuare
Prerak K,

1
Devi ancora eseguire il marshalling sul thread dell'interfaccia utente anche solo per accedere alle proprietà. Se il metodo non può continuare fino a quando non si accede al valore, è possibile utilizzare un delegato che restituisce il valore. Ma sì, passa attraverso il thread dell'interfaccia utente.
Jon Skeet,

Ciao Jon, credo che mi stai dirigendo nella giusta direzione. Sì, ho bisogno del valore senza di esso non posso procedere oltre. Per favore, potresti eloborare su questo "Utilizzo di un delegato che restituisce un valore". Grazie
Prerak K,

1
Utilizzare un delegato come Func <string>: string text = textbox1.Invoke ((Func <string>) () => textbox1.Text); (Supponiamo che tu stia usando C # 3.0 - altrimenti potresti usare un metodo anonimo.)
Jon Skeet,

45

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

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

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


1
Questo mi dà Invoke or BeginInvoke cannot be called on a control until the window handle has been created. L'ho risolto qui
rupweb il

42

Ho FileSystemWatcherriscontrato questo problema con 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.


2
Questo mi ha salvato la pancetta. In VB.NET ho usato.SynchronizingObject = Me
codingcoding il

20

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.


cosa c'è qui
Rawat,

@Rawat tin questo caso sarà textbox1- è passato come argomento
Rob

17

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 aggirare questo, è necessario invocare il controllo, che è ciò 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à.



10

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

10

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

CheckForIllegalCrossThreadCalls = false

nel Form1()costruttore.


9

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

Semplice! Grazie .
Ali Esmaeili,


7

Ne ho trovato la necessità durante la programmazione di un controller di app monotouch per iOS-Phone in un progetto prototipo di Win Studio Visual Studio 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);
            }
        }
    }
}

6

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

(InvokeRequired)

Ciò è utile se si lavora 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


5

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 di thread incrociati.

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


5

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

3

Stessa domanda: how-to-update-the-gui-from-another-thread-in-c

Due strade:

  1. Restituisce valore in e.result e usalo per impostare il valore della casella di testo nell'evento backgroundWorker_RunWorkerCompleted

  2. Dichiarare 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";
    }
}

2

Basta usare questo:

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

0

Azione y; // dichiarato all'interno della classe

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


0

Modo semplice e riutilizzabile per aggirare questo problema.

Metodo di estensione

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

Esempio di utilizzo

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

-3

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 usano i controlli ereditati dalla classe Control mentre SynchronizationContext può essere usato ovunque. Alcune informazioni utili sono i seguenti collegamenti

Interfaccia utente aggiornamento thread incrociato | .Netto

Interfaccia utente di aggiornamento di thread incrociati utilizzando SynchronizationContext | .Netto

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.