BackgroundWorker OnWorkCompleted抛出跨线程exception

我有一个简单的UserControl用于数据库分页,它使用控制器来执行实际的DAL调用。 我使用BackgroundWorker来执行繁重的工作,并在OnWorkCompleted事件上重新启用一些按钮,更改TextBox.Text属性并为父窗体引发事件。

表单A保存我的UserControl。 当我点击打开表单B的某个按钮时,即使我没有做任何“那里”并且只是关闭它,并尝试从我的数据库引入下一页, OnWorkCompleted会在工作线程上调用(而不是我的主线程),并抛出一个跨线程exception。

目前我在那里的处理程序中添加了对InvokeRequired的检查,但是不是要在主线程上调用OnWorkCompleted的全部内容吗? 为什么不按预期工作?

编辑:

我设法将问题缩小到arcgis和BackgroundWorker 。 我有以下解决方案,它向arcmap添加一个命令,打开一个带有两个按钮的简单Form1

第一个按钮运行一个hibernate500毫秒的BackgroundWorker并更新计数器。 在RunWorkerCompleted方法中,它检查InvokeRequired ,并更新标题以显示方法最初在主线程或工作线程内运行。 第二个按钮只打开Form2 ,它什么都不包含。

首先,所有对RunWorkerCompletedare的调用都是在主线程内完成的(正如预期的那样 – 这就是RunWorkerComplete方法的最后一点,至少我从BackgroundWorker上的MSDN中了解到的)

打开和关闭Form2 ,始终在工作线程上调用RunWorkerCompleted 。 我想补充一点,我可以将此解决方案保留原样(在RunWorkerCompleted方法中检查InvokeRequired ),但我想了解为什么它会违背我的期望。 在我的“真实”代码中,我想知道在主线程上调用RunWorkerCompleted方法。

我设法在form.Show(); 我的BackgroundTesterBtn命令 – 如果我使用ShowDialog() ,我没有问题( RunWorkerCompleted总是在主线程上运行)。 我需要在我的ArcMap项目中使用Show() ,这样用户就不会绑定到表单了。

我还尝试在正常的WinForms项目上重现该错误。 我添加了一个简单的项目,只打开没有ArcMap的第一个表单,但在这种情况下我无法重现该错误 – RunWorkerCompleted在主线程上运行,无论我在打开Form2之前和之后使用Show()ShowDialog() 。 我尝试在Form1之前添加第三个表单作为主表单,但它没有改变结果。

这是我的简单sln(VS2005sp1) – 它需要

ESRI.ArcGIS.ADF(9.2.4.1420)

ESRI.ArcGIS.ArcMapUI(9.2.3.1380)

ESRI.ArcGIS.SystemUI(9.2.3.1380)

它看起来像一个bug:

http://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=116930

http://thedatafarm.com/devlifeblog/archive/2005/12/21/39532.aspx

所以我建议使用防弹(伪代码):

 if(control.InvokeRequired) control.Invoke(Action); else Action() 

是不是要在主线程上调用OnWorkCompleted的全部内容? 为什么不按预期工作?

不,这不对。
你不能在任何旧线程上运行任何旧东西。 线程不是礼貌的对象,你可以简单地说“运行这个,请”。

线程的更好的心理模型是货运列车。 一旦它开始,它就在它自己的轨道上。 你无法改变它的路线或阻止它。 如果你想影响它,你要么必须等到它到达下一个火车站(例如:让它手动检查一些事件),或者使它脱轨( Thread.Abort和CrossThreadexception与脱轨一样有很多相同的后果火车……小心!)。

Winforms控件支持这种行为(它们具有Control.BeginInvoke ,它允许你在UI线程上运行任何函数),但这只能起作用,因为它们有一个特殊的钩子进入Windows UI消息泵并编写一些特殊的处理程序。 按照上述类比,他们的火车在车站办理登机手续并定期查找新方向,您可以使用该设施发布自己的指示。

BackgroundWorker旨在用于通用(它不能绑定到Windows GUI),因此它无法使用Windows Control.BeginInvokefunction。 它必须假设你的主线程是一个不可阻挡的“火车”做它自己的事情,所以完成的事件必须在工作线程中运行或根本不运行。

但是,当您使用winforms时,在OnWorkCompleted处理程序中,您可以使用上面提到的BeginInvokefunction让Window执行另一个回调。 像这样:

 // Assume we're running in a windows forms button click so we have access to the // form object in the "this" variable. void OnButton_Click(object sender, EventArgs e ) var b = new BackgroundWorker(); b.DoWork += ... blah blah // attach an anonymous function to the completed event. // when this function fires in the worker thread, it will ask the form (this) // to execute the WorkCompleteCallback on the UI thread. // when the form has some spare time, it will run your function, and // you can do all the stuff that you want b.RunWorkerCompleted += (s, e) { this.BeginInvoke(WorkCompleteCallback); } b.RunWorkerAsync(); // GO! } void WorkCompleteCallback() { Button.Enabled = false; //other stuff that only works in the UI thread } 

另外,不要忘记这个:

在访问Result属性之前,RunWorkerCompleted事件处理程序应始终检查Error和Canceled属性。 如果引发了exception或操作已取消,则访问Result属性会引发exception。

BackgroundWorker检查委托实例是否指向支持ISynchronizeInvoke接口的ISynchronizeInvoke 。 您的DAL层可能没有实现该接口。 通常,您可以在Form上使用BackgroundWorker ,它支持该接口。

如果您想使用DAL层中的BackgroundWorker并希望从那里更新UI,您有三个选择:

  • 你会继续调用Invoke方法
  • 在DAL类上实现接口ISynchronizeInvoke ,并手动重定向调用(它只有三个方法和一个属性)
  • 在调用BackgroundWorker之前(因此,在UI线程上),调用SynchronizationContext.Current并将内容实例保存在实例变量中。 然后SynchronizationContext将为您提供Send方法,它将完全执行Invoke所做的操作。

避免GUI中的跨线程问题的最佳方法是使用SynchronizationContext 。