c# - 在跨线程 WinForm 事件处理中避免 Invoke/BeginInvoke 的困境?

标签 c# .net winforms multithreading events

我仍然受到 WinForm UI 中后台线程的困扰。为什么?以下是一些问题:

  • 显然最重要的问题是,除非我在创建它的同一个线程上执行,否则我无法修改 Control。
  • 如您所知,Invoke、BeginInvoke 等在创建 Control 之后才可用。
  • 即使在 RequiresInvoke 返回 true 之后,BeginInvoke 仍然可以抛出 ObjectDisposed,即使它没有抛出,如果控件被销毁,它也可能永远不会执行代码。
  • 即使在 RequiresInvoke 返回 true 之后,Invoke 也可以无限期地挂起,等待在调用 Invoke 的同时处置的控件执行。

  • 我正在为这个问题寻找一个优雅的解决方案,但在我详细了解我正在寻找的内容之前,我想我会澄清这个问题。这是将通用问题放在后面,并在其后面放一个更具体的示例。在此示例中,假设我们正在通过 Internet 传输大量数据。用户界面必须能够显示正在进行的传输的进度对话框。进度对话框应不断快速更新(每秒更新 5 到 20 次)。用户可以随时关闭进度对话框,并在需要时再次调用它。此外,为了争论,让我们假设如果对话框可见,它必须处理每个进度事件。用户可以在进度对话框中单击取消,通过修改事件参数,取消操作。

    现在我需要一个适合以下约束条件的解决方案:
  • 允许工作线程调用 Control/Form 上的方法并阻塞/等待直到执行完成。
  • 允许对话框本身在初始化等时调用相同的方法(因此不使用 invoke)。
  • 不对处理方法或调用事件施加实现负担,解决方案应该只更改事件订阅本身。
  • 适当处理对可能正在处理的对话的阻塞调用。不幸的是,这不像检查 IsDisposed 那样容易。
  • 必须能够与任何事件类型一起使用(假设类型为 EventHandler 的委托(delegate))
  • 不得将异常转换为 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 被分派(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/

    相关文章:

    c# - 在 .NET WinForms 中获取 MFC 功能包 GUI 元素

    c# - 使用传递的参数附加方法后分离事件处理程序

    c# - 从右到左打开 XML Sheetview

    .net - 跟踪用户在业务应用程序中的操作

    android - FasterXML Jackson ObjectMapper for .Net MVC4 JSON POST 结果类型对象

    c++ - 在线程中更改 Windows 窗体控件

    c# - 如何处理 HTTPHandler 中的异常?

    c# - Object 是否实现了 IEnumerable 接口(interface) C#?

    c# - 线程无序问题

    c# - 如何设置TreeView控件的背景图片? (VS 2008/.Net 3.5/C#/WinForms)