【问题标题】:Running a UI thread into a background worker thread将 UI 线程运行到后台工作线程中
【发布时间】:2015-12-04 17:02:29
【问题描述】:

据我所知,由于 UI 和 Worker 线程之间的交互,无法将 winform 加载到 backgroundworker 的 DoWork 中, 但是我找到了一种方法来做到这一点,我定义了另一个显示表单的线程,然后将该线程启动到后台工作人员的 DoWork 中。 它起作用了,我现在可以控制那个后台工作人员的进程......但我不确定它是否是一种安全方法。我的简化计划是:

namespace WindowsFormsApplication1
{

    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        void UI1()
        {
            using (Form2 f = new Form2())
                f.ShowDialog();
        }
        Thread ui1;

        private void Form1_Load(object sender, EventArgs e)
        { 
         ui1 = new Thread(UI1);
         Control.CheckForIllegalCrossThreadCalls = false;
         temp.backgroundWorker1.DoWork+=new DoWorkEventHandler(backgroundWorker1_DoWork);  
        }
        private void button1_Click(object sender, EventArgs e)
        {
        temp.backgroundWorker1.RunWorkerAsync();        
        }
        private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
        {
            progressBar1.Minimum = 0;
            progressBar1.Maximum = 100000;
            progressBar1.Value = 0;
            for (int i = 0; i < 100000; i++)
            {
               progressBar1.Value++;
               if (i == 5000)
               {
                   ui1.Start();
                   temp.backgroundWorker1.CancelAsync();
                   temp.ew.Reset();
               }
               if (temp.backgroundWorker1.CancellationPending)
                    temp.ew.WaitOne();
            }     
       }
    }

在 Form2 中:

        private void button1_Click(object sender, EventArgs e)
        {
            temp.ew.Set();
            this.Close();
        }

【问题讨论】:

  • 这是灾难的完美秘诀。你为什么不直接使用 BackgroundWorker 来更新“正常”的 UI 线程(因为这已经为你完成了)?
  • 什么是“正常”的 UI 线程?我现在无法访问 Visual Studio,但整个计划是:后台工作人员的进程停止,直到另一个表单打开并且用户插入信息并按下按钮,新表单关闭并且后台工作人员继续工作......所以我需要另一个在进程停止时加载的表单
  • 你的计划倒退了。如果在后台处理过程中需要用户输入,则需要将进程拆分为可以相互独立执行的块。

标签: c# multithreading winforms thread-safety backgroundworker


【解决方案1】:

在您的代码示例之后更新了答案

首先,拥有多个 UI 线程或从不同线程访问您的 UI 是一个非常糟糕的主意。这是许多难以重现和调试的错误的根源。

因此设置Control.CheckForIllegalCrossThreadCalls = false 永远不应该在生产代码中使用。

我将尝试给你一个代码示例,它与你的(几乎)做同样的事情,但没有任何第二个 UI 线程。

Form2 和与 Form1 的通信

public partial class Form2 : Form
{
  public Form2()
  {
    InitializeComponent();
  }

  public event EventHandler ButtonClicked;
  protected virtual void OnButtonClicked()
  {
    EventHandler handler = ButtonClicked;
    if (handler != null) handler(this, EventArgs.Empty);
  }

  private void button1_Click(object sender, EventArgs e)
  {
    OnButtonClicked();
    Close();
  }
}

这是我的Form2 代码。它使用Form1 可以订阅的EventHandler(如下所示)在Form2.button1 被点击时得到通知。

Form1 使用 `BackgroundWorker` 及其事件

public partial class Form1 : Form
{
  private readonly BackgroundWorker backgroundWorker1 = new BackgroundWorker();
  private readonly ManualResetEvent ew = new ManualResetEvent(true);

  public Form1()
  {
    InitializeComponent();

    // this can all be done in designer too
    backgroundWorker1.WorkerReportsProgress = true;
    backgroundWorker1.DoWork += backgroundWorker1_DoWork;
    backgroundWorker1.ProgressChanged += backgroundWorker1_ProgressChanged;
    backgroundWorker1.RunWorkerCompleted += backgroundWorker1_RunWorkerCompleted;

    progressBar1.Minimum = 0;
    progressBar1.Maximum = 10000;
    progressBar1.Value = 0;
  }

