如何停止后台工作者在表单的关闭事件?

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

有什么办法可以关闭这个应用程序而不出现异常或死锁吗?

以下是简单的 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
}
53212 次浏览

一个可行的解决方案,但是太复杂了。这个想法是产生计时器,将继续尝试关闭形式,形式将拒绝关闭,直到该 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();
}

我会将与文本框关联的 SynchronizationContext 传递给 Background Worker,并使用它在 UI 线程上执行 Update。使用 SynchronizationContext。发送后,可以检查控件是否被释放或释放。

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

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

据我所知,唯一的死锁安全和异常安全方法是实际取消 FormClosing 事件。如果 BGW 仍在运行,则设置 e.Cancel = true,并设置一个标志来指示用户请求关闭。然后在 BGW 的 RunWorkerCompleted 事件处理程序中检查该标志,如果设置了 Close () ,则调用 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...
}

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

不能调用调用或 BeginInvoke 直到窗口句柄为止 已经被创造出来了。

我认为这可以通过在“ Loached”回调中而不是构造函数中启动 worker 来修改,但是如果使用 BackoundWorker 的 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,内存使用情况似乎也不错。为了谨慎起见,也许应该使用比1ms 更高的值?如果有专家的意见,我们将不胜感激... ... 更新: 似乎只要更新不是太频繁,就应该没问题: 林克

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

这是我的解决方案(抱歉它在 VB.Net 中)。

当我运行 FormClosing 事件时,我运行 Background Worker1.CancelAsync ()来将 CancelationPending 值设置为 True。不幸的是,这个程序从来没有机会真正检查将 e.Cancel 设置为 true 的取消值(据我所知,这只能在 Background Worker1 _ DoWork 中完成)。 我没有去掉那句台词,虽然看起来没什么区别。

我添加了一行将我的全局变量 bClosingForm 设置为 True。然后,我在 Background Worker _ WorkCompleted 中添加了一行代码,以便在执行任何结束步骤之前,检查 e.Cancled 和全局变量 bClosingForm。

使用这个模板,您应该能够在任何时候关闭您的表单,即使后台工作者正在处理某些事情(这可能不太好,但它一定会发生,所以不妨处理一下)。我不确定是否有必要,但是在这一切发生之后,您可以将后台工作者完全处置在 Form _ Close 事件中。

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

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

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

在每一个背景下,Worker 的 DoWork 方法都是:

bgWorkesThreads.Add(Thread.CurrentThread);

你可以使用的艺术品:

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

我在 Word 外接程序控件中使用了这个,我在 CustomTaskPane中使用了它。如果有人提前关闭了文档或应用程序,那么我的所有后台工作都完成了工作,它会产生一些 COM Exception(我不太记得具体是哪个)。CancelAsync()不起作用。

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

另一种方式:

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

那我呢?

    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

后台工作者不应使用 Invoke 更新文本框。它应该很好地要求 UI 线程使用事件 ProgressChanged 更新文本框,并在附加的文本框中添加值。

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

在接收 Progress 时,UI 线程检查窗体是否关闭,如果不关闭,则更新文本框。

这并不适用于所有人,但是如果你正在一个 BackoundWorker 中周期性地做一些事情,比如每秒或每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);
}
}

我真的不明白为什么 DoEvents 在这种情况下会被认为是一个如此糟糕的选择,如果您正在使用 this。我觉得这样会很整洁。

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


}