Control.BeginInvoke 无法执行委托的原因

本文关键字:执行 BeginInvoke Control | 更新日期: 2023-09-27 18:21:07

概述

是否有关于 Control.BeginInvoke(( 不执行它被传递的委托的解释?

代码示例

我们在 Winforms 应用程序中采用了以下模式,以便在 UI 线程上安全地执行 UI 相关工作:

private Control hiddenControl = new Control();
private void uiMethod()
{
  MethodInvoker uiDelegate = new MethodInvoker(delegate()
  {
    Logging.writeLine("Start of uiDelegate");
    //ui releated operations
    childDialog = new ChildDialog();
    childDialow.show();
    Logging.writeLine("End of uiDelegate");
  });
  if (hiddenControl.InvokeRequired)
  {
    Logging.writeLine("Start of InvokeRequired block");
    hiddenControl.BeginInvoke(uiDelegate);
    Logging.writeLine("End of InvokeRequired block");
  }
  else
  {
    uiDelegate();
  }
}

在这里,我们显式创建一个控件"hiddenControl",以便在 UI 线程上运行委托。 我们从不调用 endInvoke,因为它显然不是 Control.BeginInvoke 所必需的,而且我们永远不需要返回值,因为我们的方法无论如何都只是操作 UI。

虽然非常冗长,但这种模式似乎是一个相对容易接受的解决方案。 然而,有一些证据表明,即使是这种模式也可能在所有情况下都有效。

观察

我不排除应用程序错误并责怪WinForms。 毕竟,选择可能没有被破坏。 然而,我无法解释为什么代表似乎根本不会竞选。

在我们的例子中,我们有时会观察到"uiDelegate 的开始"日志消息在某些线程场景中永远不会执行,即使"调用的启动"块的开始"和"调用必需的结束块"成功执行也是如此。

复制此行为非常困难,因为我们的应用程序是作为 DLL 交付的;我们的客户在自己的应用程序中运行它。 因此,我们无法保证如何或在哪个线程中调用这些方法。

我们排除了 UI 线程不足,因为观察到 UI 未锁定。 据推测,如果 UI 正在更新,则消息泵是可操作的,可用于从消息队列中提取消息并执行其委托。

总结

鉴于这些信息,我们有什么可以尝试使这些呼叫更加防弹吗? 如前所述,我们对给定应用程序中的其他线程的控制相对较少,并且不控制调用这些方法的上下文。

还有什么会影响委托成功传递给 Control.BeginInvoke(( 的执行方式?

Control.BeginInvoke 无法执行委托的原因

根据

MSDN InvokeRequired即使在应该true InvokeRequired的情况下,即在创建该控件/窗体(或其父控件(的Handle之前访问InvokeRequired的情况下,也可以返回false

基本上您的检查不完整,这会导致您看到的结果。

您需要检查IsHandleCreated - 如果这是false那么您就有麻烦了,因为需要 Invoke/BeginInvoke,但无法正常工作,因为 Invoke/BeginInvoke检查哪个线程创建了Handle来发挥他们的魔力......

只有当IsHandleCreated true你才会根据InvokeRequired返回的内容采取行动 - 类似于:

if (control.IsHandleCreated)
{
    if (control.InvokeRequired)
    {
        control.BeginInvoke(action);
    }
    else
    {
        action.Invoke();
    }
}
else 
{ 
    // in this case InvokeRequired might lie - you need to make sure that this never happens! 
    throw new Exception ( "Somehow Handle has not yet been created on the UI thread!" );
}

因此,以下几点对于避免此问题很重要

始终确保在首次访问 UI 线程以外的线程之前已创建Handle

根据 MSDN,您只需要在 UI 线程中引用control.Handle即可强制创建它 - 在您的代码中,这必须在您第一次从不是 UI 线程的任何线程访问该控件/窗体之前发生。

有关其他可能性,请参阅@JaredPar的答案。

BeginInvoke调用可能失败的原因有多种。

    控件
  1. 及其所有父控件都没有创建内部句柄。 这将导致呼叫站点出现异常
  2. 控件发布了委托,但在 UI 线程上实际运行之前被销毁
  3. UI 线程停止泵送消息(通常是线程结束(

听起来 #2 很可能在这里引起了您的悲伤。 我在开发 winform 应用程序时遇到过几次这个问题。 这给我带来了足够的悲伤,以至于我从Control.BeginInvoke切换到SynchronizationContext.Current.PostSynchronizationContext.Current实例将在 WinForms 应用程序中 UI 线程的生命周期内存在,恕我直言,它比调用特定Control更可靠

您可能在第一次日志记录调用中引发异常。不过,我建议看一下TPL(如果您使用的是.Net 4.0(。任务可以使代码更具可读性,您可以执行以下操作

Task t = Task.Factory.StartNew(()=>{...Do Some stuff not on the UI thread...});
Task continuationTask = t.ContinueWith((previousTask)=>{...Do your UI stuff now 
         (let the Task Scheduler deal with jumping back on the UI thread...},
    TaskScheduler.FromCurrentSynchronizationContext());

然后,您还可以通过continuationTask.Exceptions轻松检查是否有任何任务有异常。