Недопустимая операция с перекрестным потоком:Элемент управления, доступный из потока, отличного от потока, в котором он был создан
-
02-07-2019 - |
Вопрос
У меня есть сценарий.(Windows Forms, C #, .NET)
- Существует основная форма, в которой размещен некоторый пользовательский элемент управления.
- Пользовательский элемент управления выполняет некоторые сложные операции с данными, так что если я напрямую вызываю
UserControl_Load
метод пользовательский интерфейс перестает отвечать на запросы на время выполнения метода загрузки. - Чтобы преодолеть это, я загружаю данные в другой поток (стараясь изменять существующий код как можно меньше).
- Я использовал фоновый рабочий поток, который будет загружать данные и по завершении уведомит приложение о том, что оно выполнило свою работу.
- Теперь возникла настоящая проблема.Весь пользовательский интерфейс (основная форма и ее дочерние пользовательские элементы управления) был создан в основном главном потоке.В методе ЗАГРУЗКИ usercontrol я извлекаю данные на основе значений некоторого элемента управления (например, текстового поля) в 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
}
}
НО, НО, НО...кажется, я вернулся к исходной точке.Приложение снова перестанет отвечать.Кажется, это связано с выполнением условия if строки #1.Задача загрузки снова выполняется родительским потоком, а не третьим, который я создал.
Я не знаю, правильно я это воспринял или неправильно.Я новичок в многопоточности.
Как мне решить эту проблему, а также каков эффект выполнения строки # 1, если блокируется?
Ситуация такова:Я хочу загрузить данные в глобальную переменную на основе значения элемента управления.Я не хочу изменять значение элемента управления из дочернего потока.Я никогда не собираюсь делать это из дочернего потока.
Таким образом, доступ к значению осуществляется только для того, чтобы соответствующие данные могли быть извлечены из базы данных.
Решение
Согласно Комментарий к обновлению Прерака К. (с момента удаления):
Наверное, я неправильно сформулировал вопрос.
Ситуация такова:Я хочу загрузить данные в глобальную переменную на основе значения элемента управления.Я не хочу изменять значение элемента управления из дочернего потока.Я никогда не собираюсь делать это из дочернего потока.
Таким образом, доступ к значению осуществляется только для того, чтобы соответствующие данные могли быть извлечены из базы данных.
Тогда решение, которое вы хотите получить, должно выглядеть следующим образом:
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;
}
}
}
Другие советы
Потоковая модель в пользовательском интерфейсе
Пожалуйста, прочтите Модель нарезания резьбы в приложениях пользовательского интерфейса для понимания основных концепций.Ссылка переходит на страницу, описывающую потоковую модель WPF.Однако Windows Forms использует ту же идею.
Поток пользовательского интерфейса
- Существует только один поток (UI thread), доступ к которому разрешен Система.Windows.Формы.Контроль и члены его подклассов.
- Попытка получить доступ к члену Система.Windows.Формы.Контроль из другого потока, отличного от потока пользовательского интерфейса, это вызовет исключение между потоками.
- Поскольку существует только один поток, все операции пользовательского интерфейса помещаются в очередь в качестве рабочих элементов в этот поток:
- Если нет работы для потока пользовательского интерфейса, то есть пробелы в работе, которые могут быть использованы вычислениями, не связанными с пользовательским интерфейсом.
- Чтобы использовать упомянутые пробелы, используйте Система.Windows.Формы.Управляйте.Вызывайте или Система.Windows.Формы.Контроль.BeginInvoke начать вызывать методы:
BeginInvoke и методы вызова
- Вычислительные издержки вызываемого метода должны быть небольшими, так же как и вычислительные издержки методов обработчика событий, поскольку там используется поток пользовательского интерфейса - тот самый, который отвечает за обработку пользовательского ввода.Независимо от того, является ли это Система.Windows.Формы.Управляйте.Вызывайте или Система.Windows.Формы.Контроль.BeginInvoke начать вызывать.
- Для выполнения дорогостоящей вычислительной операции всегда используйте отдельный поток.Начиная с .NET 2.0 Фоновый работник предназначен для выполнения дорогостоящих вычислительных операций в Windows Forms.Однако в новых решениях вы должны использовать шаблон async-await, как описано здесь.
- Использование Система.Windows.Формы.Управляйте.Вызывайте или Система.Windows.Формы.Контроль.BeginInvoke начать вызывать методы только для обновления пользовательского интерфейса.Если вы используете их для сложных вычислений, ваше приложение заблокирует:
Призывать
- Система.Windows.Формы.Управляйте.Вызывайте заставляет отдельный поток ожидать завершения вызванного метода:
BeginInvoke начать вызывать
- Система.Windows.Формы.Контроль.BeginInvoke начать вызывать не заставляет отдельный поток ждать завершения вызванного метода:
Кодовое решение
Прочитайте ответы на вопрос Как обновить графический интерфейс из другого потока в C #?.Для C # 5.0 и .NET 4.5 рекомендуемым решением является здесь.
Вы хотите использовать Invoke или BeginInvoke только для выполнения минимальной части работы, необходимой для изменения пользовательского интерфейса.Ваш "тяжелый" метод должен выполняться в другом потоке (напримерчерез BackgroundWorker), но затем с помощью Control.Вызовите / Control.BeginInvoke просто для обновления пользовательского интерфейса.Таким образом, ваш поток пользовательского интерфейса будет свободен для обработки событий пользовательского интерфейса и т.д.
Видишь мой резьбонарезное изделие для Пример WinForms - хотя статья была написана до того, как BackgroundWorker появился на сцене, и, боюсь, я не обновил ее в этом отношении.BackgroundWorker просто немного упрощает обратный вызов.
У меня была эта проблема с FileSystemWatcher
и обнаружил, что следующий код решил проблему:
fsw.SynchronizingObject = this
Затем элемент управления использует текущий объект формы для обработки событий и, следовательно, будет находиться в том же потоке.
Я знаю, что теперь уже слишком поздно.Однако даже сегодня у вас возникают проблемы с доступом к элементам управления перекрестными потоками?Это самый короткий ответ на сегодняшний день: P
Invoke(new Action(() =>
{
label1.Text = "WooHoo!!!";
}));
Вот как я получаю доступ к любому элементу управления формой из потока.
Элементы управления в .NET, как правило, не являются потокобезопасными.Это означает, что вы не должны получать доступ к элементу управления из потока, отличного от того, в котором он находится.Чтобы обойти это, вам нужно призывать контроль, который является тем, что пытается сделать ваш 2-й образец.
Однако в вашем случае все, что вы сделали, это передали долго работающий метод обратно в основной поток.Конечно, это не совсем то, что вы хотите сделать.Вам нужно немного переосмыслить это, чтобы все, что вы делали в основном потоке, - это устанавливали быстрое свойство здесь и там.
Я нахожу код проверки и вызова, который должен быть разбросан по всем методам, связанным с формами, слишком подробным и ненужным.Вот простой метод расширения, который позволяет вам полностью избавиться от него:
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");
Больше никакой возни - все просто.
Самое чистое (и правильное) решение проблем с межпоточностью пользовательского интерфейса - это использовать SynchronizationContext, см. Синхронизация вызовов пользовательского интерфейса в многопоточном приложении статья, в ней это очень хорошо объясняется.
Новый взгляд с использованием 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;
}
}
Вы можете добавить другие вещи к методу расширения, такие как обертывание его в операторе Try / Catch, позволяющем вызывающему объекту указывать, какой тип возвращать после завершения, обратный вызов исключения вызывающему объекту:
Добавление Try Catch, автоматического протоколирования исключений и обратного вызова
/// <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
}
}
Вам нужно посмотреть на пример Backgroundworker:
http://msdn.microsoft.com/en-us/library/system.componentmodel.backgroundworker.aspx
Особенно то, как он взаимодействует со слоем пользовательского интерфейса.Судя по вашему сообщению, это, похоже, отвечает на ваши вопросы.
Следуйте самому простому (на мой взгляд) способу изменения объектов из другого потока:
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;
}
}
}
Я обнаружил необходимость в этом при программировании контроллера приложения monotouch для iOS-телефонов в проекте прототипа Visual Studio winforms за пределами xamarin stuidio.Предпочитая программировать в VS, а не в xamarin studio, насколько это возможно, я хотел, чтобы контроллер был полностью отделен от платформы phone.Таким образом, реализовать это для других фреймворков, таких как Android и Windows Phone, было бы намного проще для будущего использования.
Мне нужно было решение, в котором графический интерфейс мог бы реагировать на события без необходимости иметь дело с кодом переключения между потоками при каждом нажатии кнопки.В принципе, позвольте контроллеру класса справиться с этим, чтобы упростить клиентский код.Возможно, у вас могло бы быть много событий в графическом интерфейсе, где, как если бы вы могли обработать это в одном месте в классе, было бы чище.Я не специалист по многозначности, дайте мне знать, если в этом есть недостатки.
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");
}
}
Форма GUI не знает, что контроллер выполняет асинхронные задачи.
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);
}
}
}
}
Это не рекомендуемый способ устранения этой ошибки, но вы можете быстро подавить ее, она сделает свое дело .Я предпочитаю это для прототипов или демонстраций .Добавить
CheckForIllegalCrossThreadCalls = false
в Form1()
конструктор .
Вот альтернативный способ, если объект, с которым вы работаете, не имеет
(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
Аналогично предыдущим ответам, но очень короткое дополнение, которое позволяет использовать все свойства элемента управления без исключения при вызове перекрестного потока.
Вспомогательный метод
/// <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;
}
this.Invoke(new MethodInvoker(delegate
{
//your code here;
}));
Например, чтобы получить текст из элемента управления потока пользовательского интерфейса:
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
Тот же вопрос :какобновитьграфическийинтерфейспользователяиздругогопотокавc
Два способа:
Верните значение в e.result и используйте его для установки значения текстового поля в событии backgroundworker_runworker completed
Объявите некоторую переменную для хранения такого рода значений в отдельном классе (который будет работать как держатель данных) .Создайте статический экземпляр этого класса, и вы сможете получить к нему доступ из любого потока.
Пример:
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";
}
}
Действие y;//объявленный внутри класса
label1.Вызвать(y=()=>label1.Text="текст");
Просто используйте это:
this.Invoke((MethodInvoker)delegate
{
YourControl.Property= value; // runs thread safe
});
Существует два варианта выполнения операций с перекрестными потоками.
Control.InvokeRequired Property
и второй - использовать
SynchronizationContext Post Method
Control.InvokeRequired полезен только при работе с элементами управления, унаследованными от класса Control, в то время как SynchronizationContext можно использовать где угодно.Некоторая полезная информация представлена по следующим ссылкам
Перекрестное обновление пользовательского интерфейса | .Net
Перекрестное обновление пользовательского интерфейса с помощью SynchronizationContext | .Net