Nieprawidłowa operacja krzyżowa: dostęp uzyskany z wątku innego niż wątek, w którym został utworzony

584

Mam scenariusz (Windows Forms, C #, .NET)

  1. Istnieje główna forma, która obsługuje kontrolę użytkowników.
  2. Kontrola użytkownika wykonuje pewne ciężkie operacje na danych, tak że jeśli bezpośrednio wywołam UserControl_Loadmetodę, interfejs użytkownika przestanie odpowiadać na czas wykonywania metody ładowania.
  3. Aby temu zaradzić, ładuję dane do innego wątku (próbując jak najmniej zmienić istniejący kod)
  4. Użyłem wątku procesu roboczego w tle, który będzie ładował dane, a po zakończeniu powiadomi aplikację, że wykonała swoją pracę.
  5. Teraz przyszedł prawdziwy problem. Cały interfejs użytkownika (formularz główny i jego potomne kontrolki użytkownika) został utworzony w głównym wątku głównym. W metodzie LOAD kontrolera użytkownika pobieram dane na podstawie wartości niektórych formantów (takich jak pole tekstowe) na userControl.

Pseudokod wyglądałby tak:

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

Wyjątkiem było

Nieprawidłowa operacja krzyżowa: dostęp uzyskany z wątku innego niż wątek, w którym został utworzony.

Aby dowiedzieć się więcej na ten temat, zrobiłem trochę googlowania i pojawiła się sugestia, jak użycie następującego kodu

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

ALE ALE ALE ... wygląda na to, że wróciłem do pierwszego. Aplikacja ponownie przestaje odpowiadać. Wydaje się, że jest to spowodowane wykonaniem linii nr 1, jeśli warunek. Zadanie ładowania jest ponownie wykonywane przez wątek nadrzędny, a nie trzeci odrodzony.

Nie wiem, czy postrzegałem to dobrze, czy źle. Jestem nowy w wątkach.

Jak to rozwiązać, a także jaki jest efekt wykonania linii nr 1, jeśli blok?

Sytuacja jest następująca : chcę załadować dane do zmiennej globalnej na podstawie wartości kontrolki. Nie chcę zmieniać wartości kontrolki z wątku potomnego. Nie zrobię tego nigdy z dziecięcego wątku.

Tak więc dostęp tylko do wartości, aby odpowiednie dane mogły zostać pobrane z bazy danych.

Prerak K
źródło
W moim szczególnym przypadku tego błędu znalazłem obejście polegające na użyciu BackgroundWorker w formularzu do obsługi części kodu wymagających dużej ilości danych. (tj. umieść cały kod problemu w metodzie backgroundWorker1_DoWork () i wywołaj go za pomocą backgroundWorker1.RunWorkerAsync ()) ... Te dwa źródła wskazały mi właściwy kierunek: stackoverflow.com/questions/4806742/... youtube.com/ oglądać? v = MLrrbG6V1zM
Giollia 18.09.19

Odpowiedzi:

433

Zgodnie z komentarzem do aktualizacji Preraka K (od momentu usunięcia):

Chyba nie przedstawiłem pytania poprawnie.

Sytuacja jest następująca: chcę załadować dane do zmiennej globalnej na podstawie wartości kontrolki. Nie chcę zmieniać wartości kontrolki z wątku potomnego. Nie zrobię tego nigdy z dziecięcego wątku.

Tak więc dostęp tylko do wartości, aby odpowiednie dane mogły zostać pobrane z bazy danych.

Rozwiązanie, które chcesz, powinno wyglądać następująco:

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

Wykonaj poważne przetwarzanie w osobnym wątku, zanim spróbujesz wrócić do wątku kontrolki. Na przykład:

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;
        }
    }
}
Jeff Hubbard
źródło
1
Minęło trochę czasu, odkąd zrobiłem programowanie w języku C #, ale na podstawie artykułu MSDN i mojej niejednolitej wiedzy wygląda to tak.
Jeff Hubbard,
1
Różnica polega na tym, że BeginInvoke () jest asynchroniczny, a Invoke () działa synchronicznie. stackoverflow.com/questions/229554/…
frzsombor
178

Model gwintowania w interfejsie użytkownika

Przeczytaj Model wątków w aplikacjach interfejsu użytkownika, aby zrozumieć podstawowe pojęcia. Link prowadzi do strony opisującej model wątków WPF. Jednak Windows Forms wykorzystuje ten sam pomysł.

Wątek interfejsu użytkownika

  • Istnieje tylko jeden wątek (wątek interfejsu użytkownika), który ma dostęp do System.Windows.Forms.Control i członków jego podklas.
  • Próba uzyskania dostępu do elementu System.Windows.Forms.Control z innego wątku niż wątek interfejsu użytkownika spowoduje wyjątek między wątkami.
  • Ponieważ istnieje tylko jeden wątek, wszystkie operacje interfejsu użytkownika są kolejkowane jako elementy pracy w tym wątku:

wprowadź opis zdjęcia tutaj

wprowadź opis zdjęcia tutaj

Metody BeginInvoke i Invoke

  • Obciążenie obliczeniowe wywoływanej metody powinno być małe, podobnie jak obciążenie obliczeniowe metod obsługi zdarzeń, ponieważ używany jest tam wątek interfejsu użytkownika - taki sam, który jest odpowiedzialny za obsługę danych wejściowych użytkownika. Niezależnie od tego, czy jest to System.Windows.Forms.Control.Invoke czy System.Windows.Forms.Control.BeginInvoke .
  • Aby wykonać kosztowną operację obliczeniową, zawsze używaj osobnego wątku. Ponieważ .NET 2.0 BackgroundWorker jest przeznaczony do wykonywania kosztownych operacji obliczeniowych w Windows Forms. Jednak w nowych rozwiązaniach należy zastosować wzorzec asynchroniczny-oczekiwanie, jak opisano tutaj .
  • Użyj metod System.Windows.Forms.Control.Invoke lub System.Windows.Forms.Control.BeginInvoke tylko do aktualizacji interfejsu użytkownika. Jeśli użyjesz ich do ciężkich obliczeń, aplikacja zablokuje:

wprowadź opis zdjęcia tutaj

Odwołać się

wprowadź opis zdjęcia tutaj

BeginInvoke

wprowadź opis zdjęcia tutaj

Rozwiązanie kodu

Przeczytaj odpowiedzi na pytanie Jak zaktualizować GUI z innego wątku w C #? . Zalecane rozwiązanie dla C # 5.0 i .NET 4.5 znajduje się tutaj .

Ryszard Dżegan
źródło
72

Chcesz użyć Invokelub BeginInvoketylko minimalnej ilości pracy wymaganej do zmiany interfejsu użytkownika. Twoja „ciężka” metoda powinna zostać wykonana w innym wątku (np. Via BackgroundWorker), ale następnie użyj Control.Invoke/ Control.BeginInvokejust, aby zaktualizować interfejs użytkownika. W ten sposób Twój wątek interfejsu użytkownika będzie mógł obsługiwać zdarzenia interfejsu użytkownika itp.

Zobacz mój artykuł na temat wątków dla przykładu WinForms - chociaż artykuł został napisany przed BackgroundWorkerprzyjazdem na scenę i obawiam się, że nie zaktualizowałem go pod tym względem. BackgroundWorkerjedynie nieco upraszcza oddzwanianie.

