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

时间:2010-12-08 12:41:22

标签: c# winforms backgroundworker

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

以下是可能存在错误的代码:

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的预期行为吗?我不希望它对形式有任何了解,因为我无法看到任何与“工人”形式相关的形式。

第二个问题:

我该如何解决这个问题?

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

  1. 在调用cleanup()之前测试是否(!this.IsDisposed)。这还不够,或者在执行清理时可以处理表单吗?
  2. 在try {} catch(ObjectDisposedException)中将调用传递给cleanup()。我不太喜欢这种方法,因为我可能正在捕获由于cleanup()中的一些其他无关错误或其调用的方法而引发的异常。
  3. 为IsClosing注册处理程序并延迟或取消关闭,直到RunWorker Completed的处理程序运行。
  4. 可能相关的其他信息:来自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)
    

1 个答案:

答案 0 :(得分:4)

您提到的例外与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事件也可以正常工作,如果它有所作为,你可以使用两者中哪一个更方便。

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