Операция с несколькими потоками недопустима: управление осуществляется из потока, отличного от потока, в котором он был создан
У меня есть сценарий. (Windows Forms, C#, .NET)
- Существует основная форма, которая содержит некоторый пользовательский элемент управления.
- Пользовательский элемент управления выполняет некоторые тяжелые операции с данными, так что, если я напрямую вызываю
UserControl_Load
Метод пользовательского интерфейса перестает отвечать на запросы при выполнении метода загрузки. - Чтобы преодолеть это, я загружаю данные в другой поток (пытаясь как можно меньше изменить существующий код)
- Я использовал фоновый рабочий поток, который будет загружать данные и когда это будет сделано, уведомит приложение о том, что оно выполнило свою работу.
- Теперь появилась настоящая проблема. Весь пользовательский интерфейс (основная форма и ее дочерние элементы управления) был создан в основном основном потоке. В методе 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, если блок?
Ситуация такова: я хочу загрузить данные в глобальную переменную на основе значения элемента управления. Я не хочу менять значение элемента управления из дочернего потока. Я не собираюсь делать это когда-либо из детской ветки.
Таким образом, только доступ к значению, так что соответствующие данные могут быть получены из базы данных.
23 ответа
Согласно обновленному комментарию 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;
}
}
}
Модель потоков в пользовательском интерфейсе
Пожалуйста, ознакомьтесь с моделью потоков в приложениях пользовательского интерфейса, чтобы понять основные понятия. Ссылка переходит на страницу, которая описывает модель потоков WPF. Тем не менее, Windows Forms использует ту же идею.
Поток пользовательского интерфейса
- Существует только один поток (поток пользовательского интерфейса), которому разрешен доступ к элементам System.Windows.Forms.Control и его подклассам.
- Попытка получить доступ к элементу System.Windows.Forms.Control из потока, отличного от потока пользовательского интерфейса, вызовет межпотоковое исключение.
- Поскольку существует только один поток, все операции пользовательского интерфейса ставятся в очередь как рабочие элементы в этом потоке:
- Если для потока пользовательского интерфейса нет работы, то существуют незанятые промежутки, которые могут использоваться вычислениями, не относящимися к пользовательскому интерфейсу.
- Чтобы использовать указанные пробелы, используйте методы http://msdn.microsoft.com/en-us/library/System.Windows.Forms.Control.Invoke.aspx или System.Windows.Forms.Control.BeginInvoke:
Методы BeginInvoke и Invoke
- Затраты на обработку вызываемого метода должны быть небольшими, а также затраты на обработку методов-обработчиков событий, потому что там используется поток пользовательского интерфейса - тот же, что отвечает за обработку пользовательского ввода. Независимо от того, является ли это http://msdn.microsoft.com/en-us/library/System.Windows.Forms.Control.Invoke.aspx или System.Windows.Forms.Control.BeginInvoke.
- Для выполнения вычислительных дорогостоящих операций всегда используйте отдельный поток. Начиная с.NET 2.0 BackgroundWorker предназначен для выполнения дорогостоящих операций в Windows Forms. Однако в новых решениях вы должны использовать шаблон async-await, как описано здесь.
- Используйте методы http://msdn.microsoft.com/en-us/library/System.Windows.Forms.Control.Invoke.aspx или System.Windows.Forms.Control.BeginInvoke только для обновления пользовательского интерфейса. Если вы используете их для тяжелых вычислений, ваше приложение заблокирует:
взывать
- http://msdn.microsoft.com/en-us/library/System.Windows.Forms.Control.Invoke.aspx заставляет отдельный поток ожидать завершения вызванного метода:
BeginInvoke
- System.Windows.Forms.Control.BeginInvoke не заставляет отдельный поток ждать завершения вызванного метода:
Кодовое решение
Читайте ответы на вопрос Как обновить графический интерфейс из другого потока в C#?, Для C# 5.0 и.NET 4.5 рекомендуемое решение здесь.
Вы хотите использовать Invoke или BeginInvoke только для минимальной части работы, необходимой для изменения пользовательского интерфейса. Ваш "тяжелый" метод должен выполняться в другом потоке (например, через BackgroundWorker), но затем использовать Control.Invoke/Control.BeginInvoke просто для обновления пользовательского интерфейса. Таким образом, ваш поток пользовательского интерфейса будет свободен для обработки событий пользовательского интерфейса и т. Д.
См. Мою многопоточную статью для примера WinForms - хотя статья была написана до появления BackgroundWorker, и я боюсь, что я не обновил ее в этом отношении. BackgroundWorker просто немного упрощает обратный вызов.
Я знаю, что уже слишком поздно. Однако даже сегодня, если у вас возникли проблемы с доступом к элементам управления несколькими потоками? Это самый короткий ответ до даты:P
Invoke(new Action(() =>
{
label1.Text = "WooHoo!!!";
}));
Вот так я получаю доступ к любому элементу управления формы из потока.
У меня была эта проблема с FileSystemWatcher
и обнаружил, что следующий код решил проблему:
fsw.SynchronizingObject = this
Элемент управления затем использует текущий объект формы для обработки событий и, следовательно, будет в том же потоке.
Я считаю, что код проверки и вызова, который должен быть засорен во всех методах, связанных с формами, слишком многословен и не нужен. Вот простой метод расширения, который позволяет полностью избавиться от него:
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");
Нет больше возиться - просто.
Элементы управления в.NET не являются потокобезопасными. Это означает, что вы не должны обращаться к элементу управления из потока, отличного от того, в котором он находится. Чтобы обойти это, вам нужно вызвать элемент управления, который пытается выполнить ваш второй пример.
Тем не менее, в вашем случае все, что вы сделали, это передали долгосрочный метод обратно в основной поток. Конечно, это не совсем то, что вы хотите сделать. Вам нужно немного переосмыслить, чтобы все, что вы делаете в главном потоке, устанавливало быстрое свойство здесь и там.
Самое чистое (и правильное) решение для проблем многопоточности пользовательского интерфейса - это использование SynchronizationContext, см. Статью Синхронизация вызовов пользовательского интерфейса в статье о многопоточных приложениях, это очень хорошо объясняется.
Это не рекомендуемый способ устранения этой ошибки, но вы можете быстро ее устранить, это сработает. Я предпочитаю это для прототипов или демонстраций. добавлять
CheckForIllegalCrossThreadCalls = false
в Form1()
конструктор
Новый взгляд с использованием 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
}
}
Следуйте простейшему (на мой взгляд) способу изменения объектов из другого потока:
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;
}
}
}
Вам нужно взглянуть на пример Backgroundworker:
http://msdn.microsoft.com/en-us/library/system.componentmodel.backgroundworker.aspx Особенно о том, как он взаимодействует со слоем пользовательского интерфейса. Судя по вашим сообщениям, это, кажется, отвечает на ваши вопросы.
Простой и многоразовый способ решения этой проблемы.
Метод расширения
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 = "";
});
}
Я нашел в этом необходимость, когда программировал контроллер приложения 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");
}
}
Форма 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);
}
}
}
}
Вот альтернативный способ, если объект, с которым вы работаете, не имеет
(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
Например, чтобы получить текст из элемента управления потока пользовательского интерфейса:
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
В тех же строках, что и в предыдущих ответах, но очень короткое дополнение, позволяющее использовать все свойства элемента управления без исключения межпотокового вызова.
Вспомогательный метод
/// <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;
}));
Тот же вопрос: как обновлять графический интерфейс пользователя из другого потока в c
Два пути:
Верните значение в e.result и используйте его для установки значения текстового поля в событии backgroundWorker_RunWorkerCompleted
Объявите некоторую переменную для хранения таких значений в отдельном классе (который будет работать как держатель данных) . Создайте статический экземпляр этого класса, и вы можете получить к нему доступ через любой поток.
Пример:
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";
}
}
Просто используйте это:
this.Invoke((MethodInvoker)delegate
{
YourControl.Property= value; // runs thread safe
});
Существует два варианта операций с несколькими потоками.
Control.InvokeRequired Property
а второй использовать
SynchronizationContext Post Method
Control.InvokeRequired полезен только в том случае, если рабочие элементы управления унаследованы от класса Control, тогда как SynchronizationContext может использоваться где угодно. Некоторая полезная информация в виде следующих ссылок
Интерфейс Обновления Cross Thread | .Сеть
Интерфейс обновления Cross Thread с использованием SynchronizationContext | .Сеть
Я знаю, что опаздываю, но я думаю, что есть одно простое решение для решения этой проблемы. Может быть, это было бы полезно для других.
Ты можешь написать:
CheckforIllegalCrossThreadCalls
и установите его значение в
False
. Это нужно прописать в конструкторе главной страницы.
Обратитесь к этому:
Действие у; // объявлено внутри класса
label1.Invoke(у =()=> Label1.Text="текст");