[英]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: 我正在考虑可能的解决方案:
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.