Недопустимая операция между потоками: доступ к элементу управления из потока, отличного от потока, в котором он был создан

584

У меня есть сценарий. (Windows Forms, C #, .NET)

  1. Существует основная форма, которая содержит некоторый пользовательский элемент управления.
  2. Пользовательский элемент управления выполняет некоторую сложную операцию с данными, так что, если я напрямую вызываю UserControl_Loadметод, пользовательский интерфейс перестает отвечать на запросы при выполнении метода загрузки.
  3. Чтобы преодолеть это, я загружаю данные в другой поток (пытаясь как можно меньше изменить существующий код)
  4. Я использовал фоновый рабочий поток, который будет загружать данные и когда это будет сделано, уведомит приложение о том, что оно выполнило свою работу.
  5. Теперь появилась настоящая проблема. Весь пользовательский интерфейс (основная форма и ее дочерние элементы управления) был создан в основном основном потоке. В методе LOAD пользовательского контроля я выбираю данные, основанные на значениях некоторого элемента управления (например, текстового поля) в userControl.

Псевдокод будет выглядеть так:

КОД 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.
    }
}

Это исключение было

Недопустимая операция между потоками: доступ к элементу управления из потока, отличного от потока, в котором он был создан.

Чтобы узнать больше об этом, я немного погуглил, и пришло предложение, например, с использованием следующего кода

Код 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
    }
}

НО НО ... кажется, я вернулся к исходной точке. Приложение снова перестает отвечать на запросы. Кажется, это связано с выполнением строки # 1, если условие. Задача загрузки снова выполняется родительским потоком, а не третьим, который я породил.

Я не знаю, понял ли я это правильно или неправильно. Я новичок в потоке.

Как мне решить это, а также каков эффект выполнения строки # 1, если блок?

Ситуация такова : я хочу загрузить данные в глобальную переменную на основе значения элемента управления. Я не хочу менять значение элемента управления из дочернего потока. Я не собираюсь делать это когда-либо из детской ветки.

Так что доступ только к значению, так что соответствующие данные могут быть получены из базы данных.

Прерак К
источник
Для моего конкретного случая этой ошибки я нашел обходной путь, чтобы использовать BackgroundWorker в форме для обработки частей кода, интенсивно использующих данные. (т.е. поместите весь код проблемы в метод backgroundWorker1_DoWork () и вызовите его через backgroundWorker1.RunWorkerAsync ()) ... Эти два источника указали мне правильное направление: stackoverflow.com/questions/4806742/… youtube.com/ смотреть? v = MLrrbG6V1zM
Джоллия

Ответы:

433

Согласно обновленному комментарию Prerak K (так как удалено):

Я думаю, что я не представил вопрос правильно.

Ситуация такова: я хочу загрузить данные в глобальную переменную на основе значения элемента управления. Я не хочу менять значение элемента управления из дочернего потока. Я не собираюсь делать это когда-либо из детской ветки.

Таким образом, только доступ к значению, так что соответствующие данные могут быть получены из базы данных.

Решение, которое вы хотите, должно выглядеть следующим образом:

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

Выполните серьезную обработку в отдельном потоке, прежде чем пытаться переключиться обратно на поток элемента управления. Например:

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
Прошло много времени с тех пор, как я занимался программированием на C #, но, судя по статье MSDN и моим разрозненным знаниям, это выглядит так.
Джефф Хаббард
1
Разница в том, что BeginInvoke () является асинхронным, а Invoke () работает синхронно. stackoverflow.com/questions/229554/…
frzsombor
179

Модель потоков в пользовательском интерфейсе

Пожалуйста, прочитайте Модель потоков в приложениях пользовательского интерфейса ( старая ссылка на VB здесь ), чтобы понять основные понятия. Ссылка переходит на страницу, которая описывает модель потоков WPF. Однако Windows Forms использует ту же идею.

Поток пользовательского интерфейса

  • Существует только один поток (поток пользовательского интерфейса), которому разрешен доступ к элементам System.Windows.Forms.Control и его подклассам.
  • Попытка получить доступ к элементу System.Windows.Forms.Control из потока, отличного от потока пользовательского интерфейса, вызовет межпотоковое исключение.
  • Поскольку существует только один поток, все операции пользовательского интерфейса ставятся в очередь как рабочие элементы в этом потоке:

введите описание изображения здесь

  • Если для потока пользовательского интерфейса нет работы, то существуют незанятые промежутки, которые могут использоваться вычислениями, не относящимися к пользовательскому интерфейсу.
  • Чтобы использовать упомянутые пробелы, используйте методы System.Windows.Forms.Control.Invoke или System.Windows.Forms.Control.BeginInvoke :

