c# - 使用后台 worker 时如何正确处理表单关闭?

标签 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 的预期行为吗?我不希望它知道有关表单的任何信息,因为我看不到任何将表单“this”与“worker”联系起来的东西。

第二个问题:

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

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

  1. 在调用 cleanup() 之前测试是否 (!this.IsDisposed)。这够了吗,还是可以在执行清理时处理掉表单?
  2. 将对 cleanup() 的调用包装在 try {} catch (ObjectDisposedException) 中。我不太喜欢这种方法,因为我可能会捕获由于 cleanup() 或它调用的方法之一中的其他不相关错误而引发的异常。
  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 没有任何联系,除了一个线程(worker)试图访问已被另一个线程(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 :

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 组件使用单线程单元模型),因此您实际上不会受到竞争条件的影响.但是,如果您决定在将来生成更多线程,您可能会暴露竞争条件,除非您确实使用锁定。在实践中,我经常看到这种情况发生,所以我建议无论如何都要进行同步,以防万一。性能不会受到影响,因为同步只会发生一次。

关于c# - 使用后台 worker 时如何正确处理表单关闭?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/4387527/

相关文章:

c# - 如何使用 C# 将 CSV 数据粘贴到 Windows 剪贴板

c# - 如何在异常情况下读取 HttpActionExecutedContext 中的 JSON 正文

c# - 在 Winforms 中使用控件(链接标签、 TreeView )

c# - 如何将数据类型(string)转换为类型(form)

c# - 为什么这段代码会抛出异常?我认为 RunWorkerCompleted 可以访问 UI 元素

c# - react 性扩展 : buffer until subscriber is idle

c# - 如何定义与 ValueObjects 的嵌套模型关系

c# - 无法将文件写入 Windows 7 中的 Application.StartupPath

wpf - 可以使用BackgroundWorker的ReportProgress来做其他事情吗?

c# - 注意到涉及 BackgroundWorker 的模式的抽象