Jon Skeet
źródło
tutaj w moim stanie. nawet nie zmieniam interfejsu użytkownika. Po prostu uzyskuję dostęp do jego bieżących wartości z wątku potomnego. wszelkie sugestie do wdrożenia
Prerak K
1
Nadal musisz przejść do wątku interfejsu użytkownika, aby uzyskać dostęp do właściwości. Jeśli metoda nie może być kontynuowana do momentu uzyskania dostępu do wartości, możesz użyć delegata, który zwraca wartość. Ale tak, przejdź przez wątek interfejsu użytkownika.
Jon Skeet
Cześć Jon, wierzę, że zmierzasz we właściwym kierunku. Tak, potrzebuję wartości bez tego, nie mogę kontynuować. Czy mógłbyś rozwinąć tę „Używanie delegata, który zwraca wartość”. Dzięki
Prerak K
1
Użyj delegata, takiego jak Func <ciąg>: ciąg tekstowy = pole tekstowe 1. Invoke ((Func <ciąg>) () => pole tekstowe 1. tekst); (Zakładając, że używasz C # 3.0 - w przeciwnym razie możesz użyć anonimowej metody.)
Jon Skeet
45

Wiem, że jest już za późno. Jednak nawet dzisiaj, jeśli masz problemy z dostępem do kontroli wątków? To jest najkrótsza odpowiedź do dnia: P

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

W ten sposób uzyskuję dostęp do dowolnej kontroli formularza z wątku.

Brawo
źródło
1
To mi daje Invoke or BeginInvoke cannot be called on a control until the window handle has been created. Rozwiązałem go tutaj
rupweb
42

Miałem ten problem z FileSystemWatcheri stwierdziłem, że następujący kod rozwiązał problem:

fsw.SynchronizingObject = this

Następnie formant używa bieżącego obiektu formularza do obsługi zdarzeń i dlatego będzie w tym samym wątku.

Peter C.
źródło
2
To uratowało mi boczek. W VB.NET użyłem.SynchronizingObject = Me
kodowanie kodowanie
20

Uważam, że kod sprawdzania i wywoływania, który musi być zaśmiecony we wszystkich metodach związanych z formularzami, jest zbyt szczegółowy i niepotrzebny. Oto prosta metoda rozszerzenia, która pozwala całkowicie ją zlikwidować:

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

A potem możesz po prostu to zrobić:

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

Nigdy więcej bałaganu - proste.

Rob
źródło
co to jest „t” tutaj
Rawat
@Rawat tw tym przypadku będzie textbox1- przekazany jako argument
Rob
17

Formanty w .NET nie są na ogół bezpieczne dla wątków. Oznacza to, że nie powinieneś uzyskiwać dostępu do kontrolki z wątku innego niż ten, w którym ona żyje. Aby obejść ten problem, musisz wywołać kontrolkę, co próbuje druga próbka.

Jednak w twoim przypadku wszystko, co zrobiłeś, to przekazać długoterminową metodę z powrotem do głównego wątku. Oczywiście to nie tak naprawdę chcesz robić. Musisz przemyśleć to trochę, aby wszystko, co robisz w głównym wątku, ustawiało szybką właściwość tu i tam.

Joel Coehoorn
źródło
10

Nowy wygląd za pomocą Async / Await i wywołań zwrotnych. Potrzebujesz tylko jednego wiersza kodu, jeśli zachowasz metodę rozszerzenia w swoim projekcie.

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

Możesz dodać inne rzeczy do metody Extension, takie jak zawijanie jej w instrukcji Try / Catch, pozwalając dzwoniącemu powiedzieć mu, jaki typ zwrócić po zakończeniu, wyjątek oddzwanianie do dzwoniącego:

Dodanie Try Catch, automatyczne rejestrowanie wyjątków i 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
        }
    }
John Peters
źródło
10

To nie jest zalecany sposób rozwiązania tego błędu, ale można go szybko ukryć, wykona zadanie. Wolę to w przypadku prototypów lub wersji demonstracyjnych. Dodaj

CheckForIllegalCrossThreadCalls = false

w Form1()konstruktorze.

Özgür
źródło
9

Postępuj zgodnie z najprostszym (moim zdaniem) sposobem modyfikowania obiektów z innego wątku:

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;
        }
    }
}
Vanderley Maia
źródło
Prosty ! dzięki .
Ali Esmaeili
7

