BackgroundWorker OnWorkCompleted 抛出跨线程异常
我有一个用于数据库分页的简单 UserControl,它使用控制器来执行实际的 DAL 调用。 我使用 BackgroundWorker 来执行繁重的工作,并在 OnWorkCompleted 事件中重新启用一些按钮,更改 TextBox.Text 属性并为父表单引发一个事件。
表单 A 保存我的用户控件。 当我单击某个打开表单 B 的按钮时,即使我没有在“那里”执行任何操作并只是将其关闭,并尝试从数据库中引入下一页,OnWorkCompleted
也会被调用在工作线程(而不是我的主线程)上,并抛出跨线程异常。
目前,我在处理程序中添加了对 InvokeRequired
的检查,但是 OnWorkCompleted
的全部目的不是要在主线程上调用吗? 为什么它不能按预期工作?
编辑:
我已成功将问题范围缩小到 arcgis 和 BackgroundWorker
。 我有以下解决方案,向 arcmap 添加一个命令,打开一个带有两个按钮的简单 Form1
。
第一个按钮运行一个 BackgroundWorker
,它会休眠 500 毫秒并更新计数器。 在 RunWorkerCompleted
方法中,它检查 InvokeRequired
,并更新标题以显示该方法最初是在主线程还是工作线程内运行。 第二个按钮仅打开 Form2
,其中不包含任何内容。
首先,对 RunWorkerCompletedare 的所有调用都是在主线程内进行的(正如预期的那样 - 这就是 RunWorkerComplete 方法的全部要点,至少根据我从 MSDN on BackgroundWorker
)
打开后并关闭 Form2
,RunWorkerCompleted
始终在工作线程上调用。 我想补充一点,我可以按原样保留此解决方案(检查 RunWorkerCompleted
方法中的 InvokeRequired
),但我想了解为什么会发生这种情况我的期望。 在我的“真实”代码中,我希望始终知道在主线程上调用了 RunWorkerCompleted 方法。
我设法在 BackgroundTesterBtn
中的 form.Show();
命令中找到问题 - 如果我使用 ShowDialog()
,我没问题(RunWorkerCompleted
始终在主线程上运行)。 我确实需要在 ArcMap 项目中使用 Show()
,这样用户就不会被绑定到表单。
我还尝试在正常的 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)
I have a simple UserControl for database paging, that uses a controller to perform the actual DAL calls. I use a BackgroundWorker
to perform the heavy lifting, and on the OnWorkCompleted
event I re-enable some buttons, change a TextBox.Text
property and raise an event for the parent form.
Form A holds my UserControl. When I click on some button that opens form B, even if I don't do anything "there" and just close it, and try to bring in the next page from my database, the OnWorkCompleted
gets called on the worker thread (and not my Main thread), and throws a cross-thread exception.
At the moment I added a check for InvokeRequired
at the handler there, but isn't the whole point of OnWorkCompleted
is to be called on the Main thread? Why wouldn't it work as expected?
EDIT:
I have managed to narrow down the problem to arcgis and BackgroundWorker
. I have the following solution wich adds a Command to arcmap, that opens a simple Form1
with two buttons.
The first button runs a BackgroundWorker
that sleeps for 500ms and updates a counter.
In the RunWorkerCompleted
method it checks for InvokeRequired
, and updates the title to show whethever the method was originaly running inside the main thread or the worker thread.
The second button just opens Form2
, which contains nothing.
At first, all the calls to RunWorkerCompletedare
are made inside the main thread (As expected - thats the whold point of the RunWorkerComplete method, At least by what I understand from the MSDN on BackgroundWorker
)
After opening and closing Form2
, the RunWorkerCompleted
is always being called on the worker thread. I want to add that I can just leave this solution to the problem as is (check for InvokeRequired
in the RunWorkerCompleted
method), but I want to understand why it is happening against my expectations. In my "real" code I'd like to always know that the RunWorkerCompleted
method is being called on the main thread.
I managed to pin point the problem at the form.Show();
command in my BackgroundTesterBtn
- if I use ShowDialog()
instead, I get no problem (RunWorkerCompleted
always runs on the main thread). I do need to use Show()
in my ArcMap project, so that the user will not be bound to the form.
I also tried to reproduce the bug on a normal WinForms project. I added a simple project that just opens the first form without ArcMap, but in that case I couldn't reproduce the bug - the RunWorkerCompleted
ran on the main thread, whether I used Show()
or ShowDialog()
, before and after opening Form2
. I tried adding a third form to act as a main form before my Form1
, but it didn't change the outcome.
Here is my simple sln (VS2005sp1) - it requires
ESRI.ArcGIS.ADF(9.2.4.1420)
ESRI.ArcGIS.ArcMapUI(9.2.3.1380)
ESRI.ArcGIS.SystemUI (9.2.3.1380)
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(4)
不,不是。
你不能只是在任何旧线程上运行任何旧东西。 线程不是你可以简单地说“请运行这个”的礼貌对象。
一个更好的线程心智模型是货运列车。 一旦开始,它就走上了自己的轨道。 你无法改变它的进程或阻止它。 如果你想影响它,你要么必须等到它到达下一个火车站(例如:让它手动检查某些事件),要么让它脱轨(
Thread.Abort
和CrossThread异常有与火车脱轨的后果几乎相同......小心!)。Winforms 控件在某种程度上支持这种行为(它们有
Control.BeginInvoke
,它允许您在 UI 线程上运行任何函数),但这只能起作用,因为它们有一个特殊的钩子Windows UI 消息泵并编写一些特殊的处理程序。 按照上面的类比,他们的火车在车站报到并定期寻找新的方向,您可以使用该设施发布您自己的方向。BackgroundWorker
被设计为通用目的(它不能绑定到 Windows GUI),因此它不能使用 WindowsControl.BeginInvoke
功能。 它必须假设您的主线程是一个不可阻挡的“火车”,在做自己的事情,因此完成的事件必须在工作线程中运行,或者根本不运行。但是,当您使用 winforms 时,在
OnWorkCompleted
处理程序中,您可以使用我提到的BeginInvoke
功能让窗口执行另一个回调多于。 像这样:另外,不要忘记这一点:
No, it's not.
You can't just go running any old thing on any old thread. Threads are not polite objects that you can simply say "run this, please".
A better mental model of a thread is a freight train. Once it's going, it's off on it's own track. You can't change it's course or stop it. If you want to influence it, you either have to wait til it gets to the next train station (eg: have it manually check for some events), or derail it (
Thread.Abort
and CrossThread exceptions have much the same consequences as derailing a train... beware!).Winforms controls sort of support this behaviour (They have
Control.BeginInvoke
which lets you run any function on the UI thread), but that only works because they have a special hook into the windows UI message pump and write some special handlers. To go with the above analogy, their train checks in at the station and looks for new directions periodically, and you can use that facility to post it your own directions.The
BackgroundWorker
is designed to be general purpose (it can't be tied to the windows GUI) so it can't use the windowsControl.BeginInvoke
features. It has to assume that your main thread is an unstoppable 'train' doing it's own thing, so the completed event has to run in the worker thread or not at all.However, as you're using winforms, in your
OnWorkCompleted
handler, you can get the Window to execute another callback using theBeginInvoke
functionality I mentioned above. Like this:Also, don't forget this:
它看起来像一个错误:
http://connect.microsoft.com/ VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=116930
http ://thedatafarm.com/devlifeblog/archive/2005/12/21/39532.aspx
所以我建议使用防弹(伪代码):
It looks like a bug:
http://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=116930
http://thedatafarm.com/devlifeblog/archive/2005/12/21/39532.aspx
So I suggest using the bullet-proof (pseudocode):
BackgroundWorker
检查委托实例是否指向支持接口ISynchronizeInvoke
的类。 您的 DAL 层可能没有实现该接口。 通常,您会在Form
上使用BackgroundWorker
,它确实支持该接口。如果您想使用 DAL 层中的 BackgroundWorker 并希望从那里更新 UI,您有三个选择:
ISynchronizeInvoke
,并在调用BackgroundWorker
之前手动重定向调用(只有三个方法和一个属性)SynchronizationContext.Current
并将内容实例保存在实例变量中。 然后,SynchronizationContext
将为您提供Send
方法,该方法将完全执行Invoke
的操作。The
BackgroundWorker
checks whether the delegate instance, points to a class which supports the interfaceISynchronizeInvoke
. Your DAL layer probably does not implement that interface. Normally, you would use theBackgroundWorker
on aForm
, which does support that interface.In case you want to use the
BackgroundWorker
from the DAL layer and want to update the UI from there, you have three options:Invoke
methodISynchronizeInvoke
on the DAL class, and redirect the calls manually (it's only three methods and a property)BackgroundWorker
(so, on the UI thread), to callSynchronizationContext.Current
and to save the content instance in an instance variable. TheSynchronizationContext
will then give you theSend
method, which will exactly do whatInvoke
does.避免 GUI 中跨线程问题的最佳方法是 使用 SynchronizationContext。
The best approach to avoid issues with cross-threading in GUI is to use SynchronizationContext.