BackgroundWorker OnWorkCompleted抛出跨线程异常

时间:2009-05-04 04:20:54

标签: c# winforms backgroundworker arcgis multithreading

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

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

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

编辑:

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

第一个按钮运行BackgroundWorker,睡眠时间为500毫秒并更新计数器。 在RunWorkerCompleted方法中,它检查InvokeRequired,并更新标题以显示方法最初在主线程或工作线程内运行。 第二个按钮只会打开Form2,其中不包含任何内容。

首先,对RunWorkerCompletedare的所有调用都是在主线程中进行的(正如预期的那样 - 这是RunWorkerComplete方法的最后一点,至少我从MSDN Here的理解来看。 1}})

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

我设法在我RunWorkerCompleted的{​​{1}}命令中指出了问题 - 如果我改用form.Show();,我就没有问题(BackgroundTesterBtn始终在主线)。我需要在我的ArcMap项目中使用ShowDialog(),这样用户就不会绑定到表单。

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

{{3}}是我的简单sln(VS2005sp1) - 它需要

ESRI.ArcGIS.ADF(9.2.4.1420)

ESRI.ArcGIS.ArcMapUI(9.2.3.1380)

ESRI.ArcGIS.SystemUI(9.2.3.1380)

4 个答案:

答案 0 :(得分:7)

  

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

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

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

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

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

但是,当您使用winforms时,在OnWorkCompleted处理程序中,您可以使用我上面提到的BeginInvoke功能让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
}

Also, don't forget this:

  

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

答案 1 :(得分:2)

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

如果您想使用DAL图层中的Invoke并想要从那里更新用户界面,您有三种选择:

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

答案 2 :(得分:2)

看起来像个错误:

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

答案 3 :(得分:1)

避免GUI中交叉线程问题的最佳方法是use SynchronizationContext