В программировании C# Winform неправильно обновлять элементы управления пользовательского интерфейса непосредственно между потоками, и часто будет появляться исключение «межпоточная операция недействительна: доступ к ней из потока, который не является тем, который создал элемент управления». Существует четыре распространенных способа обновления элементов управления пользовательского интерфейса Winform в потоках:
-
Обновление через метод Post/Send SynchronizationContext потока пользовательского интерфейса;
-
Обновление с помощью метода Invoke/BegainInvoke элемента управления пользовательского интерфейса;
-
Выполнять асинхронные операции, заменяя Thread на BackgroundWorker;
-
Избегайте «исключения операции с несколькими потоками», установив свойство формы и отменив проверку безопасности потоков (не потокобезопасно, не рекомендуется).
Следующие примеры иллюстрируют применение трех вышеуказанных методов, и мы надеемся, что они будут полезны учащимся, плохо знакомым с C# Winform.
Смысл обмена и общения прописан в форме, а уровень боязливости ограничен.Если есть ошибки в понимании и выражении в тексте, прошу покритиковать и исправить. -
Обновление с помощью метода Post/Send SynchronizationContext потока пользовательского интерфейса
Использование: // Это разделено на три шага
//Первый шаг: получаем контекст синхронизации UI-потока (в конструкторе формы или в событии FormLoad)
///
/// Контекст синхронизации для UI-потока
///
SynchronizationContext m_SyncContext = null;
public Form1()
{
InitializeComponent();
//Получить контекст синхронизации потока пользовательского интерфейса
m_SyncContext = SynchronizationContext.Current;
//Control.CheckForIllegalCrossThreadCalls = false;
}
//Шаг 2: Определяем основной метод потока
///
/// Основной метод потока
///
private void ThreadProcSafePost()
{
//...выполнение задач потока
//Обновить пользовательский интерфейс в потоке (контекст синхронизации m_SyncContext через поток пользовательского интерфейса)
m_SyncContext.Post(SetTextSafePost, «Этот текст был безопасно установлен SynchronizationContext-Post.»);
//...Выполнение других задач потока
}
//Шаг 3: Определите метод обновления элементов управления пользовательского интерфейса
///
/// Метод для обновления содержимого текстового поля
///
///
private void SetTextSafePost(object text)
{
this.textBox1.Text = text.ToString();
}
//После этого запускаем поток
///
/// Событие кнопки запуска потока
///
///
///
private void setSafePostBtn_Click(object sender, EventArgs e)
{
this.demoThread = new Thread(new ThreadStart(this.ThreadProcSafePost));
this.demoThread.Start();
}
Объяснение: Три части, выделенные жирным шрифтом, являются ключом. Основной принцип этого метода: в процессе выполнения потока данные, которые необходимо обновить в UI-контроле, уже не обновляются напрямую, а данные отправляются в UI в виде асинхронных/синхронных сообщений через Метод Post/Send контекста потока пользовательского интерфейса Очередь сообщений потока; после того, как поток пользовательского интерфейса получает сообщение, он решает обновить свой собственный элемент управления напрямую, вызывая метод SetTextSafePost асинхронным/синхронным образом в зависимости от того, является ли сообщение асинхронным. сообщение или синхронное сообщение.
По сути, сообщение, отправляемое в UI-поток, представляет собой не простые данные, а команду вызова делегата.
//Обновить пользовательский интерфейс в потоке (контекст синхронизации m_SyncContext через поток пользовательского интерфейса)
m_SyncContext.Post(SetTextSafePost, «Этот текст был безопасно установлен SynchronizationContext-Post.»);
Эту строку кода можно интерпретировать так: отправьте асинхронное сообщение в контекст синхронизации (m_SyncContext) потока пользовательского интерфейса (поток пользовательского интерфейса, после получения сообщения, выполните делегат асинхронно, вызовите метод SetTextSafePost, параметр "this текст был..."").
2. Обновление с помощью метода Invoke/BegainInvoke элемента управления пользовательского интерфейса.
Использование: Подобно методу 1, его можно разделить на три этапа.
// Есть три шага
// Шаг 1: Определяем тип делегата
// Тип делегата интерфейса, который будет обновлять текст
delegate void SetTextCallback(string text);
//Шаг 2: Определяем основной метод потока
///
/// Основной метод потока
///
private void ThreadProcSafe()
{
//...выполнение задач потока
// Обновляем UI в потоке (с помощью метода .Invoke элемента управления)
this.SetText("Этот текст был установлен безопасно.");
//...Выполнение других задач потока
}
//Шаг 3: Определите метод обновления элементов управления пользовательского интерфейса
///
/// Метод для обновления содержимого текстового поля
///
///
private void SetText(string text)
{
// InvokeRequired required compares the thread ID of the
// calling thread to the thread ID of the creating thread.
// If these threads are different, it returns true.
if (this.textBox1.InvokeRequired)//Истина, если поток, вызывающий элемент управления, и поток, создавший элемент управления, не совпадают
{
while (!this.textBox1.IsHandleCreated)
{
// Решаем исключение "Доступ к выпущенному дескриптору" при закрытии формы
if (this.textBox1.Disposing || this.textBox1.IsDisposed)
return;
}
SetTextCallback d = new SetTextCallback(SetText);
this.textBox1.Invoke(d, new object[] { text });
}
else
{
this.textBox1.Text = text;
}
}
//После этого запускаем поток
///
/// Событие кнопки запуска потока
///
///
///
private void setTextSafeBtn_Click(
object sender,
EventArgs e)
{
this.demoThread =
new Thread(new ThreadStart(this.ThreadProcSafe));
this.demoThread.Start();
}
Описание: этот метод в настоящее время является основным методом, используемым для обновления пользовательского интерфейса между потоками.Используя метод Invoke/BegainInvoke элемента управления, делегат передается в поток пользовательского интерфейса для вызова для достижения потокобезопасных обновлений. Принцип аналогичен методу 1. По сути, сообщение, которое должно быть отправлено в поток, вызывается и делегируется в поток пользовательского интерфейса для обработки через управляющий дескриптор.
Свойство Control.InvokeRequired получает значение, указывающее, должен ли вызывающий объект вызывать метод Invoke при выполнении вызова метода для элемента управления, поскольку вызывающий объект находится в потоке, отличном от потока, создавшего элемент управления. Значение true, если дескриптор элемента управления был создан в потоке, отличном от вызывающего потока (указывает, что вы должны вызывать элемент управления с помощью метода вызова), в противном случае — значение false.
Элементы управления в Windows Forms привязаны к определенному потоку и не являются потокобезопасными. Поэтому, если метод элемента управления вызывается из другого потока, один из методов вызова элемента управления должен использоваться для маршалинга вызова в соответствующий поток. Это свойство можно использовать для определения необходимости вызова метода вызова, что полезно, когда неизвестно, какой поток владеет элементом управления.
3. Выполните асинхронные операции, заменив Thread на BackgroundWorker.
Использование: // Это разделено на три шага
//Первый шаг: определить объект BackgroundWorker и зарегистрировать событие (выполнить тело потока, выполнить событие обновления пользовательского интерфейса)
private BackgroundWorker backgroundWorker1 =null;
public Form1()
{
InitializeComponent();
backgroundWorker1 = new System.ComponentModel.BackgroundWorker();
//Установить обновление отчета о ходе выполнения
backgroundWorker1.WorkerReportsProgress = true;
//Регистрация метода тела потока
backgroundWorker1.DoWork += new DoWorkEventHandler(backgroundWorker1_DoWork);
//Регистрация метода обновления пользовательского интерфейса
backgroundWorker1.ProgressChanged += new ProgressChangedEventHandler(backgroundWorker1_ProgressChanged);
//backgroundWorker1.RunWorkerCompleted += new System.ComponentModel.RunWorkerCompletedEventHandler(this.backgroundWorker1_RunWorkerCompleted);
}
//Шаг 2: Определяем главное событие потока выполнения
//основной метод потока
public void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
//...выполнение задач потока
// Обновляем UI в потоке (через метод ReportProgress)
backgroundWorker1.ReportProgress(50, «Этот текст был безопасно установлен BackgroundWorker.»);
//...Выполнение других задач потока
}
//Шаг 3: Определите и выполните событие обновления пользовательского интерфейса
//метод обновления пользовательского интерфейса
public void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
this.textBox1.Text = e.UserState.ToString();
}
//После этого запускаем поток
//Запускаем backgroundWorker
private void setTextBackgroundWorkerBtn_Click(object sender, EventArgs e)
{
this.backgroundWorker1.RunWorkerAsync();
}
Описание: BackgroundWorker — хороший выбор при выполнении асинхронных задач в C# Winform. Это продукт идеи EAP (асинхронный шаблон, основанный на событиях). DoWork используется для выполнения асинхронных задач. Во время или после выполнения задачи мы можем выполнять потокобезопасные обновления пользовательского интерфейса с помощью событий ProgressChanged и ProgressCompleteded.
Примечание: // установить обновление отчета о ходе выполнения
backgroundWorker1.WorkerReportsProgress = true;
По умолчанию BackgroundWorker не сообщает о ходе выполнения и должен отображать и задавать свойство отчета о ходе выполнения. -
Избегайте «недопустимого исключения межпоточной операции», установив свойство формы и отменив проверку безопасности потока.
Использование: задайте для статического свойства CheckForIllegalCrossThreadCalls класса Control значение false.
public Form1()
{
InitializeComponent();
// Указывает, что вызовы неправильного потока больше не перехватываются
Control.CheckForIllegalCrossThreadCalls = false;
}
Описание. Установив свойство CheckForIllegalCrossThreadCalls, вы можете указать, следует ли перехватывать исключения небезопасных операций между потоками. Значение этого свойства по умолчанию равно true, то есть небезопасной операцией между потоками является перехват исключений (исключение "межпоточная операция недопустима"). Исключение просто маскируется установкой этого свойства в false. Control.CheckForIllegalCrossThreadCalls аннотируется следующим образом
//
// Резюме:
// Получает или задает значение, указывающее, следует ли перехватывать вызовы потока ошибок, которые обращаются к System.Windows.Forms.Control.Handle элемента управления при отладке приложения
// Атрибуты.
//
// возвращаем результат:
// true, если был перехвачен вызов не того потока, в противном случае — false.
[EditorBrowsable(EditorBrowsableState.Advanced)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[SRDescription("ControlCheckForIllegalCrossThreadCalls")]
[Browsable(false)]
public static bool CheckForIllegalCrossThreadCalls { get; set; }
Другой пример вызова элементов управления Windows Forms между потоками
Если операция потока неверна, будет создано исключение InvalidOperationException при вызове элемента управления Windows Forms между потоками.
Исключение говорит [Недопустимая операция между потоками: доступ к элементу управления 'listBox1' был получен из потока, отличного от потока, создавшего его.].
Я полагаю, что многие люди отключают перехват вызовов в неправильный поток, устанавливая для свойства Control.CheckForIllegalCrossThreadCalls значение false.
Этот обязательный запрет на захват не является гуманным вариантом.
Мы можем вызывать элементы управления Windows Forms через потоки с помощью метода Invoke элемента управления.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Threading;
namespace VJSDN.Tech.ThreadAccess
{
// Прототипы делегатов методов, вызываемые между потоками.
public delegate void ShowMessageMethod(string msg);
// Прототипы делегатов методов, вызываемые между потоками.
public delegate void CreateControlMethod();
//Примечание: поток: основной поток Основной поток относится к потоку, в котором находится текущая форма.
// Поток B: Поток, созданный пользователем.
// Поток C: Поток с параметрами.
public partial class Form1 : Form
{
private Thread _threadB = null;//Второй поток.Создание элементов управления.
private Thread _threadC = null;//Третий поток.Поток с параметрами.
private Button _btnOnB = null; //Управление кнопкой, созданное вторым потоком.
private ListBox _listBoxOnB = null;//элемент управления ListBox, созданный вторым потоком.
private Panel _PanelOnB = null;//Панель управления, созданная вторым потоком.
public Form1()
{
InitializeComponent();
// Следует ли перехватывать вызовы потока ошибок, которые обращаются к свойству System.Windows.Forms.Control.Handle элемента управления.
//Control.CheckForIllegalCrossThreadCalls = false;
}
private void btnCreateThreadB_Click(object sender, EventArgs e)
{
//Создать поток B внутри основного потока.
_threadB = new Thread(new ThreadStart(MethodThreadB)); // Запускаем поток.
_threadB.Start();
}
private void createC_Click(object sender, EventArgs e)
{
//Создаем поток C с параметрами в основном потоке.
_threadC = new Thread(new ParameterizedThreadStart(MethodThreadC));
_threadC.Start(100);//Переносим параметр 100 в поток C и вычисляем сумму чисел в пределах 100.
}
// C-поток работает...
private void MethodThreadC(object param)
{
int total = int.Parse(param.ToString());
this.Invoke(new ShowMessageMethod(this.ShowMessage), "Поток C подсчитывается: 1+2+n=?(nint результат = 0;//счетчик
for (int i = 1; i <= total; i++) result += i;
this.Invoke(new ShowMessageMethod(this.ShowMessage), "Результат вычисления потока C:" + result.ToString());
}
//Выполняется поток B...
private void MethodThreadB()
{
//Межпоточная операция: попытка вставить дочерний элемент управления в элемент управления panel2 потока A в потоке B.
this.Invoke(новый CreateControlMethod(this.CreatePanelOnThreadB));//Создать панель
this.Invoke(new CreateControlMethod(this.CreateButtonOnThreadB));//Создать кнопку
this.Invoke(новый CreateControlMethod(this.CreateListBoxOnThreadB));//Создать ListBox
this.Invoke(new ShowMessageMethod(this.ShowMessage), "Поток B управляет элементом управления ListBox в потоке A");
this.Invoke(new ShowMessageMethod(this.ShowMessage), "Если сообщение может быть отображено, операция выполнена успешно!");
}
//Примечание. Этот метод аналогичен прототипу делегата CreateControlMethod.
private void CreateControlCross(Label lbl)
{
_PanelOnB.Controls.Add(lbl);
}
//Примечание. Этот метод аналогичен прототипу делегата ShowMessageMethod.
private void ShowMessage(string msg)
{
this.listBox1.Items.Add(msg);
}
private void CreatePanelOnThreadB()
{
_PanelOnB = new Panel();
_PanelOnB.BackColor = System.Drawing.Color.Silver;
_PanelOnB.Location = new System.Drawing.Point(264, 12);
_PanelOnB.Name = "панель2";
_PanelOnB.Size = new System.Drawing.Size(244, 355);
_PanelOnB.TabIndex = 1;
this.Controls.Add(_PanelOnB);//Создаем контейнер Panel
Label _lblB = new Label();
_lblB.AutoSize = true;
_lblB.Font = new System.Drawing.Font("Arial", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
_lblB.Location = new System.Drawing.Point(3, 7);
_lblB.Name = "метка1";
_lblB.Size = new System.Drawing.Size(84, 12);
_lblB.TabIndex = 0;
_lblB.Text = "Тема B, созданная пользователем";
_PanelOnB.Controls.Add(_lblB);//Добавить метку в контейнер Panel
}
private void CreateButtonOnThreadB()
{
_btnOnB = new Button();
_btnOnB.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
_btnOnB.Location = new System.Drawing.Point(5, 63);
_btnOnB.Name = "btnSendToA";
_btnOnB.Size = new System.Drawing.Size(167, 23);
_btnOnB.TabIndex = 4;
_btnOnB.Text = "Отправить сообщение элементу управления, созданному потоком A";
_btnOnB.UseVisualStyleBackColor = true;
_btnOnB.Click += new System.EventHandler(this.btnSendToA_Click);
//panel2 — это элемент управления, созданный в основном потоке.
_PanelOnB.Controls.Add(_btnOnB);
}
private void CreateListBoxOnThreadB()
{
_listBoxOnB = new ListBox();
_listBoxOnB.FormattingEnabled = true;
_listBoxOnB.ItemHeight = 12;
_listBoxOnB.Location = new System.Drawing.Point(5, 240);
_listBoxOnB.Name = "listBox2";
_listBoxOnB.Size = new System.Drawing.Size(236, 112);
_listBoxOnB.TabIndex = 2;
//panel2 — это элемент управления, созданный в основном потоке.
_PanelOnB.Controls.Add(_listBoxOnB);
}
private void btnSendToA_Click(object sender, EventArgs e)
{
listBox1.Items.Add("Поток B отправляет сообщение A");
}
private void btnSendToB_Click(object sender, EventArgs e)
{
if (_listBoxOnB != null)
_listBoxOnB.Items.Add("Поток A отправляет сообщение B");
else
MessageBox.Show("Поток B еще не создан!");
}
}
}