如何在Form的Closing事件中停止BackgroundWorker?


72

我有一个生成BackgroundWorker的表单,该表单应该更新表单自己的文本框(在主线程上),因此进行Invoke((Action) (...));调用。
如果HandleClosingEvent我真bgWorker.CancelAsync()那么我得到ObjectDisposedExceptionInvoke(...)电话,可以理解。但是,如果我坐在那里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,这样就不必等到invokemessage返回?
Mez

是。没有死锁,但是我不知道何时在主线程上处理BeginInvoke,因此我回到了ObjectDisposed异常。
THX-1138,2009年

Answers:


102

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

这可行。我使用workerThread.CancellationPending + workerThread.IsBusy标志,而不是mCompleted。
THX-1138,2009年

8
IsBusy是异步线程的属性,这有点危险。它可以比赛。它实际上没有,但这很幸运。另外,在启动RunWorkerCompleted之前,将取消CancellationPending。
汉斯·帕桑

3
次要信息:您需要告诉BackGroundWorker实例可以取消它。
卡洛斯,

2
说起种族...如果工人恰好在此之后正常完成工作,这是否会失败if (!mCompleted)
伊恩2012年

7
@lain:否,OnFormClosing和backgroundWorker1_RunWorkerCompleted都在UI线程上运行。一个不能被另一个打断。
Sacha K,

3

我找到了另一种方式。如果您有更多的backgroundWorkers,则可以进行以下操作:

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

并在每个backgroundWorker的DoWork方法中进行以下操作:

bgWorkesThreads.Add(Thread.CurrentThread);

您可以使用的Arter:

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

我在Control中的Word加载项中使用了该功能,在中使用了它CustomTaskPane。如果有人更早地关闭了文档或应用程序,那么我所有的backgroundWorkes都会完成他们的工作,这会引发一些问题COM Exception(我不记得是哪个)。CancelAsync()不起作用。

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


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事件中完全处置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

+1在BackgroundWorker的RunWorkerCompleted EventHandler中处理。那就是我所做的
jp2code

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

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

有趣的是,删除上面的sleep调用会阻塞UI,消耗大量CPU,并不断增加内存使用量。我想这与GUI的消息队列超载有关。但是,在完整的睡眠调用后,CPU使用率实际上为0,内存使用率似乎也不错。为谨慎起见,也许应该使用大于1 ms的值?专家的意见将不胜感激...更新:看来,只要更新不是太频繁,就应该可以:链接

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


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

0

您的背景工作人员不应使用“调用”来更新文本框。它应该很好地要求UI线程使用事件ProgressChanged以及附加在文本框中的值来更新文本框。

在事件“关闭”(或事件“关闭”)期间,UI线程在取消后台工作之前记住该窗体已关闭。

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


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

-1

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


WindowsFormsSynchronizationContext.Post(...)只是调用BeginInvoke(...),所以和我已经在做的Invoke()并没有太大区别。除非我错过了什么,否则请您详细说明一下?
THX-1138,2009年

-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

-2

另一种方式:

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

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

对我来说这听起来很客气。不会这样做。
2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.