出现“线程无法访问非本线程创建的资源”的错误

出现原因

出现“线程无法访问非本线程创建的资源”的错误_第1张图片

在WinForm中,如果你尝试在一个线程上操作另一个线程创建的控件,就会出现“线程无法访问非本线程创建的资源”的错误。这是因为Windows窗体的设计原则是单线程模型,即只有创建该控件的线程才能对其进行操作。

解决方法 

1.使用 Control.InvokeControl.BeginInvoke 方法

这是处理跨线程操作的最常见和推荐的方法。当你在一个线程中创建了一个控件,然后另一个线程想要访问这个控件时,你可以使用 Control.InvokeControl.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 事件处理程序中执行后台操作,然后在 ProgressChangedRunWorkerCompleted 事件处理程序中更新 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实例worker1worker2,分别用于控制线程1和线程2的操作。当点击“开始线程1”按钮时,会启动线程1的后台操作,而点击“开始线程2”按钮时,则会启动线程2的后台操作。

DoWork事件处理程序中,分别使用ReportProgress方法报告进度,并在ProgressChanged事件处理程序中对相应的TextBox控件进行更新。在RunWorkerCompleted事件处理程序中,对TextBox控件进行最终的更新。

3.设置 Control.CheckForIllegalCrossThreadCalls 属性

Control.CheckForIllegalCrossThreadCalls 是一个全局设置,会影响到所有的控件。如果你将这个属性设置为 false,那么就可以在任何线程上操作控件,而不会抛出异常。

然而,这种方法并不推荐使用。这是因为它违反了 Windows 窗体的设计原则,可能会导致未定义的行为或者难以调试的问题。除非你完全理解这种方法的风险,并且愿意承担这些风险,否则应该避免使用这种方法。

出现“线程无法访问非本线程创建的资源”的错误_第2张图片

你可能感兴趣的:(c#)