如何在使用后台工作时正确处理表单关闭?

我在一些代码中观察到一个奇怪的错误,我怀疑它与关闭表单和后台工作者交互的方式有关。

以下是可能出错的代码:

var worker = new BackgroundWorker(); worker.DoWork += (sender, args) => { command(); }; worker.RunWorkerCompleted += (sender, args) => { cleanup(); if (args.Error != null) MessageBox.Show("...", "...", MessageBoxButtons.OK, MessageBoxIcon.Exclamation); }; worker.RunWorkerAsync(); 

当按下按钮时,该代码在表单中的方法中执行。 command()很慢,可能需要几秒钟才能运行。

用户按下执行上述代码的按钮以执行。 在完成之前,表单已关闭。

问题是调用cleanup() 有时会引发ObjectDisposedException。 我说“有时”,因为这在我的电脑上永远不会发生。 如果在完成command()之前关闭了表单,则不会执行我为RunWorkerCompleted注册的处理程序。 在另一台计算机上,处理程序一次被调用一次。 在同事的电脑上,它几乎总是被称为。 显然,处理程序的执行概率随着计算机的年龄/速度而增加。

第一个问题:

这是BakgroundWorker的预期行为吗? 我不希望它对forms有任何了解,因为我看不到任何与“工人”相关的forms。

第二个问题:

我该如何解决这个问题呢?

我正在考虑可能的解决方案:

  1. 在调用cleanup()之前测试是否(!this.IsDisposed)。 这还不够,还是可以在执行清理时处理表单?
  2. 在try {} catch(ObjectDisposedException)中调用cleanup()。 我不太喜欢这种方法,因为我可能会捕获由于cleanup()中的一些其他无关错误或它调用的方法之一而引发的exception。
  3. 注册IsClosing的处理程序并延迟或取消关闭,直到RunWorker Completed的处理程序运行。

可能相关的其他信息:来自command()的代码将导致对“this”中的GUI对象进行更新。 通过调用此F#函数执行此类更新:

 /// Run a delegate on a ISynchronizeInvoke (typically a Windows.Form). let runOnInvoker (notification_receiver : ISynchronizeInvoke) excHandler (dlg : Delegate) args = try let args : System.Object[] = args |> Seq.cast |> Array.ofSeq notification_receiver.Invoke (dlg, args) |> ignore with | :? System.InvalidOperationException as op -> excHandler(op) 

您提到的例外与BackgroundWorker没有任何连接,除了一个线程(工作者)试图访问已由另一个线程(UI)处理的控件的事实。

我将使用的解决方案是将事件处理程序附加到Form.FormClosed事件以设置一个标志,告诉您UI已被拆除。 然后, RunWorkerCompleted句柄将在尝试对表单执行任何操作之前检查UI是否已被拆除。

虽然如果您没有明确处理表单,这种方法可能比检查IsDisposed更可靠地工作,但它不能100%保证在清理代码检查标志并找到之后表单不会被关闭和/或处理它还在那里。 这是你自己提到的竞争条件。

要消除这种竞争条件,您需要进行同步,例如:

 // set this to new object() in the constructor public object CloseMonitor { get; private set; } public bool HasBeenClosed { get; private set; } private void Form1_FormClosed(object sender, FormClosedEventArgs e) { lock (this.CloseMonitor) { this.HasBeenClosed = true; // other code } } 

而对于工人:

 worker.RunWorkerCompleted += (sender, args) => { lock (form.CloseMonitor) { if (form.HasBeenClosed) { // maybe special code for this case } else { cleanup(); // and other code } } }; 

Form.FormClosing事件也可以正常工作,你可以使用两者中哪一个更方便,如果它Form.FormClosing

请注意,编写此代码的方式,两个事件处理程序都将被安排在UI线程上执行(这是因为WinForms组件使用单线程单元模型),因此您实际上不会受到竞争条件的影响。 但是,如果您决定在将来生成更multithreading,则可能会暴露竞争条件,除非您使用锁定。 在实践中,我经常看到这种情况发生,所以我建议同步以防止面向未来。 性能不会受到影响,因为同步只发生一次。