如何在Form Closing事件上停止BackgroundWorker?

时间:2009-11-13 19:32:31

标签: c# winforms multithreading backgroundworker

我有一个生成BackgroundWorker的表单,它应该更新表单自己的文本框(在主线程上),因此Invoke((Action) (...));调用。
如果在HandleClosingEvent我只是bgWorker.CancelAsync(),那么我会在ObjectDisposedException电话上获得Invoke(...),这是可以理解的。但是,如果我坐在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
    }

12 个答案:

答案 0 :(得分:94)

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

答案 1 :(得分:3)

我找到了另一种方式。如果你有更多的背景工作者,你可以做:

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

并且在每个backgroundWorker的DoWork方法中:

bgWorkesThreads.Add(Thread.CurrentThread);

你可以使用的艺术:

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

我在Control中的Word加载项中使用了它,我在CustomTaskPane中使用了它。如果某人早先关闭了文档或应用程序,那么我的所有后台工作都会完成他们的工作,它会引发一些COM Exception(我不记得那些不可思议)。CancelAsync()不起作用。

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

答案 2 :(得分:2)

这是我的解决方案(抱歉,它在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

答案 3 :(得分: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();
}

答案 4 :(得分:1)

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

  

无法调用Invoke或BeginInvoke   在控件上直到窗口处理   已创建。

我想这可以通过在'Loaded'回调而不是构造函数上启动worker来修改,但是如果使用BackgroundWorker的Progress报告机制,则可以完全避免这种整个考验。以下效果很好:

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

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

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

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

答案 5 :(得分:0)

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

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

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

答案 6 :(得分:0)

这对每个人都不起作用,但是如果你在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);
            }
    }

答案 7 :(得分:0)

如果您使用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);

}

答案 8 :(得分:-1)

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

答案 9 :(得分:-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

答案 10 :(得分:-2)

一种有效的解决方案,但过于复杂。我们的想法是生成定时器,该定时器将继续尝试关闭表单,并且表单将拒绝关闭,直到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();
}

答案 11 :(得分:-2)

另一种方式:

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