【问题标题】:How to stop BackgroundWorker on Form's Closing event?如何在窗体的关闭事件中停止 BackgroundWorker?
【发布时间】:2010-12-16 10:14:26
【问题描述】:

我有一个生成 BackgroundWorker 的表单,它应该更新表单自己的文本框(在主线程上),因此 Invoke((Action) (...)); 调用。
如果在HandleClosingEvent 我只是做bgWorker.CancelAsync() 然后我在Invoke(...) 电话上得到ObjectDisposedException,这是可以理解的。但是,如果我坐在HandleClosingEvent 中等待 bgWorker 完成,那么 .Invoke(...) 永远不会返回,这也是可以理解的。

任何想法如何关闭此应用程序而不会出现异常或死锁?

以下是简单 Form1 类的 3 个相关方法:

    public Form1() {
        InitializeComponent();
        Closing += HandleClosingEvent;
        this.bgWorker.RunWorkerAsync();
    }

    private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) {
        while (!this.bgWorker.CancellationPending) {
            Invoke((Action) (() => { this.textBox1.Text = Environment.TickCount.ToString(); }));
        }
    }

    private void HandleClosingEvent(object sender, CancelEventArgs e) {
        this.bgWorker.CancelAsync();
        /////// while (this.bgWorker.CancellationPending) {} // deadlock
    }

【问题讨论】:

  • 您是否尝试过使用 BegingInvoke 而不是 Invoke,这样您就不必等到调用消息返回?
  • 是的。没有死锁,但我不知道 BeginInvoke 何时被处理(在主线程上),所以我回到 ObjectDisposed 异常。

标签: c# winforms multithreading backgroundworker


【解决方案1】:

我知道的唯一死锁安全和异常安全的方法是实际取消 FormClosing 事件。如果 BGW 仍在运行,则设置 e.Cancel = true 并设置一个标志以指示用户请求关闭。然后在 BGW 的 RunWorkerCompleted 事件处理程序中检查该标志,如果已设置,则调用 Close()。

private bool closePending;

protected override void OnFormClosing(FormClosingEventArgs e) {
    if (backgroundWorker1.IsBusy) {
        closePending = true;
        backgroundWorker1.CancelAsync();
        e.Cancel = true;
        this.Enabled = false;   // or this.Hide()
        return;
    }
    base.OnFormClosing(e);
}

void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) {
    if (closePending) this.Close();
    closePending = false;
    // etc...
}

【讨论】:

  • 这行得通。我使用了 workersThread.CancellationPending + workersThread.IsBusy 标志,而不是 mCompleted。
  • 这有点危险,IsBusy 是异步线程的属性。它可以比赛。实际上没有,但那是运气。此外,CancellationPending 在 RunWorkerCompleted 触发之前被重置。
  • 小信息:你需要告诉你的 BackGroundWorker 实例它可以被取消。
  • 说到比赛......如果工人恰好在if (!mCompleted)之后正常完成,这不会失败吗?
  • @lain :不,OnFormClosing 和 backgroundWorker1_RunWorkerCompleted 都在 UI 线程上运行。一个不能被另一个打断。
【解决方案2】:

我找到了另一种方法。如果你有更多的 backgroundWorkers 你可以做:

List<Thread> bgWorkersThreads  = new List<Thread>();

并且在每个后台Worker的DoWork方法中make:

bgWorkesThreads.Add(Thread.CurrentThread);

您可以使用的Arter:

foreach (Thread thread in this.bgWorkersThreads) 
{
     thread.Abort();    
}

我在 Control 中的 Word 插件中使用了它,我在 CustomTaskPane 中使用了它。如果有人更早关闭文档或应用程序,那么我所有的 backgroundWorkes 都完成了他们的工作,它会引发一些 COM Exception(我不记得具体是哪个)。CancelAsync() 不起作用。

但是有了这个,我可以在DocumentBeforeClose事件中立即关闭backgroundworkers使用的所有线程,我的问题就解决了。

