问题描述
我仍然被 WinForm UI 中的后台线程所困扰.为什么?以下是一些问题:
I'm still plagued by background threading in a WinForm UI. Why? Here are some of the issues:
- 显然是最重要的问题,除非我在创建它的同一个线程上执行,否则我无法修改控件.
- 如您所知,Invoke、BeginInvoke 等在创建控件之后才可用.
- 即使在 RequiresInvoke 返回 true 之后,BeginInvoke 仍然可以抛出 ObjectDisposed,即使它没有抛出,如果控件被销毁,它也可能永远不会执行代码.
- 即使在 RequiresInvoke 返回 true 之后,Invoke 也可以无限期地挂起,等待与调用 Invoke 同时处置的控件执行.
我正在为这个问题寻找一个优雅的解决方案,但在我详细了解我正在寻找的内容之前,我想我会澄清这个问题.这是将通用问题放在后面,并在其后面放一个更具体的示例.在这个例子中,假设我们正在通过互联网传输大量数据.用户界面必须能够显示正在进行的传输的进度对话框.进度对话框应该持续快速地更新(每秒更新 5 到 20 次).用户可以随时关闭进度对话框,并在需要时再次调用它.此外,为了争论,让我们假设如果对话框可见,它必须处理每个进度事件.用户可以在进度对话框中点击取消,通过修改事件参数,取消操作.
I'm looking for an elegant solution to this problem, but before I get into specifics of what I'm looking for I thought I would clarify the problem. This is to take the generic problem and put a more concrete example behind it. For this example let's say we are transferring larger amounts of data over the internet. The user interface must be able to show a progress dialog for the transfer already in-progress. The progress dialog should update constantly and quickly (updates 5 to 20 times per second). The user can dismiss the progress dialog at any time and recall it again if desired. And further, lets pretend for arguments sake that if the dialog is visible, it must process every progress event. The user can click Cancel on the progress dialog and via modifying the event args, cancel the operation.
现在我需要一个适合以下约束条件的解决方案:
Now I need a solution that will fit in the following box of constraints:
- 允许工作线程调用 Control/Form 上的方法并阻塞/等待直到执行完成.
- 允许对话框本身在初始化等时调用相同的方法(因此不使用 invoke).
- 不对处理方法或调用事件施加实现负担,解决方案应该只更改事件订阅本身.
- 适当处理对可能正在处理的对话的阻塞调用.不幸的是,这不像检查 IsDisposed 那样容易.
- 必须能够与任何事件类型一起使用(假设类型为 EventHandler 的委托)
- 不得将异常转换为 TargetInvocationException.
- 该解决方案必须适用于 .Net 2.0 及更高版本
那么,鉴于上述限制,这可以解决吗?我搜索和挖掘了无数的博客和讨论,可惜我仍然两手空空.
So, can this be solved given the constraints above? I've searched and dug through countless blogs and discussions and alas I'm still empty handed.
更新:我确实意识到这个问题没有简单的答案.我只在这个网站上呆了几天,我看到一些有很多经验的人回答问题.我希望这些人中的一个人已经足够解决了这个问题,让我不用花一周左右的时间来构建一个合理的解决方案.
Update: I do realize that this question has no easy answer. I've only been on this site for a couple of days and I've seen some people with a lot of experience answering questions. I'm hoping that one of these individuals has solved this sufficiently enough for me to not spend the week or so it will take to build a reasonable solution.
更新 #2:好的,我将尝试更详细地描述问题,看看会发生什么(如果有的话).以下属性允许我们确定它的状态有几件事引起了关注......
Update #2: Ok, I'm going to try and describe the problem in a little more detail and see what (if anything) shakes out. The following properties that allow us to determine it's state have a couple of things raise concerns...
Control.InvokeRequired = 如果在当前线程上运行或 IsHandleCreated 为所有父级返回 false,则记录为返回 false.我对 InvokeRequired 实现有可能抛出 ObjectDisposedException 甚至可能重新创建对象的句柄感到困扰.由于 InvokeRequired 可以在我们无法调用时返回 true(正在进行 Dispose),并且即使我们可能需要使用 invoke(进行中创建)它也可以返回 false,因此在所有情况下都不能信任.我可以看到我们可以信任 InvokeRequired 返回 false 的唯一情况是当 IsHandleCreated 在调用前后都返回 true 时(顺便说一句,InvokeRequired 的 MSDN 文档确实提到了检查 IsHandleCreated).
Control.InvokeRequired = Documented to return false if running on current thread or if IsHandleCreated returns false for all parents.I'm troubled by the InvokeRequired implementation having the potential to either throw ObjectDisposedException or potentially even re-create the object's handle. And since InvokeRequired can return true when we are not able to invoke (Dispose in progress) and it can return false even though we might need to use invoke (Create in progress) this simply can't be trusted in all cases. The only case I can see where we can trust InvokeRequired returning false is when IsHandleCreated returns true both before and after the call (BTW the MSDN docs for InvokeRequired do mention checking for IsHandleCreated).
Control.IsHandleCreated = 如果已将句柄分配给控件,则返回 true;否则为假.尽管 IsHandleCreated 是一个安全调用,但如果控件正在重新创建它的句柄,它可能会崩溃.这个潜在问题似乎可以通过在访问 IsHandleCreated 和 InvokeRequired 时执行锁定(控制)来解决.
Control.IsHandleCreated = Returns true if a handle has been assigned to the control; otherwise, false.Though IsHandleCreated is a safe call it may breakdown if the control is in the process of recreating it's handle. This potential problem appears to be solveable by performing a lock(control) while accessing the IsHandleCreated and InvokeRequired.
Control.Disposing = 如果控件正在处理中,则返回 true.
Control.Disposing = Returns true if the control is in the process of disposing.
...我的头很痛:(希望上面的信息能让任何有这些问题的人更清楚地了解这些问题.我感谢你在这方面的空闲思考周期.
... my head hurts :( Hopefully the information above will shed a little more light on the issues for anyone having these troubles. I appreciate your spare thought cycles on this.
总结一下麻烦... 下面是 Control.DestroyHandle() 方法的后半部分:
Closing in on the trouble... The following is the later half of the Control.DestroyHandle() method:
if (!this.RecreatingHandle && (this.threadCallbackList != null))
{
lock (this.threadCallbackList)
{
Exception exception = new ObjectDisposedException(base.GetType().Name);
while (this.threadCallbackList.Count > 0)
{
ThreadMethodEntry entry = (ThreadMethodEntry) this.threadCallbackList.Dequeue();
entry.exception = exception;
entry.Complete();
}
}
}
if ((0x40 & ((int) ((long) UnsafeNativeMethods.GetWindowLong(new HandleRef(this.window, this.InternalHandle), -20)))) != 0)
{
UnsafeNativeMethods.DefMDIChildProc(this.InternalHandle, 0x10, IntPtr.Zero, IntPtr.Zero);
}
else
{
this.window.DestroyHandle();
}
您会注意到 ObjectDisposedException 被分派给所有等待的跨线程调用.紧随其后的是对 this.window.DestroyHandle() 的调用,它依次销毁窗口并将其句柄引用设置为 IntPtr.Zero,从而防止进一步调用 BeginInvoke 方法(或更准确地说,MarshaledInvoke 处理 BeginInvoke 和 Invoke).这里的问题是在 threadCallbackList 上的锁释放之后,可以在 Control 的线程将窗口句柄归零之前插入一个新条目.这似乎是我所看到的情况,虽然很少见,但经常足以停止发布.
You'll notice the ObjectDisposedException being dispatched to all waiting cross-thread invocations. Shortly following this is the call to this.window.DestroyHandle() which in turn destroys the window and set's it's handle reference to IntPtr.Zero thereby preventing further calls into the BeginInvoke method (or more precisely MarshaledInvoke which handle both BeginInvoke and Invoke). The problem here is that after the lock releases on threadCallbackList a new entry can be inserted before the Control's thread zeros the window handle. This appears to be the case I'm seeing, though infrequently, often enough to stop a release.
更新 #4:
很抱歉继续拖延;但是,我认为值得在这里记录.我已经设法解决了上面的大部分问题,并且正在寻找一个有效的解决方案.我又遇到了一个我担心的问题,但直到现在,还没有看到in-the-wild".
Sorry to keep dragging this on; however, I thought it worth documenting here. I've managed to solve most of the problems above and I'm narrowing in on a solution that works. I've hit one more issue I was concerned about, but until now, have not seen 'in-the-wild'.
这个问题与编写 Control.Handle 属性的天才有关:
This issue has to do with the genius that wrote Control.Handle property:
public IntPtr get_Handle()
{
if ((checkForIllegalCrossThreadCalls && !inCrossThreadSafeCall) && this.InvokeRequired)
{
throw new InvalidOperationException(SR.GetString("IllegalCrossThreadCall", new object[] { this.Name }));
}
if (!this.IsHandleCreated)
{
this.CreateHandle();
}
return this.HandleInternal;
}
这本身还不错(不管我对 get { } 修改的看法如何);但是,当与 InvokeRequired 属性或 Invoke/BeginInvoke 方法结合使用时,效果不佳.这是调用的基本流程:
This by itself is not so bad (regardless of my opinions on get { } modifications); however, when combined with the InvokeRequired property or the Invoke/BeginInvoke method it is bad. Here is the basic flow the Invoke:
if( !this.IsHandleCreated )
throw;
... do more stuff
PostMessage( this.Handle, ... );
这里的问题是,从另一个线程我可以成功通过第一个if语句,之后句柄被控件的线程销毁,从而导致Handle属性的get在我的线程上重新创建窗口句柄.这会导致在原始控件的线程上引发异常.这个真的让我难住了,因为没有办法防范.如果他们只使用 InternalHandle 属性并测试 IntPtr.Zero 的结果,这将不是问题.
The issue here is that from another thread I can successfully pass through the first if statement, after which the handle is destroyed by the control's thread, thus causing the get of the Handle property to re-create the window handle on my thread. This then can cause an exception to be raised on the original control's thread. This one really has me stumped as there is no way to guard against this. Had they only use the InternalHandle property and tested for result of IntPtr.Zero this would not be an issue.
推荐答案
如上所述,您的场景非常适合 BackgroundWorker
- 为什么不直接使用它?您对解决方案的要求过于笼统,而且相当不合理 - 我怀疑是否有任何解决方案可以满足所有要求.
Your scenario, as described, neatly fits BackgroundWorker
- why not just use that? Your requirements for a solution are way too generic, and rather unreasonable - I doubt there is any solution that would satisfy them all.
这篇关于在跨线程 WinForm 事件处理中避免 Invoke/BeginInvoke 的困境?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!