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(( 的执行方式?
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
调用可能失败的原因有多种。
- 控件
- 及其所有父控件都没有创建内部句柄。 这将导致呼叫站点出现异常
- 控件发布了委托,但在 UI 线程上实际运行之前被销毁
- UI 线程停止泵送消息(通常是线程结束(
听起来 #2 很可能在这里引起了您的悲伤。 我在开发 winform 应用程序时遇到过几次这个问题。 这给我带来了足够的悲伤,以至于我从Control.BeginInvoke
切换到SynchronizationContext.Current.Post
。 SynchronizationContext.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轻松检查是否有任何任务有异常。