【讨论】:

    【解决方案3】:

    这是我的解决方案(对不起,它在 VB.Net 中)。

    当我运行 FormClosing 事件时,我运行 BackgroundWorker1.CancelAsync() 以将 CancellationPending 值设置为 True。不幸的是,程序永远没有机会检查 CancellationPending 值以将 e.Cancel 设置为 true(据我所知,这只能在 BackgroundWorker1_DoWork 中完成)。 我没有删除那条线,尽管它似乎并没有什么不同。

    我添加了一行将我的全局变量 bClos​​ingForm 设置为 True。然后,在执行任何结束步骤之前,我在 BackgroundWorker_WorkCompleted 中添加了一行代码来检查 e.Cancelled 和全局变量 bClos​​ingForm。

    使用此模板,您应该可以随时关闭您的表单,即使后台工作人员正在处理某事(这可能不好,但它一定会发生,所以不妨处理一下) .我不确定是否有必要,但您可以在这一切发生后完全在 Form_Closed 事件中处理后台工作人员。

    Private bClosingForm As Boolean = False
    
    Private Sub SomeFormName_FormClosing(ByVal sender As Object, ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing
        bClosingForm = True
        BackgroundWorker1.CancelAsync() 
    End Sub
    
    Private Sub backgroundWorker1_DoWork(ByVal sender As Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
        'Run background tasks:
        If BackgroundWorker1.CancellationPending Then
            e.Cancel = True
        Else
            'Background work here
        End If
    End Sub
    
    Private Sub BackgroundWorker1_RunWorkerCompleted(ByVal sender As System.Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles BackgroundWorker1.RunWorkerCompleted
        If Not bClosingForm Then
            If Not e.Cancelled Then
                'Completion Work here
            End If
        End If
    End Sub
    

    【讨论】:

    • +1 在 BackgroundWorker 的 RunWorkerCompleted EventHandler 处处理此问题。我就是这样做的
    【解决方案4】:

    你能不等待表单析构函数中的信号吗?

    AutoResetEvent workerDone = new AutoResetEvent();
    
    private void HandleClosingEvent(object sender, CancelEventArgs e)
    {
        this.bgWorker.CancelAsync();
    }
    
    private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
    {
        while (!this.bgWorker.CancellationPending) {
            Invoke((Action) (() => { this.textBox1.Text =   
                                     Environment.TickCount.ToString(); }));
        }
    }
    
    
    private ~Form1()
    {
        workerDone.WaitOne();
    }
    
    
    void backgroundWorker1_RunWorkerCompleted( Object sender, RunWorkerCompletedEventArgs e )
    {
        workerDone.Set();
    }
    

    【讨论】:

      【解决方案5】:

      首先,ObjectDisposedException 只是一个可能的陷阱。运行 OP 的代码在很多情况下都会产生以下 InvalidOperationException:

      无法调用 Invoke 或 BeginInvoke 在一个控件上直到窗口句柄 已创建。

      我想这可以通过在“加载”回调而不是构造函数上启动工作程序来修改,但是如果使用 BackgroundWorker 的进度报告机制,则可以完全避免整个考验。以下效果很好:

      private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
      {
          while (!this.bgWorker.CancellationPending)
          {
              this.bgWorker.ReportProgress(Environment.TickCount);
              Thread.Sleep(1);
          }
      }
      
      private void bgWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
      {
          this.textBox1.Text = e.ProgressPercentage.ToString();
      }
      

      我有点劫持了百分比参数,但可以使用另一个重载来传递任何参数。

      有趣的是,删除上述 sleep 调用会阻塞 UI,消耗大量 CPU 并不断增加内存使用。我想这与 GUI 的消息队列过载有关。但是,在 sleep 调用完好无损的情况下,CPU 使用率几乎为 0,内存使用率似乎也很好。谨慎起见,也许应该使用高于 1 ms 的值?专家意见将不胜感激... 更新:看来只要更新不是太频繁,应该没问题:Link

      无论如何,我无法预见 GUI 的更新间隔必须短于几毫秒的情况(至少,在人类正在观看 GUI 的情况下),所以我认为大多数情况下的时间进度报告将是正确的选择

      【讨论】:

        【解决方案6】:

        如果您使用 this.enabled = false,我真的不明白为什么在这种情况下 DoEvents 被视为如此糟糕的选择。我觉得这样会很整洁。

        protected override void OnFormClosing(FormClosingEventArgs e) {
        
            this.Enabled = false;   // or this.Hide()
            e.Cancel = true;
            backgroundWorker1.CancelAsync();  
        
            while (backgroundWorker1.IsBusy) {
        
                Application.DoEvents();
        
            }
        
            e.cancel = false;
            base.OnFormClosing(e);
        
        }
        

        【讨论】:

        • 在我的 Do...While(IsBusy()) 检查循环中添加 DoEvents() 效果很好。我的后台工作人员内部的运行循环(包括对 CancellationPending 的检查非常快(.0004 mSec))。不确定这是否是使它在这里可靠的原因。 DoEvents() 被普遍诟病和厌恶良好的编码,我完全忘记了它的存在!非常感谢您的建议!
        【解决方案7】:

        您的后台工作人员不应使用 Invoke 来更新文本框。它应该很好地要求 UI 线程使用事件 ProgressChanged 更新文本框,并将值放入附加的文本框中。

        在事件关闭(或者可能是事件关闭)期间,UI 线程会在取消后台工作程序之前记住表单已关闭。

        在收到 progressChanged 后,UI 线程会检查表单是否已关闭,只有在未关闭时才会更新文本框。

        【讨论】:

          【解决方案8】:

          这不适用于所有人,但是如果您定期在 BackgroundWorker 中执行某些操作,例如每秒或每 10 秒(可能轮询服务器),这似乎可以很好地以有序的方式停止进程并且没有错误消息(至少到目前为止)并且易于理解;

           public void StopPoll()
                  {
                      MyBackgroundWorker.CancelAsync(); //Cancel background worker
                      AutoResetEvent1.Set(); //Release delay so cancellation occurs soon
                  }
          
           private void bw_DoWork(object sender, DoWorkEventArgs e)
                  {
                      while (!MyBackgroundWorker.CancellationPending)
                      {
                      //Do some background stuff
                      MyBackgroundWorker.ReportProgress(0, (object)SomeData);
                      AutoResetEvent1.WaitOne(10000);
                      }
              }
          

          【讨论】:

            【解决方案9】:

            我会将与文本框关联的 SynchronizationContext 传递给 BackgroundWorker,并使用它在 UI 线程上执行更新。使用 SynchronizationContext.Post,您可以检查控件是否被释放或释放。

            【讨论】:

            • WindowsFormsSynchronizationContext.Post(...) 只是调用BeginInvoke(...),所以它与我已经在做的 Invoke() 没有太大区别。除非我遗漏了什么,请您详细说明一下吗?
            【解决方案10】:

            Me.IsHandleCreated 呢?

                Private Sub BwDownload_RunWorkerCompleted(sender As Object, e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles BwDownload.RunWorkerCompleted
                If Me.IsHandleCreated Then
                    'Form is still open, so proceed
                End If
            End Sub
            

            【讨论】:

              【解决方案11】:

              另一种方式:

              if (backgroundWorker.IsBusy)
              {
                  backgroundWorker.CancelAsync();
                  while (backgroundWorker.IsBusy)
                  {
                      Application.DoEvents();
                  }
              }
              

              【讨论】:

                【解决方案12】:

                一种可行的解决方案,但过于复杂。这个想法是产生一个计时器,它将继续尝试关闭表单,并且表单将拒绝关闭,直到所说的 bgWorker 死了。

                private void HandleClosingEvent(object sender, CancelEventArgs e) {
                    if (!this.bgWorker.IsBusy) {
                        // bgWorker is dead, let Closing event proceed.
                        e.Cancel = false;
                        return;
                    }
                    if (!this.bgWorker.CancellationPending) {
                        // it is first call to Closing, cancel the bgWorker.
                        this.bgWorker.CancelAsync();
                        this.timer1.Enabled = true;
                    }
                    // either this is first attempt to close the form, or bgWorker isn't dead.
                    e.Cancel = true;
                }
                
                private void timer1_Tick(object sender, EventArgs e) {
                    Trace.WriteLine("Trying to close...");
                    Close();
                }
                

                【讨论】:

                • 这对我来说听起来很 hacky。不会这样做。
                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多