在WinForm UI中,我仍然受到后台线程的困扰。为什么呢?下面是一些问题:
- 显然最重要的问题是,除非我在创建它的同一个线程上执行,否则我无法修改控件。
- 正如您所知,Invoke、BeginInvoke等在创建控件之前是不可用的。
- 即使RequiresInvoke返回true,当控件被销毁时,BeginInvoke仍然可能抛出ObjectDisposed异常,即使它没有抛出异常,它也可能永远不会执行代码。
- 即使RequiresInvoke返回true,Invoke也可能无限期地挂起等待与调用Invoke同时被处理的控件执行。
我正在寻找一个优雅的解决方案来解决这个问题,但在进入我需要的具体内容之前,我想澄清这个问题。这是为了将通用问题具体化。假设我们正在通过互联网传输大量数据,用户界面必须能够显示当前正在进行的传输的进度对话框。进度对话框应该快速而持续地更新(每秒更新5到20次)。用户可以随时取消进度对话框,如果需要,可以再次调用它。此外,让我们假装,如果对话框可见,它必须处理每个进度事件。用户可以在进度对话框上单击“取消”,通过修改事件参数,取消操作。
现在我需要一个符合以下约束条件的解决方案:
- 允许工作线程调用控件/窗体上的方法并阻塞/等待,直到执行完成。
- 允许对话框本身在初始化或类似情况下调用此同一方法(因此不使用invoke)。
- 不应对处理方法或调用事件施加任何实现负担,解决方案应该只改变事件订阅本身。
- 适当地处理阻塞调用到可能正在处于处理过程中的对话框。不幸的是,这并不像检查IsDisposed那么容易。
- 必须能够与任何事件类型一起使用(假设委托类型为EventHandler)
- 不能将异常转换为TargetInvocationException。
- 该解决方案必须适用于.Net 2.0及更高版本
所以,考虑到上述限制条件,这个问题是否可以解决?我已经搜索和研究了无数的博客和讨论,但我仍然一筹莫展。
更新:我意识到这个问题没有简单的答案。我在这个网站上只呆了几天,看到了一些有经验的人回答问题。我希望其中有一个人已经解决了这个问题,使我不必花费一周左右的时间来构建一个合理的解决方案。
更新#2:好的,我将尝试更详细地描述问题,并看看会出现什么(如果有)...
Control.InvokeRequired = 返回false,如果在当前线程上运行或如果IsHandleCreated对所有父级都返回false。
Control.IsHandleCreated = 如果已为控件分配了句柄,则返回true;否则返回false。
Control.Disposing = 如果正在处理disposing过程中,则返回true。
Control.IsDisposed = 如果控件已被处理,则返回true。
InvokeRequired的实现可能会引发ObjectDisposedException,甚至可能重新创建对象的句柄。当Dispose正在进行时,InvokeRequired可以返回true,即使需要使用invoke也可能返回false(Create in progress)。因此,在某些情况下,InvokeRequired不能完全信任。唯一可以信任InvokeRequired返回false的情况是,IsHandleCreated在调用之前和之后都返回true(顺便说一下,MSDN文档提到了检查IsHandleCreated)。
虽然IsHandleCreated是一个安全的调用,但如果控件正在重新创建句柄,则可能会出现问题。通过在访问IsHandleCreated和InvokeRequired时执行lock(control),可以解决这个潜在的问题。
我考虑订阅Disposed事件并检查IsDisposed属性,以确定BeginInvoke是否会完成。这里的大问题是,在Disposing -> Disposed转换期间缺乏同步锁。如果您订阅了Disposed事件并验证Disposing == false&& IsDisposed == false,您仍然可能永远不会看到Disposed事件的触发。这是因为Dispose的实现设置Disposing = false,然后设置Disposed = true。这提供了一个机会(尽管很小),让您在处理过的控件上将Disposing和IsDisposed都读取为false。
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)。 这里的问题是,在线程CallbackList上释放锁定后,控件的线程可以在将窗口句柄置零之前插入新条目。 尽管不经常发生,但这似乎是我看到的情况,足以阻止发布。
更新#4:
很抱歉让这个问题一直拖着,但我认为在这里记录它值得一提。 我已成功解决了上面大部分的问题,并专注于找到可行的解决方案。 我遇到的最后一个问题是我担心的问题,但直到现在,我还没有在“野外”中看到过。
这个问题与编写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方法相结合时就变得糟糕了。以下是Invoke的基本流程:
if( !this.IsHandleCreated )
throw;
... do more stuff
PostMessage( this.Handle, ... );
这里的问题在于,另一个线程可以成功通过第一个 if 语句,之后控制线程将销毁句柄,因此会导致在我的线程上重新创建窗口句柄时获取句柄属性。这可能会导致在原始控件线程上引发异常。这真的让我感到困惑,因为没有办法防止这种情况发生。如果他们只使用了 InternalHandle 属性并测试了 IntPtr.Zero 的结果,那么这就不会成为问题。