BackgroundWorker 的 OnWorkCompleted 抛出跨线程异常

3
我有一个简单的用户控件用于数据库分页,它使用控制器执行实际的DAL调用。我使用BackgroundWorker来执行繁重的工作,在OnWorkCompleted事件上重新启用一些按钮,更改TextBox.Text属性并为父窗体引发事件。
表格A持有我的用户控件。当我点击打开表格B的某个按钮时,即使我什么也不做,只是关闭它,并尝试从我的数据库中带入下一页,OnWorkCompleted会在工作线程上被调用(而不是我的主线程),并抛出跨线程异常。
目前,我在那里的处理程序中添加了InvokeRequired的检查,但是OnWorkCompleted的整个目的不是应该在主线程上被调用吗?为什么不能按预期工作?
编辑:
我已经成功将问题缩小到arcgis和BackgroundWorker。我有以下解决方案,其中添加了一个命令到arcmap,该命令打开一个简单的Form1,其中包含两个按钮。
第一个按钮运行一个BackgroundWorker,它会休眠500毫秒并更新计数器。在RunWorkerCompleted方法中,它检查InvokeRequired,并更新标题以显示该方法最初是在主线程还是工作线程内运行。第二个按钮只是打开Form2,其中什么也没有。
起初,所有对RunWorkerCompleted的调用都在主线程中进行(正如预期的那样 - 这是BackgroundWorkerMSDN所解释的)。
打开并关闭 Form2 后,RunWorkerCompleted 始终在工作线程上调用。我想补充说明的是,我可以只按原样解决此问题(在 RunWorkerCompleted 方法中检查 InvokeRequired),但我想知道为什么会违背我的预期发生这种情况。在我的“真实”代码中,我希望始终知道 RunWorkerCompleted 方法在主线程上被调用。
我成功地将问题定位到 BackgroundTesterBtn 中的 form.Show(); 命令 - 如果我使用 ShowDialog(),则不会出现问题(RunWorkerCompleted 总是在主线程上运行)。但是我需要在 ArcMap 项目中使用 Show(),以便用户不受限于该表单。
我还尝试在普通的WinForms项目中重现了这个bug。我添加了一个简单的项目,只是打开第一个没有ArcMap的窗体,但在那种情况下,无论我使用Show()还是ShowDialog(),在打开Form2之前和之后,RunWorkerCompleted都在主线程上运行。我尝试添加第三个窗体作为Form1之前的主窗体,但结果没有改变。
这是我的简单sln (VS2005sp1) Here - 它需要...

ESRI.ArcGIS.ADF(9.2.4.1420)

ESRI.ArcGIS.ArcMapUI(9.2.3.1380)

ESRI.ArcGIS.SystemUI(9.2.3.1380)

4个回答

6
“OnWorkCompleted” 的整个意义难道不是要在主线程中调用吗?为什么不能按预期运行?
不是这样的。 您不能简单地在任何线程上运行任何东西。线程不是可以随便说“请运行此函数”的可爱对象。
一个更好的线程模型是货车。一旦它出发了,就会进入自己的轨道。你不能改变它的路线或停止它。如果你想影响它,你要么等待它到达下一个火车站(例如:手动检查某些事件),要么让它脱轨(Thread.Abort和CrossThread异常与让火车脱轨有相同的后果...注意!)。
Winforms控件“有点”支持这种行为(它们具有Control.BeginInvoke,可以让您在UI线程上运行任何函数),但这只能工作因为它们对Windows UI消息泵有特殊的挂钩并编写了一些特殊的处理程序。根据上述类比,它们的车辆在车站办理业务,并定期寻找新方向,您可以使用该设施将其发布给您自己的方向。
BackgroundWorker被设计为通用(它不能绑定到Windows GUI),因此它不能使用Windows Control.BeginInvoke功能。它必须假设您的主线程是无法停止的“火车”,正在做自己的事情,因此完成事件必须在工作线程中运行,否则将不会运行。
但是,由于您使用的是winforms,在您的OnWorkCompleted处理程序中,您可以让窗口使用我上面提到的BeginInvoke功能执行另一个回调。就像这样:
// 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 和 Cancelled 属性。如果引发了异常或操作被取消,则访问 Result 属性会引发异常。


在查看BackgroundWorker的MSDN时,我发现我应该使用RunWorkerCompleted事件来处理表单。"您必须小心,不要在DoWork事件处理程序中操作任何用户界面对象。相反,通过ProgressChanged和RunWorkerCompleted事件与用户界面通信。" 此外 - 每次我尝试一个更简单的场景,它都可以正常工作并在主线程上调用。就像在那里的MSDN示例中,或者在我的应用程序中大多数时间一样。 这类似于www.developmentnow.com/g/36_2008_3_0_0_1056508/Update-GUI-from-RunWorkerCompleted.htm。 - Noam Gal
很有趣...我创建了一个测试应用程序(控制台应用程序),其中包含一个简单的后台工作器,并且它在工作线程中触发。也许BackgroundWorker构造函数有一些神奇的东西,如果它是在UI线程上创建还是不是会改变它的行为? - Orion Edwards

2

2
BackgroundWorker会检查委托实例是否指向支持接口ISynchronizeInvoke的类。您的DAL层可能没有实现该接口。通常,您会在支持该接口的Form上使用BackgroundWorker
如果您想从DAL层使用BackgroundWorker并想要从那里更新UI,则有三个选项:
  • 继续调用Invoke方法
  • 在DAL类上实现接口ISynchronizeInvoke,并手动重定向调用(只有三个方法和一个属性)
  • 在调用BackgroundWorker之前(因此,在UI线程上),调用SynchronizationContext.Current并将内容实例保存在实例变量中。然后,SynchronizationContext将为您提供Send方法,它将完全执行Invoke的操作。

我正在一个继承了ISyncronizeInvoke的UserControl中使用BackgroundWorker。我的分页控制器不知道它正在运行在一个工作线程内,也不关心这一点。它只是执行一个分页查询。 异常是在UserControl中从OnWorkCompleted事件抛出的。 - Noam Gal

1

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