简体   繁体   English

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

[英]How to properly handle form closing when using background workers?

I am observing a strange bug in some of my code which I suspect is tied to the way closing a form and background workers interact. 我在一些代码中观察到一个奇怪的错误,我怀疑它与关闭表单和后台工作者交互的方式有关。

Here is the code potentially at fault: 这是可能出错的代码:

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

This code is executed in a method in a form, when a button is pressed. 当按下按钮时,该代码在表单中的方法中执行。 command() is slow, it may take a few seconds to run. command()很慢,可能需要几秒钟才能运行。

The user presses a button which executes the code above to be executed. 用户按下执行上述代码的按钮以执行。 Before it is done, the form is closed. 在完成之前,表单已关闭。

The problem is that calling cleanup() sometimes raises ObjectDisposedException. 问题是调用cleanup() 有时会引发ObjectDisposedException。 I say "sometimes", because this never happens on my computer. 我说“有时”,因为这在我的电脑上永远不会发生。 If the form is closed before command() is done, the handler I registered for RunWorkerCompleted is not executed. 如果在完成command()之前关闭了表单,则不会执行我为RunWorkerCompleted注册的处理程序。 On another computer, the handler is called once out of hundred times. 在另一台计算机上,处理程序一次被调用一次。 On a coworker's computer, it's almost always called. 在同事的电脑上,它几乎总是被称为。 Apparently, the probability of execution of the handler rises with the age/slowness of the computer. 显然,处理程序的执行概率随着计算机的年龄/速度而增加。

First question: 第一个问题:

Is this the expected behaviour of BakgroundWorker? 这是BakgroundWorker的预期行为吗? I would not expect it to know anything about the form, as there is nothing I can see that ties the form "this" with "worker". 我不希望它对表格有任何了解,因为我无法看到任何与“工人”形式相关联的形式。

Second question: 第二个问题:

How should I go about fixing that problem? 我该如何解决这个问题呢?

Possible solutions I'm considering: 我正在考虑可能的解决方案:

  1. Test if (!this.IsDisposed) before calling cleanup(). 在调用cleanup()之前测试是否(!this.IsDisposed)。 Is that enough, or can the form be disposed while cleanup is being executed? 这还不够,还是可以在执行清理时处理表单?
  2. Wrap the call to cleanup() in a try {} catch (ObjectDisposedException). 在try {} catch(ObjectDisposedException)中调用cleanup()。 I don't like that kind of approach too much, as I may be catching exceptions that were raised due to some other unrelated bug in cleanup() or one of the methods it calls. 我不太喜欢这种方法,因为我可能会捕获由于cleanup()中的一些其他无关错误或其调用的方法而引发的异常。
  3. Register a handler for IsClosing and delay or cancel closing until the handler for RunWorker Completed has run. 注册IsClosing的处理程序并延迟或取消关闭,直到RunWorker Completed的处理程序运行。

Additional information that may be relevant: code from command() will cause updates to be done to GUI objects in "this". 可能相关的其他信息:来自command()的代码将导致对“this”中的GUI对象进行更新。 Such updates are performed via calls to this F# function: 通过调用此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)

The exceptions you mentioned do not have any connection to BackgroundWorker , other than the fact that one thread (the worker) tries to access controls which have been disposed by another thread (the UI). 您提到的例外与BackgroundWorker没有任何连接,除了一个线程(工作者)试图访问已由另一个线程(UI)处理的控件的事实。

The solution I would use is to attach an event handler to the Form.FormClosed event to set a flag that tells you the UI has been torn down. 我将使用的解决方案是将事件处理程序附加到Form.FormClosed事件以设置一个标志,告诉您UI已被拆除。 Then, then RunWorkerCompleted handle will check to see if the UI has been torn down before trying to do anything with the form. 然后,在尝试对表单执行任何操作之前, RunWorkerCompleted句柄将检查UI是否已被拆除。

While this approach will probably work more reliably than checking IsDisposed if you are not disposing the form explicitly, it does not provide a 100% guarantee that the form will not be closed and/or disposed just after the cleanup code has checked the flag and found that it is still there. 虽然如果您没有明确处理表单,这种方法可能比检查IsDisposed更可靠地工作,但它不能100%保证在清理代码检查了标志并找到之后表单不会被关闭和/或处理它还在那里。 This is the race condition you yourself mention. 这是你自己提到的竞争条件。

To eliminate this race condition, you will need to synchronize, for example like this: 要消除这种竞争条件,您需要进行同步,例如:

// 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
    }
}

and for the worker: 而对于工人:

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

The Form.FormClosing event will also work fine for this purpose, you can use whichever of the two is more convenient if it makes a difference. Form.FormClosing事件也可以正常工作,你可以使用两者中哪一个更方便,如果它Form.FormClosing

Note that, the way this code is written, both event handlers will be scheduled for execution on the UI thread (this is because WinForms components use a single-threaded apartment model) so you would actually not be affected by a race condition. 请注意,编写此代码的方式,两个事件处理程序将被安排在UI线程上执行(这是因为WinForms组件使用单线程单元模型),因此您实际上不会受到竞争条件的影响。 However, if you decide to spawn more threads in the future you might expose the race condition unless you do use locking. 但是,如果您决定在将来生成更多线程,则可能会暴露竞争条件,除非您使用锁定。 In practice I have seen this happen quite often, so I suggest synchronizing anyway to be future-proof. 在实践中,我经常看到这种情况发生,所以我建议同步,以便面向未来。 Performance will not be affected as the sync only happens once. 性能不会受到影响,因为同步只发生一次。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM