BackgroundWorker OnWorkCompleted 抛出跨线程异常
-
03-07-2019 - |
题
我有一个用于数据库分页的简单 UserControl,它使用控制器来执行实际的 DAL 调用。我用一个 BackgroundWorker
执行繁重的工作,并在 OnWorkCompleted
事件我重新启用一些按钮,更改 TextBox.Text
属性并为父窗体引发一个事件。
表单 A 保存我的用户控件。当我单击某个打开表单 B 的按钮时,即使我“那里”没有执行任何操作并只是将其关闭,并尝试从我的数据库中引入下一页, OnWorkCompleted
在工作线程(而不是我的主线程)上调用,并抛出跨线程异常。
目前我添加了一张支票 InvokeRequired
在处理程序那里,但这不是重点 OnWorkCompleted
是在主线程上调用吗?为什么它不能按预期工作?
编辑:
我已经设法将问题范围缩小到 arcgis 和 BackgroundWorker
. 。我有以下解决方案,将命令添加到 arcmap,打开一个简单的 Form1
有两个按钮。
第一个按钮运行 BackgroundWorker
休眠 500 毫秒并更新计数器。在里面 RunWorkerCompleted
它检查的方法 InvokeRequired
, ,并更新标题以显示该方法最初是在主线程还是工作线程内运行。第二个按钮刚刚打开 Form2
, ,其中不包含任何内容。
首先,所有的调用 RunWorkerCompletedare
是在主线程内制作的(正如预期的那样 - 这就是 RunWorkerComplete 方法的重点,至少根据我从 微软软件定义网络 在 BackgroundWorker
)
打开和关闭后 Form2
, , 这 RunWorkerCompleted
总是在工作线程上被调用。我想补充一点,我可以按原样保留这个问题的解决方案(检查 InvokeRequired
在里面 RunWorkerCompleted
方法),但我想了解为什么它会违背我的预期。在我的“真实”代码中,我想始终知道 RunWorkerCompleted
方法正在主线程上被调用。
我设法找出问题所在 form.Show();
命令在我的 BackgroundTesterBtn
- 如果我使用 ShowDialog()
相反,我没有遇到任何问题(RunWorkerCompleted
始终在主线程上运行)。我确实需要使用 Show()
在我的ArcMap项目中,这样用户就不会被绑定到表单。
我还尝试在正常的 WinForms 项目上重现该错误。我添加了一个简单的项目,只在没有 ArcMap 的情况下打开第一个表单,但在这种情况下我无法重现该错误 - RunWorkerCompleted
在主线程上运行,无论我使用 Show()
或者 ShowDialog()
, ,打开前和打开后 Form2
. 。我尝试在我的之前添加第三个表单作为主表单 Form1
, ,但这并没有改变结果。
这里 是我的简单 sln (VS2005sp1) - 它需要
ESRI.ArcGIS.ADF(9.2.4.1420)
ESRI.ArcGIS.ArcMapUI(9.2.3.1380)
ESRI.ArcGIS.SystemUI (9.2.3.1380)
解决方案
看起来像个错误:
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
和CrossThread异常有与出轨火车相同的后果......小心!)。
Winforms控件排序支持这种行为(他们有 Control.BeginInvoke
,它允许你在UI线程上运行任何函数),但这只能起作用,因为他们有一个特殊挂钩进入windows 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
}
在访问Result属性之前,RunWorkerCompleted事件处理程序应始终检查Error和Cancelled属性。如果引发了异常或操作被取消,则访问Result属性会引发异常。
这 BackgroundWorker
检查委托实例是否指向支持该接口的类 ISynchronizeInvoke
. 。您的 DAL 层可能没有实现该接口。通常,您会使用 BackgroundWorker
在一个 Form
, ,它确实支持该接口。
如果您想使用 BackgroundWorker
从 DAL 层并想要从那里更新 UI,您有三个选择:
- 你会继续打电话
Invoke
方法 - 实现接口
ISynchronizeInvoke
在 DAL 类上,并手动重定向调用(只有三个方法和一个属性) - 在调用之前
BackgroundWorker
(所以,在 UI 线程上),调用SynchronizationContext.Current
并将内容实例保存在实例变量中。这SynchronizationContext
然后会给你Send
方法,它会做什么Invoke
做。
避免GUI中交叉线程问题的最佳方法是使用SynchronizationContext 。