【问题标题】:C# Cross-thread operation not valid in BackgroundWorkerC# 跨线程操作在 BackgroundWorker 中无效
【发布时间】:2020-08-08 01:48:58
【问题描述】:

我在加载主表单时将项目添加到列表框:

private void MainForm_Load(object sender, EventArgs e)
{
    Dictionary<string, string> item = new Dictionary<string, string>();
    item.Add("Test 1", "test1");
    item.Add("Test 2", "test 2");

    cmbTest.DataSource = new BindingSource(item, null);
    cmbTest.DisplayMember = "Key";
    cmbTest.ValueMember = "Value";
}

然后我尝试在 BackgroundWorker 中获取所选项目的值,但失败了。

private void TestWorker_DoWork(object sender, DoWorkEventArgs e)
{
    string test = ((KeyValuePair<string, string>)cmbTest.SelectedItem).Value;
    MessageBox.Show(test);
}

【问题讨论】:

  • 不是一个好的测试。您通常避免使用 DoWork 方法中的 GUI 控件。在 DoWork 方法中执行长时间运行的操作。将结果传递给 RunWorkerCompleted 事件,您可以在其中再次访问 GUI 控件。
  • 为什么要使用 BackGroundWorker 来读取 SelectedItem 值?如果您决定在后台线程中加载数据,那是可以理解的,事实并非如此。你能解释一下你要解决什么(真正的)问题吗?

标签: c# multithreading winforms invoke


【解决方案1】:

后台工作人员不应尝试对 UI 进行任何操作。线程唯一能做的就是通知那些感兴趣的人后台工作人员计算了一些值得注意的东西。

此通知是使用事件 ProgressChanged 完成的。

  • 使用 Visual Studio Designer 创建 BackGroundWorker。
  • 让设计人员为 DoWork、ProgressChanged 和 RunWorkerCompleted(如果需要)添加事件处理程序
  • 如果 BackGroundWorker 想要通知表单应该显示某些内容,请使用 ProgressChanged

您可能简化了您的问题,但后台工作人员不应读取组合框的选定值。如果组合框发生变化,表单应该在传递所选组合框项的值时启动后台工作程序。

所以让我们让问题变得更有趣一点:如果用户在comboBox1 中选择了一个项目,后台工作人员会被命令使用所选的组合框值来计算一些东西。

在计算过程中,BackGroundWorker 会定期通知表单有关进度和中间计算值的信息。完成后,返回最终结果。

代码会是这样的:

private void InitializeComponent()
{
    this.backgroundWorker1 = new System.ComponentModel.BackgroundWorker();
    this.SuspendLayout();
    this.backgroundWorker1.DoWork += new DoWorkEventHandler(this.DoBackgroundWork);
    this.backgroundWorker1.ProgressChanged += new ProgressChangedEventHandler(this.NotifyProgress);
    this.backgroundWorker1.RunWorkerCompleted += new RunWorkerCompletedEventHandler(
                                                 this.OnBackgounrWorkCompleted);
    ...
}

当一个项目被选择 comboBox1 时,后台工作程序使用选定的值启动。在后台工作程序启动时,用户无法再次更改组合框 1,因为我们无法在同一后台工作程序仍处于忙碌状态时启动它。

因此组合框被禁用,并显示一个进度条。在计算过程中,进度条会更新,中间结果显示在 Label1 中。当 backgroundworker 完成后,进度条被移除,最终结果显示在 Lable1 中并再次启用组合框。

请注意,当后台工作人员正在计算时,表单的其余部分仍在工作。

private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
{
    ComboBox comboBox = (ComboBox)sender;

    // disable ComboBox, show ProgressBar:
    comboBox.Enabled = false;
    this.progressBar1.Minimum = 0;
    this.progressBar1.Maximum = 100;
    this.progressBar1.Value = 0;
    this.progressBar1.Visible = true;

    // start the backgroundworker using the selected value:
    this.backgroundWorker1.RunWorkerAsync(comboBox.SelectedValue);
}

后台工作:

private void DoBackgroundWork(object sender, DoWorkEventArgs e)
{
    // e.Argument contains the selected value of the combobox
    string test = ((KeyValuePair<string, string>)e.Argument;

    // let's do some lengthy processing:
    for (int i=0; i<10; ++i)
    {
        string intermediateText = Calculate(test, i);

        // notify about progress: use a percentage and intermediateText
        this.backgroundWorker1.ReportProgress(10*i, intermediateText);
    }

    string finalText = Calculate(test, 10);

    // the result of the background work is finalText
    e.Result = finalText;
}

您的表单会定期收到有关进度的通知:让它更新 ProgressBar 并在 Label1 中显示中间文本

private void NotifyProgress(object sender, ProgressChangedEventArgs e)
{
    this.progressBar1.Value = e.ProgressPercentage;
    this.label1.Text = e.UserState.ToString();
}

BackgroundWorker 完成后,最终文本显示在 label1 中,进度条消失,再次启用 Combobox:

private void OnBackgoundWorkCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    this.label1.Text = e.Result.ToString();
    this.progressBar1.Visible = false;
    this.comboBox1.Enabled = true;
}

【讨论】:

  • 这是使用BackgroundWorker的正确方法。使用它提供的用于安全地将数据传入和传出后台线程的特定机制(DoWorkEventArgsArgumentResult 属性)是正确的。这个例子还强调了为什么现代开发人员放弃了BackgroundWorker,转而使用 async-await 技术。正确使用BackgroundWorker 既费力又费力。与async-await 做同样的事情感觉就像在公园里散步。
  • 西奥多,你是对的。自从我学会了 async-await 之后,我就很少再使用 backgroundworkers 了。我打算建议转移到 async-await,但也许这一次信息太多了。所以我专注于最初的问题。所以,Nathan,只要你有空闲时间:熟悉一下 async-await。搜索 Stephen Cleary 异步等待。他有很好的、有用的、易于理解的文档。
【解决方案2】:

原因是后台工作者在不同的线程上运行。您必须使用 UI 线程从 UI 线程中读取值。在这种情况下,cmbTest 在 UI 上

 this.Invoke(new Action(() =>
 {
   string test = ((KeyValuePair<string, string>)cmbTest.SelectedItem).Value;
 }));

如果您需要值来执行异步进程

private void TestWorker_DoWork(object sender, DoWorkEventArgs e)
{
 string test;
 this.Invoke(new Action(() =>
 {
     //Any other things you need from UI thread
     test = ((KeyValuePair<string, string>)cmbTest.SelectedItem).Value;
 }));
 //Here you have access to UI thread values
}

【讨论】:

    【解决方案3】:

    UI 元素只能由 UI 线程访问。

    如果您正在使用 WinForms,那么您可以使用 Control.InvokeRequired 标志和 Control.Invoke 方法:

    private void TestWorker_DoWork(object sender, DoWorkEventArgs e)
    {
        if(!cmbTest.InvokeRequired)
        {
            string test = ((KeyValuePair<string, string>)cmbTest.SelectedItem).Value;
            MessageBox.Show(test);
        }
        else 
        {
            string test;
            Invoke(() => test = ((KeyValuePair<string, string>)cmbTest.SelectedItem).Value);
            Invoke(() => MessageBox.Show(test));
        }
    }
    

    Control.InvokeRequired 获取一个值,该值指示调用者在对控件进行方法调用时是否必须调用invoke 方法,因为调用者位于与创建控件的线程不同的线程上。

    Control.Invoke 在拥有控件底层窗口句柄的线程上执行指定的委托。

    如果您使用的是 WPF,那么也有相应的解决方案。 Dispatcher.Invoke 在 Dispatcher 关联的线程上同步执行指定的委托:

    private void TestWorker_DoWork(object sender, DoWorkEventArgs e)
    {
        this.Dispatcher.Invoke(() => {
            string test = ((KeyValuePair<string, string>)cmbTest.SelectedItem).Value;
            MessageBox.Show(test);
        });
    }
    

    您可以在 Microsoft 文档上阅读有关 thread-safe calls to windows forms controls 的更多信息,或者阅读有关 WPF Threading Model 的更多信息。

    【讨论】:

      【解决方案4】:

      其他解决方案。

      声明表单域:

      private string _value;
      

      在您运行BackgroundWorker的位置的UI控件中输入此字段中的值

      private void RunWorkButton_Click(object sender, EventArgs e)
      {
          _value = ((KeyValuePair<string, string>)cmbTest.SelectedItem).Value;
          testWorker.RunWorkerAsync();
      }
      

      接下来,在DoWork方法中使用这个字段:

      private void TestWorker_DoWork(object sender, DoWorkEventArgs e)
      {
          // use _value somehow
          string test = _value;
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2015-12-13
        • 1970-01-01
        • 2012-09-12
        • 1970-01-01
        • 1970-01-01
        • 2011-01-15
        • 1970-01-01
        相关资源
        最近更新 更多