出现原因
在WinForm中,如果你尝试在一个线程上操作另一个线程创建的控件,就会出现“线程无法访问非本线程创建的资源”的错误。这是因为Windows窗体的设计原则是单线程模型,即只有创建该控件的线程才能对其进行操作。
解决方法
1.使用
Control.Invoke
或Control.BeginInvoke
方法这是处理跨线程操作的最常见和推荐的方法。当你在一个线程中创建了一个控件,然后另一个线程想要访问这个控件时,你可以使用
Control.Invoke
或Control.BeginInvoke
方法。
Control.Invoke
方法会阻塞当前线程,直到操作完成。而Control.BeginInvoke
方法则是异步的,它会立即返回,并在后台执行操作。这两个方法都需要一个委托作为参数,这个委托封装了你想要执行的操作。这些操作会在创建控件的线程上执行,因此是线程安全的。
public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void Form1_Load(object sender, EventArgs e) { Thread thread1 = new Thread(() => { for (int i = 1; i <= 100; i += 2) { UpdateTextBox(textBox1, i.ToString()); Thread.Sleep(100); } }); Thread thread2 = new Thread(() => { for (int i = 2; i <= 100; i += 2) { UpdateTextBox(textBox2, i.ToString()); Thread.Sleep(100); } }); thread1.Start(); thread2.Start(); } private void UpdateTextBox(TextBox textBox, string text) { if (textBox.InvokeRequired) { textBox.Invoke((MethodInvoker)delegate { UpdateTextBox(textBox, text); }); } else { textBox.Text = text; } } }
Control.InvokeRequired
属性用于检查当前线程是否是创建控件的线程。如果当前线程是创建控件的线程,InvokeRequired
返回false
;如果当前线程不是创建控件的线程,InvokeRequired
返回true
。因此,我们在访问控件之前先检查
InvokeRequired
属性,如果它返回true
,那么我们就使用Control.Invoke
方法来在创建控件的线程上执行操作。Control.Invoke
方法接收一个委托,并在创建控件的线程上同步执行该委托。这样做的目的是确保我们总是在正确的线程上访问控件,避免线程冲突,保证应用程序的稳定性和正确性。
在这个方法中,如果
InvokeRequired
返回true
,我们就使用Control.Invoke
方法来重新调用UpdateTextBox
方法。这次调用将在创建textBox的线程上执行,所以InvokeRequired
将返回false
,然后我们就可以安全地更新textBox的Text属性。2.使用
BackgroundWorker
组件
BackgroundWorker
组件提供了对后台操作的支持。你可以在DoWork
事件处理程序中执行后台操作,然后在ProgressChanged
和RunWorkerCompleted
事件处理程序中更新 UI 控件。因为这些事件处理程序是在创建
BackgroundWorker
的线程上引发的,所以你可以在这些事件处理程序中安全地访问 UI 控件。
BackgroundWorker
组件还支持报告进度和取消操作,这使得它非常适合用于长时间运行的操作。using System; using System.ComponentModel; using System.Threading; using System.Windows.Forms; namespace MultiThreadedForm { public partial class Form1 : Form { private BackgroundWorker worker1; private BackgroundWorker worker2; public Form1() { InitializeComponent(); InitializeBackgroundWorkers(); } private void InitializeBackgroundWorkers() { // 初始化worker1 worker1 = new BackgroundWorker(); worker1.WorkerReportsProgress = true; worker1.DoWork += new DoWorkEventHandler(Worker1_DoWork); worker1.ProgressChanged += new ProgressChangedEventHandler(Worker1_ProgressChanged); worker1.RunWorkerCompleted += new RunWorkerCompletedEventHandler(Worker1_RunWorkerCompleted); // 初始化worker2 worker2 = new BackgroundWorker(); worker2.WorkerReportsProgress = true; worker2.DoWork += new DoWorkEventHandler(Worker2_DoWork); worker2.ProgressChanged += new ProgressChangedEventHandler(Worker2_ProgressChanged); worker2.RunWorkerCompleted += new RunWorkerCompletedEventHandler(Worker2_RunWorkerCompleted); } // 线程1的后台操作 private void Worker1_DoWork(object sender, DoWorkEventArgs e) { for (int i = 1; i <= 100; i += 2) { worker1.ReportProgress(i); Thread.Sleep(100); // 模拟耗时操作 } } // 更新textbox1的进度 private void Worker1_ProgressChanged(object sender, ProgressChangedEventArgs e) { textBox1.Text = e.ProgressPercentage.ToString(); } // 线程1完成后的操作 private void Worker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { textBox1.Text = "Thread 1 completed!"; } // 线程2的后台操作 private void Worker2_DoWork(object sender, DoWorkEventArgs e) { for (int i = 2; i <= 100; i += 2) { worker2.ReportProgress(i); Thread.Sleep(100); // 模拟耗时操作 } } // 更新textbox2的进度 private void Worker2_ProgressChanged(object sender, ProgressChangedEventArgs e) { textBox2.Text = e.ProgressPercentage.ToString(); } // 线程2完成后的操作 private void Worker2_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { textBox2.Text = "Thread 2 completed!"; } // 开始线程1 private void StartThread1Button_Click(object sender, EventArgs e) { if (!worker1.IsBusy) { worker1.RunWorkerAsync(); } } // 开始线程2 private void StartThread2Button_Click(object sender, EventArgs e) { if (!worker2.IsBusy) { worker2.RunWorkerAsync(); } } } }
我们创建了两个
BackgroundWorker
实例worker1
和worker2
,分别用于控制线程1和线程2的操作。当点击“开始线程1”按钮时,会启动线程1的后台操作,而点击“开始线程2”按钮时,则会启动线程2的后台操作。在
DoWork
事件处理程序中,分别使用ReportProgress
方法报告进度,并在ProgressChanged
事件处理程序中对相应的TextBox
控件进行更新。在RunWorkerCompleted
事件处理程序中,对TextBox
控件进行最终的更新。3.设置
Control.CheckForIllegalCrossThreadCalls
属性
Control.CheckForIllegalCrossThreadCalls
是一个全局设置,会影响到所有的控件。如果你将这个属性设置为false
,那么就可以在任何线程上操作控件,而不会抛出异常。然而,这种方法并不推荐使用。这是因为它违反了 Windows 窗体的设计原则,可能会导致未定义的行为或者难以调试的问题。除非你完全理解这种方法的风险,并且愿意承担这些风险,否则应该避免使用这种方法。