Control.BeginInvoke为什么不执行委托?可能的原因是什么?

3

概述

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();
  }
}

在这里,我们为了在UI线程上运行委托,显式创建一个名为“hiddenControl”的控件。我们从不调用endInvoke,因为对于Control.BeginInvoke来说,这似乎是不必要的,而且我们也从来不需要返回值,因为我们的方法只是操作UI。

尽管非常冗长,该模式似乎是比较 被广泛 接受解决方案。然而,有证据表明,即使在所有情况下,这种模式也可能效果不佳。

观察

我不排除应用程序错误并将其归咎于WinForms。毕竟,select probably isn't broken。然而,我无法解释为什么委托可能似乎根本没有运行。
在我们的情况下,我们有时会观察到在某些线程场景中,“uiDelegate的开始”日志消息从未执行,即使“InvokeReqiured块的开始”和“InvokeRequired块的结束”成功执行。
由于我们的应用程序作为DLL交付,因此很难复制此行为。我们的客户在自己的应用程序中运行它。因此,我们不能保证这些方法可能以何种方式或在哪个线程中被调用。
我们排除了UI线程饥饿现象,因为观察到UI没有锁定。假设如果正在更新UI,则消息泵正在操作并可用于从消息队列中拉取消息并执行其委托。
总结:
鉴于这些信息,是否有任何可以尝试使这些调用更加牢固的方法?如先前提到的,我们对给定应用程序中的其他线程相对较少控制,并且不控制调用这些方法的上下文。
什么因素会影响委托成功传递给Control.BeginInvoke()并执行的情况?
3个回答

7
根据 MSDN,即使在控件/表单的 Handle(或其父级)创建之前访问 InvokeRequired 的情况下,InvokeRequired 可能会返回false,这就是你看到的结果。
基本上,你的检查不完整,导致了你所看到的结果。
你需要检查 IsHandleCreated - 如果它为false,那么你会遇到麻烦,因为一个 Invoke/BeginInvoke 是必要的,但它们不能很好地工作,因为Invoke/BeginInvoke 检查哪个线程创建了 Handle 来执行它们的操作...
只有当 IsHandleCreatedtrue时,你才会根据 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的答案。


1
非常好,我相信Control.Handle是问题所在。我们在构造函数中特别调用了control.Handle,但如果c'tor不是在主线程上调用,则该控件绑定到非UI线程,并根据http://msdn.microsoft.com/en-us/library/system.windows.forms.control.invokerequired.aspx的说法,“将控件隔离在没有消息泵的线程上并使应用程序不稳定。” - Chris Scott
嗨,这个句柄检查解决了你所有的问题吗?我们已经开始使用同步来进行这些调用,但仍然会发现一些UI未响应或一些线程永远等待UI响应(发送调用)。期待您的回复,谢谢。 - thewpfguy

4
有几个原因可能导致BeginInvoke调用失败:
  1. 控件及其所有父级未创建内部句柄。这会在调用站点引发异常。
  2. 控件发布了委托,但在它实际在UI线程上运行之前被销毁了。
  3. UI线程停止处理消息(通常是线程结束)。
听起来很可能是#2导致了你的问题。我在开发WinForm应用程序时遇到过这个问题。它给我带来了足够的烦恼,以至于我从Control.BeginInvoke切换到SynchronizationContext.Current.Post。在WinForms应用程序中,SynchronizationContext.Current实例将在UI线程的生命周期内保持活动状态,并且在我的看法下比基于特定Control的调用更可靠。

谢谢。我想尝试SynchronizationContext.Current模式,但如果无法保证在主UI线程上,该如何获取对其的引用?如果不在主线程上,SynchronizationContext.Current将返回null。我能以可靠的方式发现该上下文吗,或者我需要暴露一个方法来传递上下文? - Chris Scott
通常我只是将它传递到我启动的任何后台操作的开头。有时可能会很繁琐,但这是唯一可靠的方法来完成它。 - JaredPar
我们还尝试了使用Synchronization.Current的Send/Post方法,但有时候UI线程并没有响应这些调用,尽管它运行得很好。 - thewpfguy

3

你可能在第一次日志记录调用时抛出了异常。我建议看一下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轻松检查任何任务是否有异常。

谢谢,我会记住的,但不幸的是我们还不能转移到4.0。 - Chris Scott

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接