如何在表单的关闭事件上停止BackgroundWorker?

发布于 2024-08-11 16:52:55 字数 1017 浏览 6 评论 0原文

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

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

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:

    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
    }

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(12

〗斷ホ乔殘χμё〖 2024-08-18 16:52:55

据我所知,执行此操作的唯一死锁安全和异常安全方法是实际取消 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...
}

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.

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...
}
弃爱 2024-08-18 16:52:55

我找到了另一种方法。如果您有更多的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中使用了它。如果有人提前关闭文档或应用程序,然后我的所有后台工作都完成了工作,则会引发一些 COM 异常(我不记得具体是哪个)。CancelAsync() 不会。不工作。

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

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

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

and in every backgroundWorker's DoWork method make:

bgWorkesThreads.Add(Thread.CurrentThread);

Arter that you can use:

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.

But with this, I can close all threads which are used by backgroundworkers Immediately in DocumentBeforeClose event and my problem is solved.

离线来电— 2024-08-18 16:52:55

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

Here was my solution (Sorry it's in 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.

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.

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.

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
请止步禁区 2024-08-18 16:52:55

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

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

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();
}
青朷 2024-08-18 16:52:55

首先,ObjectDisposeException 只是这里可能的一个陷阱。运行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();
}

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

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

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

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:

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

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:

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 hijacked 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

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

咽泪装欢 2024-08-18 16:52:55

我真的不明白为什么在这种情况下,如果您使用 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);

}

I really dont see why DoEvents is regarded as such a bad choice in this case if you are using this.enabled = false. I think it would make it quite neat.

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

}
请你别敷衍 2024-08-18 16:52:55

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

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

收到 ProgressChanged 后,UI 线程检查表单是否已关闭,只有在没有关闭的情况下,才会更新文本框。

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.

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

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

心如荒岛 2024-08-18 16:52:55

这并不适合所有人,但是如果您定期在后台工作程序中执行某些操作,例如每秒或每 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);
            }
    }

This won't work for everyone, but if you are doing something in a BackgroundWorker periodically, like every second or every 10 seconds, (perhaps polling a server) this seems to work well to stop the process in an orderly manner and without error messages (at least so far) and is easy to follow;

 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);
            }
    }
新一帅帅 2024-08-18 16:52:55

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

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.

×纯※雪 2024-08-18 16:52:55

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

What about 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
十雾 2024-08-18 16:52:55

另一种方式:

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

Another way:

if (backgroundWorker.IsBusy)
{
    backgroundWorker.CancelAsync();
    while (backgroundWorker.IsBusy)
    {
        Application.DoEvents();
    }
}
雨夜星沙 2024-08-18 16:52:55

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

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 bgWorker is dead.

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();
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文