我仍然受到 WinForm UI 中后台线程的困扰。为什么?以下是一些问题:
我正在为这个问题寻找一个优雅的解决方案,但在我详细了解我正在寻找的内容之前,我想我会澄清这个问题。这是将通用问题放在后面,并在其后面放一个更具体的示例。在此示例中,假设我们正在通过 Internet 传输大量数据。用户界面必须能够显示正在进行的传输的进度对话框。进度对话框应不断快速更新(每秒更新 5 到 20 次)。用户可以随时关闭进度对话框,并在需要时再次调用它。此外,为了争论,让我们假设如果对话框可见,它必须处理每个进度事件。用户可以在进度对话框中单击取消,通过修改事件参数,取消操作。
现在我需要一个适合以下约束条件的解决方案:
那么,鉴于上述约束,这可以解决吗?我搜索和挖掘了无数的博客和讨论,可惜我仍然两手空空。
更新:我确实意识到这个问题没有简单的答案。我只在这个网站上呆了几天,我看到一些有很多经验的人回答问题。我希望这些人中的一个人已经足够地解决了这个问题,让我不用花一周左右的时间来建立一个合理的解决方案。
更新 #2:好的,我将尝试更详细地描述问题,看看会发生什么(如果有的话)。以下属性允许我们确定它的状态有几件事引起了关注......
我对 InvokeRequired 实现有可能抛出 ObjectDisposedException 甚至可能重新创建对象的句柄感到困扰。由于 InvokeRequired 可以在我们无法调用时返回 true(正在进行处理),并且即使我们可能需要使用调用(进行中创建)它也可以返回 false,因此在所有情况下都不能信任。我可以看到我们可以信任 InvokeRequired 返回 false 的唯一情况是当 IsHandleCreated 在调用前后都返回 true 时(顺便说一句,InvokeRequired 的 MSDN 文档确实提到检查 IsHandleCreated)。
尽管 IsHandleCreated 是一个安全调用,但如果控件正在重新创建它的句柄,它可能会崩溃。这个潜在问题似乎可以通过在访问 IsHandleCreated 和 InvokeRequired 时执行锁定(控制)来解决。
我正在考虑订阅 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 被分派(dispatch)给所有等待的跨线程调用。紧随其后的是对 this.window.DestroyHandle() 的调用,它依次销毁窗口并将其句柄引用设置为 IntPtr.Zero,从而防止进一步调用 BeginInvoke 方法(或更准确地说,MarshaledInvoke 处理 BeginInvoke 和 Invoke)。这里的问题是在 threadCallbackList 上的锁释放之后,可以在 Control 的线程将窗口句柄归零之前插入一个新条目。这似乎是我所看到的情况,虽然很少见,但通常足以阻止发布。
更新 #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 方法结合使用时,效果不佳。这是调用的基本流程:
if( !this.IsHandleCreated )
throw;
... do more stuff
PostMessage( this.Handle, ... );
这里的问题是,从另一个线程我可以成功地通过第一个 if 语句,之后句柄被控件的线程销毁,从而导致 Handle 属性的 get 在我的线程上重新创建窗口句柄。这会导致在原始控件的线程上引发异常。这个真的让我难住了,因为没有办法防范。如果他们只使用 InternalHandle 属性并测试 IntPtr.Zero 的结果,这将不是问题。
最佳答案
如上所述,您的场景非常适合 BackgroundWorker
- 为什么不直接使用它?您对解决方案的要求太笼统,而且相当不合理 - 我怀疑是否有任何解决方案可以满足所有要求。
关于c# - 在跨线程 WinForm 事件处理中避免 Invoke/BeginInvoke 的困境?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/1364116/