.NET后台工作者 - InvalidOperationException: 跨线程操作无效。

5
我有一个用.NET Winforms编写的项目。 我需要实现一个数据挖掘操作,将文本打印到TextBox并更新进度。
我试图使用BackgroundWorker来完成,但它抛出了一个InvalidOperationException(Cross-thread operation not valid: Control ‘xxxxx’ accessed from a thread other than the thread it was created on
为了缩小问题可能的原因范围,我开始了一个新项目,并包括以下内容: Button - 启动BackgroundWorker Label - 打印文本。 和ProgressBar。
然而,结果是一样的。我在SOF上搜索,被告知要使用委托,但我不熟悉它。
这是引发错误的代码示例:
using System;
using System.Collections.Generic;
using System.ComponentModel;

namespace TestProject
{
    public partial class Form1 : Form
    {
        private readonly BackgroundWorker _bw = new BackgroundWorker();

        public Form1()
        {
            InitializeComponent();
            _bw.DoWork += RosterWork;
            _bw.ProgressChanged += BwProgressChanged;
            _bw.RunWorkerCompleted += BwRunWorkerCompleted;
            _bw.WorkerReportsProgress = true;
            _bw.WorkerSupportsCancellation = false;
        }

        private void RosterWork(object sender, DoWorkEventArgs doWorkEventArgs)
        {
            for (int i = 0; i < 1000; i++)
            {
                label1.Text = i.ToString();
                _bw.ReportProgress(Convert.ToInt32((i * (100 / 1000))));
            }
        }

        private void BwProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            progressBar1.Value = e.ProgressPercentage;
        }

        private void btnStart_Click(object sender, EventArgs e)
        {
            progressBar1.Show();
            _bw.RunWorkerAsync();
        }

        private void BwRunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            progressBar1.Hide();
        }

    }
}

更新: 我按照Jon Skeet的答案进行了尝试,它在我的测试项目中确实有效,但是回到我的真实项目时,

我的表单布局如下:

表单 - 选项卡控件 - 选项卡1 -选项卡1面板 -文本框1

当执行到这一行时:

TextBox txtbox1 = new TextBox();
Tab1Panel.Controls.Add(txtbox1);

当我程序化地将文本框添加到面板控件时,错误仍然会发生。
最终,我用以下方法进行替换:
 if (Tab1Panel.InvokeRequired)
     Tab1Panel.Invoke((MethodInvoker)delegate { Tab1Panel.Controls.Add(txtbox1); });
 else
     Tab1Panel.Controls.Add(txtbox1);

一切都是工作。如何确定控件是否需要调用InvokeRequired?它是否指定了控件?

4个回答

12
这是问题所在:
label1.Text = i.ToString();

您正在尝试更改BackgroundWorker中的标签文本,但该操作不在UI线程上运行。 BackgroundWorker的目的是在其中执行所有非UI工作,使用ReportProgress定期“返回”到UI线程,并使用正在进行的进度更新UI。
因此,您需要在BwProgressChanged中更改label1.Text,或者像从任何其他后台线程一样使用Control.Invoke/BeginInvoke。请注意保留HTML标记。
// Don't capture a loop variable in a lambda expression...
int copy = i;
Action updateLabel = () => label1.Text = copy.ToString();
label1.BeginInvoke(updateLabel);

关于复制部分的更多信息,请参阅Eric Lippert的博客文章,“循环变量闭合被认为是有害的”。在这种特定情况下,这只是一个问题,因为我正在使用BeginInvoke。这个可以改成:

Action updateLabel = () => label1.Text = i.ToString();
label1.Invoke(updateLabel);

...但是现在后台工作线程总是等待UI赶上才继续执行,而在实际生活中这通常不是你想要的。我一般更喜欢使用BeginInvoke而不是Invoke


谢谢Jon Skeet!我在Google上搜索了关于.NET线程的内容。我读了一些有关Thread / ThreadStart / ThreadPool / Delegate的文章。它们似乎都是线程编程中预期标准。那么使用BackgroundWorker是正确的方式吗? - Cheung
@Shiba:恐怕我真的不太明白你的评论——尤其是“所有这些更像是预期的标准”这一部分。 - Jon Skeet

5
使用这段代码,它将有效运行。
 BeginInvoke((MethodInvoker)delegate
               {
                   TextBox1.Text += "your text here";

               });

0
您正在从后台工作线程访问已在GUI线程上创建的标签。 不允许从控件创建的线程以外的线程访问Windows控件,因此会出现异常。 您不应直接访问标签,而应该从线程内引发事件,并确保它在正确的线程上调用,以向UI发出信号,以便更改标签的内容。

0

您必须从创建它们的线程执行控件的方法。要从不同的线程更新它们,请使用Invoke。可以在这里找到如何实现的示例。您还应该阅读MSDN上关于表单和控件的多线程文章Multithreading with Forms and Controls


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接