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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/1731384/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-06 20:21:01  来源:igfitidea点击:

How to stop BackgroundWorker on Form's Closing event?

c#winformsmultithreadingbackgroundworker

提问by THX-1138

I have a form that spawns a BackgroundWorker, that should update form's own textbox (on main thread), hence Invoke((Action) (...));call.
If in HandleClosingEventI just do bgWorker.CancelAsync()then I get ObjectDisposedExceptionon Invoke(...)call, understandably. But if I sit in HandleClosingEventand wait for bgWorker to be done, than .Invoke(...) never returns, also understandably.

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

Any ideas how do I close this app without getting the exception, or the deadlock?

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

Following are 3 relevant methods of the simple Form1 class:

以下是简单 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
    }

采纳答案by Hans Passant

The only deadlock-safe and exception-safe way to do this that I know is to actually cancel the FormClosing event. Set e.Cancel = true if the BGW is still running and set a flag to indicate that the user requested a close. Then check that flag in the BGW's RunWorkerCompleted event handler and call Close() if it is set.

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

回答by THX-1138

One solution that works, but too complicated. The idea is to spawn the timer that will keep trying to close the form, and form will refuse to close until said bgWorkeris dead.

一种有效的解决方案,但太复杂了。这个想法是产生将继续尝试关闭表单的计时器,并且表单将拒绝关闭,直到说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();
}

回答by Hasani Blackwell

I'd pass in the SynchronizationContext associated with the textbox to the BackgroundWorker and use that to perform Updates on the UI thread. Using SynchronizationContext.Post, you can check if the control is disposed or disposing.

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

回答by Cheeso

Can you not wait on the signal in the destructor of the form?

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

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

回答by Ohad Schneider

Firstly, the ObjectDisposedException is only one possible pitfall here. Running the OP's code has produced the following InvalidOperationException on a substantial number of occasions:

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

Invoke or BeginInvoke cannot be called on a control until the window handle has been created.

在创建窗口句柄之前,不能在控件上调用 Invoke 或 BeginInvoke。

I suppose this could be amended by starting the worker on the 'Loaded' callback rather than the constructor, but this entire ordeal can be avoided altogether if BackgroundWorker's Progress reporting mechanism is used. The following works well:

我想这可以通过在“已加载”回调而不是构造函数上启动工作程序来修改,但是如果使用 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();
}

I kind of hiHymaned the percentage parameter but one can use the other overload to pass any parameter.

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

It is interesting to note that removing the above sleep call clogs the UI, consumes high CPU and continually increases the memory use. I guess it has something to do with the message queue of the GUI being overloaded. However, with the sleep call intact, the CPU usage is virtually 0 and the memory usage seems fine, too. To be prudent, perhaps a higher value than 1 ms should be used? An expert opinion here would be appreciated... Update: It appears that as long as the update isn't too frequent, it should be OK: Link

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

In any case, I can't foresee a scenario where the updating of the GUI has to be in intervals shorter than a couple of milliseconds (at least, in scenarios where a human is watching the GUI), so I think most of the time progress reporting would be the right choice

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

回答by Tim Hagel

Here was my solution (Sorry it's in VB.Net).

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

When I run the FormClosing event I run BackgroundWorker1.CancelAsync() to set the CancellationPending value to True. Unfortunately, the program never really gets a chance to check the value CancellationPending value to set e.Cancel to true (which as far as I can tell, can only be done in BackgroundWorker1_DoWork). I didn't remove that line, although it doesn't really seem to make a difference.

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

I added a line that would set my global variable, bClosingForm, to True. Then I added a line of code in my BackgroundWorker_WorkCompleted to check both e.Cancelled as well as the global variable, bClosingForm, before performing any ending steps.

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

Using this template, you should be able to close your form out at any time even if the backgroundworker is in the middle of something (which might not be good, but it's bound to happen so it might as well be dealt with). I'm not sure if it's necessary, but you could dispose the Background worker entirely in the Form_Closed event after this all takes place.

使用此模板,您应该能够随时关闭您的表单,即使后台工作人员正在处理某事(这可能不好,但它肯定会发生,因此不妨处理一下)。我不确定是否有必要,但是在这一切发生后,您可以在 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

回答by Bronix

I've found another way. If you have more backgroundWorkers you can make:

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

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

and in every backgroundWorker's DoWork method make:

并在每个 backgroundWorker 的 DoWork 方法中制作:

bgWorkesThreads.Add(Thread.CurrentThread);

Arter that you can use:

您可以使用的 Arter:

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

I used this in Word Add-in in Control, which i use in CustomTaskPane. If someone close the document or application earlier then all my backgroundWorkes finishes their work, it raises some COM Exception(I don't remember exatly which).CancelAsync()doesn't work.

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

But with this, I can close all threads which are used by backgroundworkersImmediately in DocumentBeforeCloseevent and my problem is solved.

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

回答by Kergorian

Another way:

其它的办法:

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

回答by Luke

What about Me.IsHandleCreated?

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

回答by Harald Coppoolse

Your backgroundworker should not use Invoke to update the textbox. It should ask the UI thread nicely to update the textbox using event ProgressChanged with the value to put in the textbox attached.

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

During event Closed (or maybe event Closing), the UI thread remembers that the form is closed before it cancels the backgroundworker.

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

Upon receiving the progressChanged the UI thread checks if the form is closed and only if not, it updates the textbox.

收到 progressChanged 后,UI 线程检查表单是否关闭,如果没有,它会更新文本框。