Potrzebowałem tego podczas programowania kontrolera aplikacji monotouch na telefony z iOS w studiu wizualnym prototypowego projektu winforms poza Xamarin stuidio. Woląc programować w VS niż w Xamarin Studio, chciałem, aby kontroler był całkowicie oddzielony od szkieletu telefonu. W ten sposób wdrożenie tego dla innych platform, takich jak Android i Windows Phone, byłoby znacznie łatwiejsze do wykorzystania w przyszłości.

Chciałem rozwiązania, w którym GUI może reagować na zdarzenia bez obciążenia związanego z przełączaniem kodu przełączania wątków za każdym kliknięciem przycisku. Zasadniczo pozwól, aby kontroler klasy zajął się tym, aby kod klienta był prosty. Mógłbyś mieć wiele zdarzeń w GUI, tak jakbyś mógł poradzić sobie w jednym miejscu w klasie byłoby czystsze. Nie jestem ekspertem od wielu teorii. Daj mi znać, jeśli jest to wadliwe.

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

Formularz GUI nie jest świadomy, że kontroler wykonuje zadania asynchroniczne.

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);
            }
        }
    }
}
RandallTo
źródło
6

Oto alternatywny sposób, jeśli obiekt, z którym pracujesz, nie ma

(InvokeRequired)

Jest to przydatne, jeśli pracujesz z formularzem głównym w klasie innej niż formularz główny z obiektem, który jest w formie głównej, ale nie ma 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;
}

Działa tak samo jak powyżej, ale jest to inne podejście, jeśli nie masz obiektu z wywołaniem invokere, ale masz dostęp do MainForm

Ashitakalax
źródło
5

Wzdłuż tych samych wierszy, co poprzednie odpowiedzi, ale bardzo krótki dodatek, który pozwala korzystać ze wszystkich właściwości Control bez wyjątku wywołania wątku krzyżowego.

Metoda pomocnicza

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

Przykładowe użycie

// 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;
}
Mikrofon
źródło
5
this.Invoke(new MethodInvoker(delegate
            {
                //your code here;
            }));
Hamid Jolany
źródło
5

Na przykład, aby pobrać tekst z kontrolki wątku interfejsu użytkownika:

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
UrsulRosu
źródło
3

To samo pytanie: how-to-update-the-gui-from-another-thread-in-c

Dwie drogi:

  1. Zwróć wartość w e.result i użyj jej do ustawienia wartości pola tekstowego w zdarzeniu backgroundWorker_RunWorkerCompleted

  2. Zadeklaruj pewną zmienną, aby przechowywać tego rodzaju wartości w osobnej klasie (która będzie działać jako posiadacz danych). Utwórz statyczną instancję tej klasy i możesz uzyskać do niej dostęp w dowolnym wątku.

Przykład:

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";
    }
}
Saurabh
źródło
2

Po prostu użyj tego:

this.Invoke((MethodInvoker)delegate
            {
                YourControl.Property= value; // runs thread safe
            });
Hasan Shouman
źródło
0

Działanie y; // zadeklarowany w klasie

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

Antonio Leite
źródło
0

Prosty i wielokrotnego użytku sposób na obejście tego problemu.

Metoda przedłużenia

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

Przykładowe użycie

private void OnAnyEvent(object sender, EventArgs args)
{
    this.LoadOnUI(() =>
    {
        label1.Text = "";
        button1.Text = "";
    });
}
Timothy Macharia
źródło
-3

Istnieją dwie opcje operacji krzyżowych.

Control.InvokeRequired Property 

a drugi to użycie

SynchronizationContext Post Method

Control.InvokeRequired jest użyteczny tylko wtedy, gdy działające kontrolki odziedziczone z klasy Control, podczas gdy SynchronizationContext może być używany w dowolnym miejscu. Niektóre przydatne informacje to następujące linki

Interfejs aktualizacji wielowątkowej | .Netto

Interfejs aktualizacji wielowątkowej za pomocą SynchronizationContext | .Netto

Nasir Mahmood
źródło