введите описание изображения здесь

Методы BeginInvoke и Invoke

  • Затраты на обработку вызываемого метода должны быть небольшими, а также затраты на обработку методов-обработчиков событий, потому что там используется поток пользовательского интерфейса - тот же, что отвечает за обработку пользовательского ввода. Независимо от того, является ли это System.Windows.Forms.Control.Invoke или System.Windows.Forms.Control.BeginInvoke .
  • Для выполнения вычислительных дорогостоящих операций всегда используйте отдельный поток. Начиная с .NET 2.0 BackgroundWorker предназначен для выполнения дорогостоящих операций в Windows Forms. Однако в новых решениях вы должны использовать шаблон async-await, как описано здесь .
  • Используйте методы System.Windows.Forms.Control.Invoke или System.Windows.Forms.Control.BeginInvoke только для обновления пользовательского интерфейса. Если вы используете их для тяжелых вычислений, ваше приложение заблокирует:

введите описание изображения здесь

Invoke

введите описание изображения здесь

BeginInvoke

введите описание изображения здесь

Кодовое решение

Читайте ответы на вопрос Как обновить графический интерфейс из другого потока в C #? , Для C # 5.0 и .NET 4.5 рекомендуемое решение здесь .

Рышард Деган
источник
72

Вы хотите только использовать InvokeилиBeginInvoke для голого минимальной части работ , необходимых для изменения пользовательского интерфейса. Ваш "тяжелый" метод должен выполняться в другом потоке (например, через BackgroundWorker), но затем использовать Control.Invoke/ Control.BeginInvokeпросто для обновления пользовательского интерфейса. Таким образом, ваш поток пользовательского интерфейса будет свободен для обработки событий пользовательского интерфейса и т. Д.

См. Мою многопоточную статью для примера WinForms - хотя статья была написана до того, как BackgroundWorkerпоявилась на сцене, и я боюсь, что я не обновил ее в этом отношении. BackgroundWorkerпросто немного упрощает обратный вызов

Джон Скит
источник
вот в таком состоянии мое. Я даже не меняю пользовательский интерфейс. Я просто доступ к его текущие значения из дочернего потока. любое предложение hw для реализации
Prerak K
1
Вам все еще нужно маршалировать в поток пользовательского интерфейса даже для доступа к свойствам. Если ваш метод не может продолжаться до тех пор, пока не будет получен доступ к значению, вы можете использовать делегат, который возвращает значение. Но да, иди через поток пользовательского интерфейса.
Джон Скит
Привет, Джон, я верю, что ты направляешь меня в правильном направлении. Да, мне нужно значение без него, я не могу продолжать дальше. Пожалуйста, не могли бы вы выразить свое мнение об этом «Использование делегата, который возвращает значение». Спасибо
Прерак К
1
Используйте делегат, такой как Func <string>: string text = textbox1.Invoke ((Func <string>) () => textbox1.Text); (Предполагается, что вы используете C # 3.0 - в противном случае вы можете использовать анонимный метод.)
Джон Скит
45

Я знаю, что уже слишком поздно. Однако даже сегодня, если у вас возникают проблемы с доступом к элементам управления несколькими потоками? Это самый короткий ответ до даты: P

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

Вот так я получаю доступ к любому элементу управления формы из потока.

Браво
источник
1
Это дает мне Invoke or BeginInvoke cannot be called on a control until the window handle has been created. Я решил это здесь
rupweb
42

У меня была эта проблема с FileSystemWatcherи обнаружил, что следующий код решил эту проблему:

fsw.SynchronizingObject = this

Элемент управления затем использует текущий объект формы для обработки событий и, следовательно, будет в том же потоке.

Питер С
источник
2
Это спасло мой бекон. В VB.NET я использовал.SynchronizingObject = Me
кодирование
20

Я считаю, что код проверки и вызова, который необходимо замусорить во всех методах, связанных с формами, слишком многословен и не нужен. Вот простой метод расширения, который позволяет полностью избавиться от него:

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

И тогда вы можете просто сделать это:

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

Нет больше возиться - просто.

Роб
источник
что тут т
Рават
@Rawat tв этом случае будет textbox1- это передано в качестве аргумента
Роб
17

Элементы управления в .NET не являются потокобезопасными. Это означает, что вы не должны обращаться к элементу управления из потока, отличного от того, в котором он находится. Чтобы обойти это, вам нужно вызвать элемент управления, который пытается ваш второй пример.

Тем не менее, в вашем случае все, что вы сделали, это передали долгосрочный метод обратно в основной поток. Конечно, это не совсем то, что вы хотите сделать. Вам нужно немного переосмыслить, чтобы все, что вы делаете в главном потоке, устанавливало быстрое свойство здесь и там.

Джоэл Коухорн
источник
15

Самое чистое (и правильное) решение для проблем с многопоточностью пользовательского интерфейса - это использование SynchronizationContext, см. Статью Синхронизация вызовов пользовательского интерфейса в статье о многопоточном приложении , это очень хорошо объясняется.

Игорь Брейц
источник
10

Новый взгляд с использованием Async / Await и обратных вызовов. Вам нужна только одна строка кода, если вы сохраняете метод расширения в своем проекте.

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

Вы можете добавить другие вещи в метод Extension, например, обернуть его в оператор Try / Catch, что позволит вызывающей стороне сообщить ему, какой тип возвращать после завершения, обратный вызов исключительной ситуации для вызывающей стороны:

Добавление Try Catch, Auto Exception Logging и 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

Это не рекомендуемый способ устранения этой ошибки, но вы можете быстро ее устранить, это сделает работу. Я предпочитаю это для прототипов или демонстраций. Добавить

CheckForIllegalCrossThreadCalls = false

в Form1()конструкторе.

Özgür
источник
9

Следуйте простейшему (на мой взгляд) способу изменения объектов из другого потока:

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;
        }
    }
}
Вандерли Майя
источник
Просто ! Спасибо .
Али Эсмаейли
7

Я нашел в этом необходимость, когда программировал контроллер приложения iOS-Phone monotouch в визуальной студии. Проект-образец winforms за пределами xamarin stuidio. Предпочитая максимально программировать в VS поверх студии xamarin, я хотел, чтобы контроллер был полностью отделен от каркаса телефона. Таким образом, реализация этого для других платформ, таких как Android и Windows Phone, будет намного проще для использования в будущем.

Я хотел найти решение, в котором графический интерфейс мог бы реагировать на события без бремени обработки кода переключения между потоками за каждым нажатием кнопки. По сути, пусть контроллер класса справится с этим, чтобы клиентский код был простым. Вы могли бы иметь много событий в GUI, где, как если бы вы могли обрабатывать это в одном месте в классе, было бы чище. Я не многопрофильный эксперт, дайте мне знать, если это неправильно.

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

Форма графического интерфейса не знает, что контроллер выполняет асинхронные задачи.

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
источник
6

Вот альтернативный способ, если объект, с которым вы работаете, не имеет

(InvokeRequired)

Это полезно, если вы работаете с основной формой в классе, отличном от основной формы, с объектом, который находится в главной форме, но не имеет 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;
}

Он работает так же, как и выше, но это другой подход, если у вас нет объекта с invokerequired, но есть доступ к MainForm

Ashitakalax
источник
5

В том же ключе, что и в предыдущих ответах, но очень короткое дополнение, позволяющее использовать все свойства элемента управления без исключения межпоточного вызова

Вспомогательный метод

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

Образец использования

// 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
this.Invoke(new MethodInvoker(delegate
            {
                //your code here;
            }));
Хамид Джолани
источник
5

Например, чтобы получить текст из элемента управления потока пользовательского интерфейса:

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
источник
3

Тот же вопрос: как обновлять графический интерфейс пользователя из другого потока в c

Два пути:

  1. Верните значение в e.result и используйте его для установки значения текстового поля в событии backgroundWorker_RunWorkerCompleted

  2. Объявите некоторую переменную для хранения таких значений в отдельном классе (который будет работать как держатель данных). Создайте статический экземпляр этого класса, и вы можете получить к нему доступ через любой поток.

Пример:

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

Просто используйте это:

this.Invoke((MethodInvoker)delegate
            {
                YourControl.Property= value; // runs thread safe
            });
Хасан Шуман
источник
0

Действие у; // объявлено внутри класса

label1.Invoke (у = () => Label1.Text = "текст");

Антонио Лейте
источник
0

Простой и многократно используемый способ обойти эту проблему.

Метод расширения

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

Образец использования

private void OnAnyEvent(object sender, EventArgs args)
{
    this.LoadOnUI(() =>
    {
        label1.Text = "";
        button1.Text = "";
    });
}
Тимофей мачария
источник
-3

Существует два варианта операций с несколькими потоками.

Control.InvokeRequired Property 

а второй использовать

SynchronizationContext Post Method

Control.InvokeRequired полезен только в том случае, если рабочие элементы управления унаследованы от класса Control, тогда как SynchronizationContext может использоваться где угодно. Некоторая полезная информация в виде следующих ссылок

Интерфейс Обновления Cross Thread | .Сеть

Интерфейс обновления Cross Thread с использованием SynchronizationContext | .Сеть

Насир Махмуд
источник