  private void button1_Click(object sender, EventArgs e)
  {
    button1.Enabled = false;
    ew.Set();
    backgroundWorker1.RunWorkerAsync();
  }

  private void form2_ButtonClicked(object sender, EventArgs e)
  {
    ew.Set();
  }

  private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
  {
    for (int i = 0; i < 10000; i++)
    {
      if (i == 5000) ew.Reset();
      backgroundWorker1.ReportProgress(i);
      ew.WaitOne();
    }
  }
  private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
  {
    progressBar1.Value = e.ProgressPercentage;
    if (e.ProgressPercentage != 5000) return;
    Form2 form2 = new Form2();
    form2.ButtonClicked += form2_ButtonClicked;
    form2.Show();
  }
  private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
  {
    button1.Enabled = true;
  }
}

所以,我用backgroundWorker1.ReportsProgress = true 表示该工作人员将引发ProgressChanged 事件。 ProgressChanged 事件的好处是,它在你的 UI 线程中执行。

当报告进度为 5000 时,我创建Form2 的实例并将form2_ButtonClicked 方法注册为该实例的ButtonClicked 事件的处理程序。

然后我使用Form.Show而不是Form.ShowDialog打开新的form2,所以它不是模态的。

工作线程本身已重置ew,因此ew.WaitOne 现在将阻塞,直到再次设置ew。这就是在提到的事件处理程序form2_ButtonClicked 中发生的事情。

作为奖励,我在单击第一个表单时禁用了button1,并在后台工作程序完成时重新启用它,这样用户就无法在第一个线程运行时启动另一个线程。如果您希望多个线程并行运行,则代码会稍微复杂一些。但是由于只有一个进度条,我假设一次应该只有一个线程。

我希望这个解释对你有所帮助。我重复我的广告以阅读official documentation at MSDN


历史完整性的原始帖子

backgroundWorker1_DoWork 方法实际上已经在与启动该后台工作程序的 UI 线程不同的线程中运行。

您正在启动另一个显示您的对话框的线程。最后,您可以直接在该后台工作人员中执行此操作:

private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
  using (Form2 f = new Form2())
    f.ShowDialog();
}

两种方式都很糟糕,回答您的问题:您的解决方案与直接从后台工作人员打开表单一样不安全!问题是,为什么你想这样做?为什么你的新Form2 应该在不同的线程中运行?如果您想要一个非模式窗口,请考虑使用Form.Show() 而不是ShowDialog(这样您的用户可以切换到父窗口而无需关闭您的Form2)。

编辑:只是为了确定:在主 UI 线程中使用 Form.Show(),而不是在后台工作线程中,而不是在其他线程中。 Form.Show() 立即返回,因此如果您在其中一个线程中调用它,该线程随后将终止并且可能也会关闭窗口。

【讨论】:

  • 整个计划是:后台工作人员的进程停止,直到另一个表单打开并且用户插入信息并按下按钮,新表单关闭并且后台工作人员继续工作......所以我需要另一种在进程停止时加载的表单,我还没有找到安全的方法
  • 抱歉,但从那条评论中,我只能大致了解您想要实现的目标,但听起来已经存在很大的误解和设计缺陷。如果您可以更详细地解释您正在尝试做的事情(并显示代码),我们可以为您提供更好的帮助。从您到目前为止所写的内容来看,我仍然相信没有必要在另一个线程上创建表单。
  • 我将在第二天再次回到办公桌时编辑我的答案(现在在我的手机上书写)。我强烈建议阅读 msdn 中后台工作人员的官方文档!尤其是ProgressChanged event、ReportProgressChangedCancelAsync 等关键字...我仍然认为不需要在另一个线程中启动第二个表单。
  • @Hnz 完成。更新了我的答案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-02-02
  • 1970-01-01
  • 2018-07-16
  • 1970-01-01
  • 2016-01-16
  • 2013-06-05
  • 1970-01-01
相关资源
最近更新 更多