【发布时间】:2010-11-24 17:56:39
【问题描述】:
我仍然被 WinForm UI 中的后台线程所困扰。为什么?以下是一些问题:
- 显然最重要的问题是,除非我在创建控件的同一线程上执行,否则我无法修改控件。
- 如您所知,Invoke、BeginInvoke 等在创建控件之前不可用。
- 即使在 RequiresInvoke 返回 true 后,BeginInvoke 仍然可以抛出 ObjectDisposed,即使它没有抛出,如果控件被销毁,它也可能永远不会执行代码。
- 即使在 RequiresInvoke 返回 true 后,Invoke 也可以无限期挂起,等待在调用 Invoke 的同时释放的控件执行。
我正在寻找一个优雅的解决方案来解决这个问题,但在我详细了解我正在寻找的内容之前,我想我会澄清这个问题。这是为了解决一般问题,并在其背后放置一个更具体的例子。对于此示例,假设我们正在通过 Internet 传输大量数据。用户界面必须能够显示正在进行的传输的进度对话框。进度对话框应不断快速更新(每秒更新 5 到 20 次)。用户可以随时关闭进度对话框并在需要时再次调用它。此外,为了争论,假设对话框是可见的,它必须处理每个进度事件。用户可以在进度对话框中点击取消,通过修改事件参数,取消操作。
现在我需要一个适合以下约束框的解决方案:
- 允许工作线程调用控件/表单上的方法并阻塞/等待直到执行完成。
- 允许对话框本身在初始化等时调用相同的方法(因此不使用调用)。
- 不对处理方法或调用事件施加任何实现负担,解决方案只需更改事件订阅本身即可。
- 适当地处理对可能正在处理中的对话框的阻塞调用。不幸的是,这不像检查 IsDisposed 那样简单。
- 必须能够与任何事件类型一起使用(假设 EventHandler 类型的委托)
- 不得将异常转换为 TargetInvocationException。
- 该解决方案必须适用于 .Net 2.0 及更高版本
那么,考虑到上述限制,这可以解决吗?我已经搜索和挖掘了无数的博客和讨论,可惜我还是两手空空。
更新:我确实意识到这个问题没有简单的答案。我只在这个网站上呆了几天,我看到一些有很多经验的人回答问题。我希望这些人中的某个人已经解决了这个问题,足以让我不用花费一周左右的时间来构建一个合理的解决方案。
更新 #2:好的,我将尝试更详细地描述问题,看看有什么问题(如果有的话)。以下允许我们确定其状态的属性有几件事引起关注...
Control.InvokeRequired = 如果在当前线程上运行或 IsHandleCreated 为所有父级返回 false,则记录为返回 false。 我对 InvokeRequired 实现有可能抛出 ObjectDisposedException 甚至可能重新创建对象的句柄感到困扰。而且由于 InvokeRequired 可以在我们无法调用(正在处理中)时返回 true,并且即使我们可能需要使用调用(正在创建中),它也可以返回 false,这在所有情况下都不能被信任。我可以看到我们可以信任 InvokeRequired 返回 false 的唯一情况是 IsHandleCreated 在调用前后都返回 true (顺便说一句,InvokeRequired 的 MSDN 文档确实提到了检查 IsHandleCreated)。
Control.IsHandleCreated = 如果已将句柄分配给控件,则返回 true;否则为假。 虽然 IsHandleCreated 是一个安全调用,但如果控件正在重新创建它的句柄,它可能会崩溃。这个潜在问题似乎可以通过在访问 IsHandleCreated 和 InvokeRequired 时执行锁定(控制)来解决。
Control.Disposing = 如果控件处于释放过程中,则返回 true。
- Control.IsDisposed = 如果控件已被释放,则返回 true。 我正在考虑订阅 Disposed 事件并检查 IsDisposed 属性以确定 BeginInvoke 是否会完成。这里最大的问题是在 Disposing -> Disposed 转换期间缺少同步锁。如果您订阅 Disposed 事件并在此之后验证 Disposing == false && IsDisposed == false 您仍然可能永远不会看到 Disposed 事件触发。这是因为 Dispose 的实现设置 Disposing = false,然后设置 Disposed = true。这为您提供了一个机会(无论多么小),在已处置的控件上将 Disposing 和 IsDisposed 都读取为 false。
...我头疼 :( 希望上面的信息能为遇到这些麻烦的人提供更多的启示。感谢您在这方面的空闲思考周期。
解决问题...下面是Control.DestroyHandle()方法的后半部分:
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 方法(或更准确地说是处理 BeginInvoke 和 Invoke 的 MarshaledInvoke)。这里的问题是,在 threadCallbackList 上的锁释放后,可以在 Control 的线程将窗口句柄归零之前插入一个新条目。这似乎是我所看到的情况,虽然不常见,但经常足以停止发布。
更新#4:
很抱歉一直拖下去;但是,我认为值得在这里记录。我已经设法解决了上述大部分问题,并且正在缩小范围内的解决方案。我又遇到了一个我担心的问题,但直到现在,还没有看到'in-the-wild'。
这个问题与编写 Control.Handle 属性的天才有关:
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 方法结合使用时,它就很糟糕了。这是调用的基本流程:
if( !this.IsHandleCreated )
throw;
... do more stuff
PostMessage( this.Handle, ... );
这里的问题是我可以从另一个线程成功通过第一个 if 语句,之后句柄被控件的线程销毁,从而导致 Handle 属性的获取在我的线程上重新创建窗口句柄.这可能会导致在原始控件的线程上引发异常。这真的让我很难过,因为没有办法防范这一点。如果他们只使用 InternalHandle 属性并测试 IntPtr.Zero 的结果,这将不是问题。
【问题讨论】:
-
提问的时候可以客气一点。
标签: c# .net winforms multithreading events