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

У меня есть сценарий. (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, если блок?

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

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

Ответы [ 21 ]

400 голосов
/ 27 сентября 2008

Согласно комментарию к обновлению 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;
        }
    }
}
162 голосов
/ 26 сентября 2013

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

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

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

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

enter image description here

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

enter image description here

Методы BeginInvoke и Invoke

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

enter image description here

Invoke

enter image description here

BeginInvoke

enter image description here

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

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

70 голосов
/ 27 сентября 2008

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

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

40 голосов
/ 13 января 2009

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

fsw.SynchronizingObject = this

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

26 голосов
/ 17 октября 2017

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

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

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

16 голосов
/ 02 мая 2016

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

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

Больше не надо возиться - просто.

16 голосов
/ 27 сентября 2008

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

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

13 голосов
/ 27 апреля 2009

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

9 голосов
/ 16 мая 2016

Новый взгляд с использованием 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, Автоматическое ведение журнала исключений и 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
        }
    }
7 голосов
/ 06 мая 2016

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

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;
        }
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...