如何在窗体关闭事件中停止BackgroundWorker?

75

我有一个表单,会启动一个BackgroundWorker,该worker应该更新表单自己的文本框(在主线程上),因此需要调用Invoke((Action) (...));
如果我在HandleClosingEvent中只是执行bgWorker.CancelAsync(),那么当我调用Invoke(...)时会出现ObjectDisposedException异常,这很容易理解。但如果我等待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
    }

你尝试过使用BeginInvoke而不是Invoke吗?这样你就不必等待invokemessage返回了。 - Mez
是的。没有死锁,但我不知道BeginInvoke何时被处理(在主线程上),所以我又回到了ObjectDisposed异常。 - THX-1138
12个回答

103

我所知道的唯一避免死锁和异常安全的方法是实际上取消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...
}

8
有点危险,IsBusy是异步线程的一个属性。它可能会竞争。实际上它没有,但那只是侥幸。此外,在RunWorkerCompleted触发之前,CancellationPending会被重置。 - Hans Passant
3
小提示:您需要告诉您的BackGroundWorker实例它可以被取消。 - Carlos
2
说到竞态条件...如果工作线程在if (!mCompleted)之后正常完成,那么这个代码块不会被关闭,对吧? - Iain
7
@lain:不,OnFormClosing和backgroundWorker1_RunWorkerCompleted都在UI线程上运行。它们互相之间是无法中断的。 - Sacha K
为什么要加上 "base.OnFormClosing(e);" 这句代码? - Shane Di Dona
显示剩余6条评论

2
我找到了另一种方法。如果你有更多的 backgroundWorkers,你可以这样做:
List<Thread> bgWorkersThreads  = new List<Thread>();

在每个backgroundWorker的DoWork方法中执行以下操作:
bgWorkesThreads.Add(Thread.CurrentThread);

之后您可以使用以下内容:

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

我在Word插件中使用了这个控件,在CustomTaskPane中使用。如果有人关闭文档或应用程序,那么所有我的后台工作完成后就会引发一些COM异常(我不记得确切是哪种)。CancelAsync()无法解决这个问题。
但是,通过使用这个方法,我可以立即关闭所有被backgroundworkers使用的线程,并在DocumentBeforeClose事件中解决我的问题。

2

这是我的解决方案(抱歉,它用的是VB.Net)。当我运行FormClosing事件时,我运行BackgroundWorker1.CancelAsync()将CancellationPending值设置为True。不幸的是,程序从来没有真正有机会检查CancellationPending值以将e.Cancel设置为true(据我所知,只能在BackgroundWorker1_DoWork中完成)。 我没有删除那一行,尽管这似乎并没有什么区别。

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

使用此模板,即使在backgroundworker正在进行某些操作时,您也应该能够随时关闭窗体(这可能不是很好,但肯定会发生,因此最好处理)。我不确定是否有必要在所有这些操作完成后在Form_Closed事件中完全处理Background worker。

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

在 BackgroundWorker 的 RunWorkerCompleted 事件处理程序中处理此事。这就是我所做的。 - user153923

1

如果您使用了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()是如此普遍地被诋毁,成为良好编码的大忌,以至于我完全忘记了它的存在!非常感谢您提出建议! - Michael Gorsich

1

你可以不在窗体的析构函数中等待信号吗?

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();
}

1

首先,ObjectDisposedException只是这里可能出现的一个问题。运行OP的代码时,在相当多的情况下会产生以下InvalidOperationException:

在窗口句柄创建之前,无法对控件调用Invoke或BeginInvoke。

我想这可以通过在“Loaded”回调上启动工作程序来进行修改,但如果使用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();
}

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

有趣的是,如果删除上述的睡眠调用,界面会变得拥挤,消耗高CPU,并不断增加内存使用。我猜这与GUI的消息队列过载有关。然而,保留睡眠调用后,CPU使用率几乎为0,内存使用似乎也很好。为了谨慎起见,可能应该使用比1毫秒更高的值?在这里,专家意见将不胜感激...更新:看起来只要更新不太频繁,就应该没问题:Link

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


0
你的BackgroundWorker不应该使用Invoke来更新文本框。它应该通过ProgressChanged事件向UI线程请求更新文本框,并将要放入文本框中的值附加在事件上。
在Closed事件(或者可能是Closing事件)期间,UI线程会在取消BackgroundWorker之前记住表单已关闭。
收到ProgressChanged事件后,UI线程会检查表单是否关闭,只有在未关闭时才会更新文本框。

0

这并不适用于所有人,但如果你正在使用 BackgroundWorker 定期地执行某些操作,比如每秒或每十秒执行一次(也许是轮询服务器),那么这种方式似乎可以很好地有序停止进程,并且没有错误消息(至少目前看来是这样),而且易于理解:

 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);
            }
    }

-1
我会将与文本框相关联的同步上下文传递给BackgroundWorker,并使用它来在UI线程上执行更新。使用SynchronizationContext.Post,您可以检查控件是否已处理或正在处理。

WindowsFormsSynchronizationContext.Post(...) 只是调用 BeginInvoke(...),所以它与我已经执行的 Invoke() 没有太大区别。除非我漏掉了什么,你能否详细说明一下? - THX-1138

-1

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

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